d / app.py
pabla1322's picture
Upload 14 files
e5de808 verified
Raw
History Blame Contribute Delete
18.9 kB
"""
app.py – Flask dashboard for Ubuntu Desktop on Hugging Face Spaces
Serves: stats API, log viewer, service restart, and embedded desktop
"""
import os
import subprocess
import time
import datetime
import glob
import json
from pathlib import Path
import psutil
from flask import Flask, jsonify, render_template_string, request
app = Flask(__name__)
START_TIME = time.time()
# ── HTML Template ─────────────────────────────────────────────────────────────
DASHBOARD_HTML = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ubuntu Space Dashboard</title>
<style>
:root {
--bg: #0d0d1a;
--surface: #141426;
--border: #1e1e3c;
--accent: #6366f1;
--accent2: #22d3ee;
--text: #e2e2f0;
--muted: #6b6b90;
--green: #34d399;
--red: #f87171;
--yellow: #fbbf24;
--radius: 10px;
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: 'Segoe UI', system-ui, sans-serif;
font-size: 14px;
min-height: 100vh;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
/* ── Layout ── */
.layout { display: grid; grid-template-columns: 220px 1fr; min-height: 100vh; }
.sidebar {
background: var(--surface);
border-right: 1px solid var(--border);
padding: 1.5rem 1rem;
display: flex; flex-direction: column; gap: 0.25rem;
}
.sidebar h2 { font-size: .75rem; color: var(--muted); text-transform: uppercase;
letter-spacing: .1em; margin: 1rem 0 .4rem .5rem; }
.sidebar a {
display: flex; align-items: center; gap: .6rem;
padding: .55rem .75rem; border-radius: 7px; color: var(--text);
transition: .15s;
}
.sidebar a:hover, .sidebar a.active { background: var(--border); text-decoration: none; }
.brand { font-size: 1.1rem; font-weight: 700; color: var(--accent);
margin-bottom: .5rem; padding: .5rem; }
.brand span { color: var(--text); }
.main { padding: 1.5rem 2rem; overflow-y: auto; }
/* ── Header ── */
.page-header { display: flex; align-items: center; justify-content: space-between;
margin-bottom: 1.5rem; }
.page-header h1 { font-size: 1.25rem; font-weight: 600; }
.uptime { font-size: .8rem; color: var(--muted); }
/* ── Cards grid ── */
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem; margin-bottom: 1.5rem; }
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.1rem 1.3rem;
}
.card-label { font-size: .7rem; text-transform: uppercase; letter-spacing: .08em;
color: var(--muted); margin-bottom: .35rem; }
.card-value { font-size: 1.6rem; font-weight: 700; line-height: 1; }
.card-sub { font-size: .75rem; color: var(--muted); margin-top: .3rem; }
.bar-wrap { margin-top: .6rem; background: var(--border); border-radius: 4px; height: 6px; }
.bar-fill { height: 6px; border-radius: 4px; transition: width .4s; }
/* ── Services table ── */
.section { background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); margin-bottom: 1.5rem; }
.section-header { padding: .9rem 1.3rem; border-bottom: 1px solid var(--border);
font-weight: 600; display: flex; align-items: center; gap: .5rem; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: .75rem 1.3rem; border-bottom: 1px solid var(--border); }
th { font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(99,102,241,.04); }
/* ── Badges ── */
.badge {
display: inline-flex; align-items: center; gap: .35rem;
padding: .2rem .65rem; border-radius: 20px; font-size: .75rem; font-weight: 600;
}
.badge-green { background: rgba(52,211,153,.12); color: var(--green); }
.badge-red { background: rgba(248,113,113,.12); color: var(--red); }
.badge-yellow { background: rgba(251,191,36,.12); color: var(--yellow); }
.dot { width:7px; height:7px; border-radius:50%; background:currentColor; }
/* ── Buttons ── */
.btn {
padding: .4rem .9rem; border-radius: 6px; border: none; cursor: pointer;
font-size: .8rem; font-weight: 600; transition: .15s;
}
.btn-primary { background: var(--accent); color: #fff; }
.btn-primary:hover { background: #5254d4; }
.btn-ghost { background: var(--border); color: var(--text); }
.btn-ghost:hover { background: #2a2a4e; }
.btn-danger { background: rgba(248,113,113,.15); color: var(--red); }
.btn-danger:hover { background: rgba(248,113,113,.25); }
/* ── Log viewer ── */
.log-box {
background: #060610; border-radius: 0 0 var(--radius) var(--radius);
padding: 1rem 1.3rem; font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: .78rem; color: #9090c0; height: 280px;
overflow-y: auto; white-space: pre-wrap; word-break: break-all;
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
.log-controls { padding: .75rem 1.3rem; border-bottom: 1px solid var(--border);
display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
select { background: var(--border); color: var(--text); border: none;
border-radius: 5px; padding: .35rem .6rem; font-size: .8rem; }
/* ── Launch bar ── */
.launch-bar {
background: linear-gradient(135deg, rgba(99,102,241,.15), rgba(34,211,238,.08));
border: 1px solid rgba(99,102,241,.3);
border-radius: var(--radius); padding: 1.3rem 1.5rem;
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem;
}
.launch-bar h2 { font-size: 1rem; font-weight: 600; }
.launch-bar p { font-size: .8rem; color: var(--muted); margin-top: .2rem; }
/* Responsive */
@media (max-width: 640px) {
.layout { grid-template-columns: 1fr; }
.sidebar { display: none; }
.main { padding: 1rem; }
}
</style>
</head>
<body>
<div class="layout">
<!-- Sidebar -->
<aside class="sidebar">
<div class="brand">🐧 <span>Ubuntu Space</span></div>
<h2>Navigation</h2>
<a href="/dashboard" class="active">πŸ“Š Dashboard</a>
<a href="/" >πŸ–₯ Desktop</a>
<a href="/vnc/vnc.html" target="_blank">πŸ”— noVNC Direct</a>
<a href="#logs">πŸ“„ Logs</a>
<h2>Info</h2>
<a href="https://huggingface.co/docs/hub/spaces" target="_blank">πŸ“– HF Spaces Docs</a>
</aside>
<!-- Main -->
<main class="main">
<div class="page-header">
<h1>System Dashboard</h1>
<span class="uptime" id="uptime-label">Loading…</span>
</div>
<!-- Launch bar -->
<div class="launch-bar">
<div>
<h2>Ubuntu 24.04 XFCE Desktop</h2>
<p>PRoot-Distro style isolated environment on Hugging Face Spaces</p>
</div>
<div style="display:flex;gap:.75rem;flex-wrap:wrap">
<a href="/" class="btn btn-primary">πŸ–₯ Open Desktop</a>
<button class="btn btn-ghost" onclick="refreshAll()">πŸ”„ Refresh</button>
</div>
</div>
<!-- Stats cards -->
<div class="cards" id="stats-cards">
<div class="card">
<div class="card-label">CPU Usage</div>
<div class="card-value" id="cpu-val">–</div>
<div class="bar-wrap"><div class="bar-fill" id="cpu-bar" style="background:var(--accent)"></div></div>
<div class="card-sub" id="cpu-cores">–</div>
</div>
<div class="card">
<div class="card-label">Memory</div>
<div class="card-value" id="ram-val">–</div>
<div class="bar-wrap"><div class="bar-fill" id="ram-bar" style="background:var(--accent2)"></div></div>
<div class="card-sub" id="ram-sub">–</div>
</div>
<div class="card">
<div class="card-label">Storage (/data)</div>
<div class="card-value" id="disk-val">–</div>
<div class="bar-wrap"><div class="bar-fill" id="disk-bar" style="background:var(--green)"></div></div>
<div class="card-sub" id="disk-sub">–</div>
</div>
<div class="card">
<div class="card-label">Session Uptime</div>
<div class="card-value" id="uptime-val" style="font-size:1.2rem">–</div>
<div class="card-sub">Space started</div>
</div>
</div>
<!-- Services -->
<div class="section">
<div class="section-header">βš™οΈ Services</div>
<table>
<thead>
<tr><th>Service</th><th>Status</th><th>PID</th><th>Action</th></tr>
</thead>
<tbody id="services-tbody">
<tr><td colspan="4" style="color:var(--muted);text-align:center;padding:1.5rem">Loading…</td></tr>
</tbody>
</table>
</div>
<!-- Log viewer -->
<div class="section" id="logs">
<div class="section-header">πŸ“„ Logs</div>
<div class="log-controls">
<select id="log-select">
<option value="supervisord">supervisord</option>
<option value="xvnc">Xvnc</option>
<option value="xfce4">XFCE4</option>
<option value="websockify">websockify</option>
<option value="dashboard">dashboard</option>
<option value="nginx">nginx</option>
</select>
<button class="btn btn-ghost" onclick="loadLog()">πŸ”ƒ Reload</button>
<label style="display:flex;align-items:center;gap:.4rem;font-size:.8rem;color:var(--muted)">
<input type="checkbox" id="auto-scroll" checked> Auto-scroll
</label>
<label style="display:flex;align-items:center;gap:.4rem;font-size:.8rem;color:var(--muted)">
<input type="checkbox" id="auto-refresh-log"> Live (5s)
</label>
</div>
<div class="log-box" id="log-box">Select a log file above…</div>
</div>
</main>
</div>
<script>
let logRefreshTimer = null;
// ── Fetch stats ──────────────────────────────────────────────
async function fetchStats() {
try {
const r = await fetch('/api/stats');
const d = await r.json();
// CPU
document.getElementById('cpu-val').textContent = d.cpu.percent + '%';
document.getElementById('cpu-bar').style.width = d.cpu.percent + '%';
document.getElementById('cpu-cores').textContent = `${d.cpu.count} cores`;
// RAM
const ramPct = d.memory.percent;
document.getElementById('ram-val').textContent = ramPct + '%';
document.getElementById('ram-bar').style.width = ramPct + '%';
document.getElementById('ram-sub').textContent =
`${d.memory.used_gb} / ${d.memory.total_gb} GB`;
// Disk
const diskPct = d.disk.percent;
document.getElementById('disk-val').textContent = diskPct + '%';
document.getElementById('disk-bar').style.width = diskPct + '%';
document.getElementById('disk-sub').textContent =
`${d.disk.used_gb} / ${d.disk.total_gb} GB`;
// Uptime
document.getElementById('uptime-val').textContent = d.uptime.human;
document.getElementById('uptime-label').textContent = 'Uptime: ' + d.uptime.human;
// Services
renderServices(d.services);
} catch(e) {
console.error('Stats fetch failed:', e);
}
}
function badgeFor(state) {
if (state === 'RUNNING') return '<span class="badge badge-green"><span class="dot"></span>Running</span>';
if (state === 'STOPPED') return '<span class="badge badge-red"><span class="dot"></span>Stopped</span>';
return `<span class="badge badge-yellow"><span class="dot"></span>${state}</span>`;
}
function renderServices(services) {
const tbody = document.getElementById('services-tbody');
tbody.innerHTML = services.map(s => `
<tr>
<td><strong>${s.name}</strong><br><small style="color:var(--muted)">${s.description}</small></td>
<td>${badgeFor(s.state)}</td>
<td style="font-family:monospace;color:var(--muted)">${s.pid || 'β€”'}</td>
<td>
<button class="btn btn-ghost" onclick="restartService('${s.name}')">β†Ί Restart</button>
</td>
</tr>
`).join('');
}
async function restartService(name) {
if (!confirm(`Restart ${name}?`)) return;
try {
const r = await fetch(`/api/restart/${name}`, { method: 'POST' });
const d = await r.json();
alert(d.message || d.error);
setTimeout(fetchStats, 1500);
} catch(e) {
alert('Restart failed: ' + e);
}
}
// ── Fetch logs ───────────────────────────────────────────────
async function loadLog() {
const name = document.getElementById('log-select').value;
try {
const r = await fetch(`/api/logs/${name}`);
const d = await r.json();
const box = document.getElementById('log-box');
box.textContent = d.content || '(empty)';
if (document.getElementById('auto-scroll').checked)
box.scrollTop = box.scrollHeight;
} catch(e) {
document.getElementById('log-box').textContent = 'Error loading log: ' + e;
}
}
document.getElementById('log-select').addEventListener('change', loadLog);
document.getElementById('auto-refresh-log').addEventListener('change', function() {
clearInterval(logRefreshTimer);
if (this.checked) logRefreshTimer = setInterval(loadLog, 5000);
});
function refreshAll() { fetchStats(); loadLog(); }
// ── Poll stats every 5 s ──────────────────────────────────────
fetchStats();
loadLog();
setInterval(fetchStats, 5000);
</script>
</body>
</html>"""
# ── Helpers ───────────────────────────────────────────────────────────────────
def _supervisor_ctl(*args) -> str:
try:
result = subprocess.run(
["supervisorctl"] + list(args),
capture_output=True, text=True, timeout=10
)
return result.stdout + result.stderr
except Exception as e:
return str(e)
def _supervisor_status() -> list[dict]:
"""Parse `supervisorctl status` into structured list."""
output = _supervisor_ctl("status")
services = []
descriptions = {
"xvnc": "Virtual framebuffer + VNC server",
"xfce4": "XFCE4 desktop session",
"websockify": "noVNC ↔ VNC WebSocket bridge",
"dashboard": "Flask stats dashboard",
"nginx": "Reverse proxy (port 7860)",
}
for line in output.strip().splitlines():
if not line.strip():
continue
parts = line.split()
if len(parts) < 2:
continue
name = parts[0]
state = parts[1]
pid = None
for p in parts:
if p.startswith("pid"):
try:
pid = parts[parts.index(p) + 1].rstrip(",")
except Exception:
pass
services.append({
"name": name,
"state": state,
"pid": pid,
"description": descriptions.get(name, ""),
})
return services
def _human_uptime(seconds: float) -> str:
d = int(seconds // 86400)
h = int((seconds % 86400) // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
if d:
return f"{d}d {h}h {m}m"
if h:
return f"{h}h {m}m {s}s"
return f"{m}m {s}s"
# ── Routes ────────────────────────────────────────────────────────────────────
@app.route("/dashboard")
def dashboard():
return render_template_string(DASHBOARD_HTML)
@app.route("/health")
def health():
return jsonify({"status": "ok", "ts": datetime.datetime.utcnow().isoformat()})
@app.route("/api/stats")
def api_stats():
cpu = psutil.cpu_percent(interval=0.3)
mem = psutil.virtual_memory()
try:
disk = psutil.disk_usage("/data")
disk_data = {
"percent": disk.percent,
"used_gb": round(disk.used / 1e9, 2),
"total_gb": round(disk.total / 1e9, 2),
"free_gb": round(disk.free / 1e9, 2),
}
except Exception:
disk_data = {"percent": 0, "used_gb": 0, "total_gb": 0, "free_gb": 0}
uptime_secs = time.time() - START_TIME
return jsonify({
"cpu": {
"percent": round(cpu, 1),
"count": psutil.cpu_count(),
},
"memory": {
"percent": mem.percent,
"used_gb": round(mem.used / 1e9, 2),
"total_gb": round(mem.total / 1e9, 2),
"free_gb": round(mem.available / 1e9, 2),
},
"disk": disk_data,
"uptime": {
"seconds": round(uptime_secs),
"human": _human_uptime(uptime_secs),
},
"services": _supervisor_status(),
"ts": datetime.datetime.utcnow().isoformat(),
})
@app.route("/api/logs/<name>")
def api_logs(name: str):
# Allowlist to prevent path traversal
allowed = {"supervisord", "xvnc", "xfce4", "websockify", "dashboard", "nginx",
"xvnc_err", "xfce4_err", "websockify_err", "dashboard_err", "nginx_err"}
if name not in allowed:
return jsonify({"error": "Unknown log name"}), 400
log_path = Path(f"/data/logs/{name}.log")
if not log_path.exists():
return jsonify({"content": f"Log file not found: {log_path}"})
try:
# Return last 100 lines
lines = log_path.read_text(errors="replace").splitlines()
return jsonify({"content": "\n".join(lines[-100:])})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/restart/<name>", methods=["POST"])
def api_restart(name: str):
allowed = {"xvnc", "xfce4", "websockify", "dashboard", "nginx"}
if name not in allowed:
return jsonify({"error": "Unknown service"}), 400
out = _supervisor_ctl("restart", name)
return jsonify({"message": out.strip()})
# ── Entry point ───────────────────────────────────────────────────────────────
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, debug=False, threaded=True)