Spaces:
Sleeping
Sleeping
| """Agent Dashboards — a tiny agent-updatable dashboard service. | |
| Agents create/update dashboards and push data over a bearer-authed REST API. Each dashboard is | |
| served at its own URL as rendered HTML (and JSON). Mutations update an in-memory store immediately; | |
| the store is synced to durable backups (GitHub primary + HF Dataset fallback) once a day at a fixed | |
| hour, on graceful shutdown, or on demand via POST /api/sync. | |
| """ | |
| import os | |
| import io | |
| import json | |
| import html | |
| import time | |
| import base64 | |
| import threading | |
| from datetime import datetime | |
| from zoneinfo import ZoneInfo | |
| import requests | |
| from fastapi import FastAPI, Header, HTTPException, Request | |
| from fastapi.responses import HTMLResponse | |
| # ---- config ----------------------------------------------------------------- | |
| AGENT_KEY = os.environ.get("AGENT_KEY", "") | |
| STORE_FILE = os.environ.get("STORE_FILE", "store.json") | |
| # primary backup: GitHub repo (Contents API) | |
| GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "") | |
| GITHUB_REPO = os.environ.get("GITHUB_REPO", "JsonLord/dashboard-backups") | |
| GITHUB_BRANCH = os.environ.get("GITHUB_BRANCH", "main") | |
| GH_API = f"https://api.github.com/repos/{GITHUB_REPO}/contents/{STORE_FILE}" | |
| # fallback backup: HF Dataset repo | |
| HF_TOKEN = os.environ.get("HF_TOKEN", "") | |
| DATASET_REPO = os.environ.get("DATASET_REPO", "") | |
| # scheduled sync: once a day at SYNC_HOUR in SYNC_TZ | |
| SYNC_HOUR = int(os.environ.get("SYNC_HOUR", "20")) | |
| try: | |
| SYNC_TZ = ZoneInfo(os.environ.get("SYNC_TZ", "UTC")) | |
| except Exception: | |
| SYNC_TZ = ZoneInfo("UTC") | |
| app = FastAPI(title="Agent Dashboards", docs_url="/docs") | |
| _lock = threading.Lock() | |
| _store = {"dashboards": {}} # id -> dashboard dict | |
| _sha = None # GitHub blob sha for in-place updates | |
| _dirty = False # unsynced in-memory changes? | |
| _last_sync = None # iso timestamp of last successful remote sync | |
| def _payload(): | |
| return json.dumps(_store, ensure_ascii=False, indent=2).encode("utf-8") | |
| def _valid(data): | |
| return isinstance(data, dict) and isinstance(data.get("dashboards"), dict) | |
| # ---- backup target: GitHub -------------------------------------------------- | |
| def _gh_headers(): | |
| return {"Authorization": f"Bearer {GITHUB_TOKEN}", | |
| "Accept": "application/vnd.github+json", | |
| "X-GitHub-Api-Version": "2022-11-28"} | |
| def _gh_load(): | |
| global _sha | |
| if not GITHUB_TOKEN: | |
| return None | |
| r = requests.get(GH_API, headers=_gh_headers(), params={"ref": GITHUB_BRANCH}, timeout=15) | |
| if r.status_code == 404: | |
| return None | |
| r.raise_for_status() | |
| j = r.json() | |
| _sha = j.get("sha") | |
| return json.loads(base64.b64decode(j["content"])) | |
| def _gh_save(payload): | |
| global _sha | |
| body = {"message": "sync dashboards", "branch": GITHUB_BRANCH, | |
| "content": base64.b64encode(payload).decode("ascii")} | |
| if _sha: | |
| body["sha"] = _sha | |
| r = requests.put(GH_API, headers=_gh_headers(), json=body, timeout=20) | |
| if r.status_code in (409, 422) and _sha: # stale sha -> refetch + retry once | |
| g = requests.get(GH_API, headers=_gh_headers(), params={"ref": GITHUB_BRANCH}, timeout=15) | |
| if g.ok: | |
| _sha = g.json().get("sha") | |
| body["sha"] = _sha | |
| r = requests.put(GH_API, headers=_gh_headers(), json=body, timeout=20) | |
| r.raise_for_status() | |
| _sha = r.json().get("content", {}).get("sha", _sha) | |
| # ---- backup target: HF Dataset (fallback) ----------------------------------- | |
| def _hf_load(): | |
| if not (HF_TOKEN and DATASET_REPO): | |
| return None | |
| from huggingface_hub import HfApi | |
| path = HfApi(token=HF_TOKEN).hf_hub_download( | |
| repo_id=DATASET_REPO, repo_type="dataset", filename=STORE_FILE, force_download=True) | |
| with open(path, encoding="utf-8") as fh: | |
| return json.load(fh) | |
| def _hf_save(payload): | |
| if not (HF_TOKEN and DATASET_REPO): | |
| return | |
| from huggingface_hub import HfApi | |
| HfApi(token=HF_TOKEN).upload_file( | |
| path_or_fileobj=io.BytesIO(payload), path_in_repo=STORE_FILE, | |
| repo_id=DATASET_REPO, repo_type="dataset", commit_message="sync dashboards") | |
| # ---- load / sync ------------------------------------------------------------ | |
| def _load(): | |
| global _store | |
| for name, fn in (("github", _gh_load), ("hf-dataset", _hf_load)): | |
| try: | |
| data = fn() | |
| if _valid(data): | |
| _store = data | |
| print(f"[store] loaded {len(_store['dashboards'])} dashboard(s) from {name}") | |
| return | |
| except Exception as exc: | |
| print(f"[store] load from {name} failed: {exc}") | |
| print("[store] starting empty") | |
| def _sync(reason="scheduled"): | |
| """Push the in-memory store to GitHub (primary) + HF Dataset (fallback).""" | |
| global _dirty, _last_sync | |
| payload = _payload() | |
| ok = [] | |
| for name, fn in (("github", _gh_save), ("hf-dataset", _hf_save)): | |
| try: | |
| fn(payload) | |
| ok.append(name) | |
| except Exception as exc: | |
| print(f"[sync] {name} failed: {exc}") | |
| if ok: | |
| _dirty = False | |
| _last_sync = datetime.now(SYNC_TZ).isoformat(timespec="seconds") | |
| print(f"[sync] {reason}: pushed to {', '.join(ok)}") | |
| return ok | |
| def _touch(): | |
| global _dirty | |
| _dirty = True | |
| def _scheduler(): | |
| last_run = None | |
| while True: | |
| try: | |
| now = datetime.now(SYNC_TZ) | |
| if now.hour == SYNC_HOUR and last_run != now.date(): | |
| with _lock: | |
| if _dirty: | |
| _sync(f"daily {SYNC_HOUR:02d}:00") | |
| last_run = now.date() | |
| except Exception as exc: | |
| print(f"[scheduler] {exc}") | |
| time.sleep(30) | |
| def _startup(): | |
| _load() | |
| threading.Thread(target=_scheduler, daemon=True).start() | |
| print(f"[store] daily sync armed for {SYNC_HOUR:02d}:00 {SYNC_TZ.key}") | |
| def _shutdown(): | |
| with _lock: | |
| if _dirty: | |
| _sync("shutdown flush") | |
| # ---- auth ------------------------------------------------------------------- | |
| def _require_key(authorization: str | None): | |
| if not AGENT_KEY: | |
| raise HTTPException(500, "AGENT_KEY not configured on the server") | |
| token = "" | |
| if authorization and authorization.lower().startswith("bearer "): | |
| token = authorization[7:].strip() | |
| if token != AGENT_KEY: | |
| raise HTTPException(401, "missing or invalid bearer token") | |
| def _now(): | |
| return datetime.now(SYNC_TZ).isoformat(timespec="seconds") | |
| def _norm(dash_id: str, body: dict, existing: dict | None = None) -> dict: | |
| now = _now() | |
| base = existing or {"created_at": now} | |
| return { | |
| "id": dash_id, | |
| "title": str(body.get("title") or (existing or {}).get("title") or dash_id), | |
| "widgets": body.get("widgets", (existing or {}).get("widgets", [])) or [], | |
| "data": body.get("data", (existing or {}).get("data", {})) or {}, | |
| "created_at": base.get("created_at", now), | |
| "updated_at": now, | |
| } | |
| # ---- JSON API --------------------------------------------------------------- | |
| def health(): | |
| return {"status": "ok", "dashboards": len(_store["dashboards"]), | |
| "dirty": _dirty, "last_sync": _last_sync, | |
| "next_sync": f"{SYNC_HOUR:02d}:00 {SYNC_TZ.key}"} | |
| def manual_sync(authorization: str | None = Header(default=None)): | |
| _require_key(authorization) | |
| with _lock: | |
| ok = _sync("manual") | |
| return {"synced_to": ok, "dirty": _dirty, "last_sync": _last_sync} | |
| def list_dashboards(authorization: str | None = Header(default=None)): | |
| _require_key(authorization) | |
| return {"dashboards": [ | |
| {"id": d["id"], "title": d["title"], "widgets": len(d.get("widgets", [])), | |
| "updated_at": d.get("updated_at")} | |
| for d in _store["dashboards"].values() | |
| ]} | |
| def get_dashboard(dash_id: str, authorization: str | None = Header(default=None)): | |
| _require_key(authorization) | |
| d = _store["dashboards"].get(dash_id) | |
| if not d: | |
| raise HTTPException(404, "dashboard not found") | |
| return d | |
| async def create_dashboard(request: Request, authorization: str | None = Header(default=None)): | |
| _require_key(authorization) | |
| body = await request.json() | |
| if not isinstance(body, dict) or not body.get("id"): | |
| raise HTTPException(400, "body must include an 'id'") | |
| dash_id = str(body["id"]) | |
| with _lock: | |
| _store["dashboards"][dash_id] = _norm(dash_id, body, _store["dashboards"].get(dash_id)) | |
| _touch() | |
| return _store["dashboards"][dash_id] | |
| async def update_dashboard(dash_id: str, request: Request, | |
| authorization: str | None = Header(default=None)): | |
| _require_key(authorization) | |
| body = await request.json() | |
| with _lock: | |
| existing = _store["dashboards"].get(dash_id) | |
| if not existing: | |
| raise HTTPException(404, "dashboard not found") | |
| merged = dict(existing) | |
| if "title" in body: | |
| merged["title"] = str(body["title"]) | |
| if "widgets" in body: | |
| merged["widgets"] = body["widgets"] or [] | |
| if "data" in body: | |
| merged["data"] = body["data"] or {} | |
| merged["updated_at"] = _now() | |
| _store["dashboards"][dash_id] = merged | |
| _touch() | |
| return merged | |
| async def update_data(dash_id: str, request: Request, | |
| authorization: str | None = Header(default=None)): | |
| _require_key(authorization) | |
| body = await request.json() | |
| if not isinstance(body, dict): | |
| raise HTTPException(400, "data body must be a JSON object of {key: value}") | |
| with _lock: | |
| d = _store["dashboards"].get(dash_id) | |
| if not d: | |
| raise HTTPException(404, "dashboard not found") | |
| data = dict(d.get("data", {})) | |
| data.update(body) # shallow merge | |
| d["data"] = data | |
| d["updated_at"] = _now() | |
| _touch() | |
| return d | |
| def delete_dashboard(dash_id: str, authorization: str | None = Header(default=None)): | |
| _require_key(authorization) | |
| with _lock: | |
| if dash_id not in _store["dashboards"]: | |
| raise HTTPException(404, "dashboard not found") | |
| del _store["dashboards"][dash_id] | |
| _touch() | |
| return {"deleted": dash_id} | |
| # ---- HTML rendering --------------------------------------------------------- | |
| CSS = """ | |
| :root{--bg:#f6f7f9;--card:#fff;--ink:#111827;--muted:#6b7280;--line:#e5e7eb;--accent:#4f46e5} | |
| @media (prefers-color-scheme:dark){:root{--bg:#0b0f17;--card:#141a24;--ink:#e5e7eb;--muted:#9aa4b2;--line:#232b36;--accent:#8b8cf9}} | |
| *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink); | |
| font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif} | |
| .wrap{max-width:1100px;margin:0 auto;padding:28px 20px 60px} | |
| a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline} | |
| h1{font-size:24px;margin:0 0 4px}.sub{color:var(--muted);margin:0 0 24px;font-size:13px} | |
| .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:16px} | |
| .card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:16px 18px} | |
| .card h3{margin:0 0 10px;font-size:13px;letter-spacing:.02em;text-transform:uppercase;color:var(--muted)} | |
| .metric .val{font-size:34px;font-weight:700;line-height:1.1} | |
| .metric .unit{font-size:18px;color:var(--muted);font-weight:600} | |
| .delta{font-size:13px;margin-top:6px}.delta.up{color:#16a34a}.delta.down{color:#dc2626} | |
| table{width:100%;border-collapse:collapse;font-size:14px} | |
| th,td{text-align:left;padding:7px 8px;border-bottom:1px solid var(--line)} | |
| th{color:var(--muted);font-weight:600;font-size:12px;text-transform:uppercase} | |
| .full{grid-column:1/-1}.idx a{display:block}.idx .card{transition:border-color .15s} | |
| .idx .card:hover{border-color:var(--accent)} | |
| .empty{color:var(--muted);padding:40px;text-align:center;border:1px dashed var(--line);border-radius:14px} | |
| canvas{max-width:100%}.foot{margin-top:28px;color:var(--muted);font-size:12px} | |
| """ | |
| def _esc(x): | |
| return html.escape("" if x is None else str(x)) | |
| def _effective(widget, data): | |
| eff = dict(widget) | |
| key = widget.get("dataKey") | |
| if key and isinstance(data.get(key), dict): | |
| eff.update(data[key]) | |
| return eff | |
| def _render_widget(widget, data, idx): | |
| w = _effective(widget, data) | |
| wtype = (w.get("type") or "metric").lower() | |
| title = _esc(w.get("title", "")) | |
| cls = "card full" if wtype in ("chart", "table") else "card" | |
| head = f"<h3>{title}</h3>" if title else "" | |
| if wtype == "metric": | |
| val = w.get("value", "—") | |
| unit = w.get("unit", "") | |
| delta = w.get("delta") | |
| dhtml = "" | |
| if isinstance(delta, (int, float)): | |
| arrow = "▲" if delta >= 0 else "▼" | |
| dhtml = f'<div class="delta {"up" if delta>=0 else "down"}">{arrow} {abs(delta)}%</div>' | |
| return (f'<div class="{cls} metric">{head}<div class="val">{_esc(val)}' | |
| f'<span class="unit"> {_esc(unit)}</span></div>{dhtml}</div>') | |
| if wtype == "table": | |
| cols = w.get("columns", []) | |
| rows = w.get("rows", []) | |
| th = "".join(f"<th>{_esc(c)}</th>" for c in cols) | |
| trs = "".join("<tr>" + "".join(f"<td>{_esc(c)}</td>" for c in r) + "</tr>" for r in rows) | |
| return f'<div class="{cls}">{head}<table><thead><tr>{th}</tr></thead><tbody>{trs}</tbody></table></div>' | |
| if wtype == "chart": | |
| chart = w.get("chart", "line") | |
| labels = w.get("labels", []) | |
| series = w.get("series", []) | |
| palette = ["#4f46e5", "#16a34a", "#ea580c", "#0891b2", "#db2777"] | |
| ds = [{"label": s.get("name", f"S{i+1}"), "data": s.get("data", []), | |
| "borderColor": palette[i % len(palette)], | |
| "backgroundColor": palette[i % len(palette)] + "55", | |
| "tension": 0.3} for i, s in enumerate(series)] | |
| cfg = {"type": chart, "data": {"labels": labels, "datasets": ds}, | |
| "options": {"responsive": True, | |
| "plugins": {"legend": {"display": len(ds) > 1}}}} | |
| return (f'<div class="{cls}">{head}<canvas id="c{idx}"></canvas>' | |
| f'<script>new Chart(document.getElementById("c{idx}"),{json.dumps(cfg)});</script></div>') | |
| if wtype == "text": | |
| return f'<div class="{cls}">{head}<div>{_esc(w.get("text",""))}</div></div>' | |
| return f'<div class="{cls}">{head}<pre>{_esc(json.dumps(w, indent=2))}</pre></div>' | |
| def _page(title, body, with_chartjs=False): | |
| cdn = ('<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>' | |
| if with_chartjs else "") | |
| return HTMLResponse( | |
| f'<!doctype html><html lang="en"><head><meta charset="utf-8">' | |
| f'<meta name="viewport" content="width=device-width,initial-scale=1">' | |
| f'<title>{_esc(title)}</title>{cdn}<style>{CSS}</style></head>' | |
| f'<body><div class="wrap">{body}' | |
| f'<div class="foot">Agent Dashboards · updated live via API</div></div></body></html>') | |
| def index(): | |
| ds = list(_store["dashboards"].values()) | |
| if not ds: | |
| body = ('<h1>Agent Dashboards</h1><p class="sub">No dashboards yet.</p>' | |
| '<div class="empty">Create one with <code>POST /api/dashboards</code></div>') | |
| return _page("Agent Dashboards", body) | |
| cards = "".join( | |
| f'<a href="/d/{_esc(d["id"])}"><div class="card"><h3>{_esc(d["id"])}</h3>' | |
| f'<div style="font-size:18px;font-weight:600">{_esc(d["title"])}</div>' | |
| f'<div class="sub" style="margin:6px 0 0">{len(d.get("widgets",[]))} widgets · ' | |
| f'{_esc((d.get("updated_at") or "")[:19])}</div></div></a>' | |
| for d in sorted(ds, key=lambda x: x["id"])) | |
| body = (f'<h1>Agent Dashboards</h1><p class="sub">{len(ds)} dashboard(s)</p>' | |
| f'<div class="grid idx">{cards}</div>') | |
| return _page("Agent Dashboards", body) | |
| def render_dashboard(dash_id: str): | |
| d = _store["dashboards"].get(dash_id) | |
| if not d: | |
| return _page("Not found", f'<h1>404</h1><p class="sub">No dashboard "{_esc(dash_id)}".</p>' | |
| '<p><a href="/">← all dashboards</a></p>') | |
| data = d.get("data", {}) | |
| widgets = d.get("widgets", []) | |
| has_chart = any((_effective(w, data).get("type") or "") == "chart" for w in widgets) | |
| cards = "".join(_render_widget(w, data, i) for i, w in enumerate(widgets)) \ | |
| or '<div class="empty">No widgets yet.</div>' | |
| body = (f'<p class="sub" style="margin-bottom:2px"><a href="/">← all dashboards</a></p>' | |
| f'<h1>{_esc(d["title"])}</h1>' | |
| f'<p class="sub">updated {_esc((d.get("updated_at") or "")[:19])}</p>' | |
| f'<div class="grid">{cards}</div>') | |
| return _page(d["title"], body, with_chartjs=has_chart) | |