"""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) @app.on_event("startup") def _startup(): _load() threading.Thread(target=_scheduler, daemon=True).start() print(f"[store] daily sync armed for {SYNC_HOUR:02d}:00 {SYNC_TZ.key}") @app.on_event("shutdown") 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 --------------------------------------------------------------- @app.get("/health") 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}"} @app.post("/api/sync") 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} @app.get("/api/dashboards") 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() ]} @app.get("/api/dashboards/{dash_id}") 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 @app.post("/api/dashboards") 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] @app.put("/api/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 @app.put("/api/dashboards/{dash_id}/data") 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 @app.delete("/api/dashboards/{dash_id}") 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"
{_esc(json.dumps(w, indent=2))}No dashboards yet.
' 'POST /api/dashboards{len(ds)} dashboard(s)
' f'No dashboard "{_esc(dash_id)}".
' '') 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 'updated {_esc((d.get("updated_at") or "")[:19])}
' f'