diff --git a/scripts/export_dashboard_snapshot.py b/scripts/export_dashboard_snapshot.py new file mode 100644 index 0000000..12822b9 --- /dev/null +++ b/scripts/export_dashboard_snapshot.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Снимок состояния харнеса Simbiot → публичный дашборд (t-261018220236-7d5851). + +Запуск из корня simbiot-harness: + ./venv/bin/python scripts/export_dashboard_snapshot.py /path/to/agent-dashboard/site/ + +Что делает: + - читает проекции стора (та же проекция, что у scheduler/CLI — консистентно); + - выгружает site/data.json: задачи, лестница, эскалации, дайджесты, + AUM-историю, траты по уровням, uptime; + - НЕ выносит секреты: только агрегаты и открытые тексты (публичная страница). + +Дашборд — статика (nginx), обновление снимка = повторный запуск скрипта + +push в репо + deploy_app на портале. Автообновление живых данных — roadmap. +""" +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from harness import es # noqa: E402 + +LEVELS = ["sec", "min", "hour", "day"] +LEVEL_META = { + "sec": {"interval": "1 с", "ctx": "0 (без LLM)", "job": "рефлексы: телеметрия, алерты"}, + "min": {"interval": "1 мин", "ctx": "256K", "job": "S-триаж: входящие, быстрые фиксы"}, + "hour": {"interval": "1 ч", "ctx": "512K", "job": "S/M-задачи, дайджест часа"}, + "day": {"interval": "1 д", "ctx": "1M", "job": "M/L-задачи, манифест дня"}, +} +DIGEST_LEVELS = {"day", "hour"} # публично показываем только содержательные уровни + + +def build() -> dict: + st = es.run_projections() + + tasks = sorted( + st["tasks"].values(), + key=lambda t: t.get("created", ""), + ) + tasks_public = [ + { + "id": t.get("id"), + "title": t.get("title"), + "size": t.get("size"), + "status": t.get("status"), + "level": t.get("level"), + "created": t.get("created"), + } + for t in tasks + ] + + ladder = {} + for lv in LEVELS: + c = st["cycle_state"].get(lv, {}) + ladder[lv] = { + "busy": bool(c.get("busy")), + "ticks": c.get("ticks", 0), + "last_tick": c.get("last_tick"), + **LEVEL_META[lv], + } + + escalations = [ + { + "id": e.get("id"), + "severity": e.get("severity"), + "status": e.get("status"), + "from_level": e.get("from_level"), + "to_level": e.get("to_level"), + "reason": e.get("reason"), + "ts": e.get("ts"), + } + for e in st.get("escalations", {}).values() + if e.get("status") == "open" + ] + + digests = [ + { + "level": d.get("level"), + "period": d.get("period"), + "text": (d.get("text") or "").strip(), + "ts": d.get("ts"), + } + for d in st.get("digests", {}).values() + if d.get("level") in DIGEST_LEVELS + ] + digests.sort(key=lambda d: d.get("period") or "", reverse=True) + + spend = {} + for s in st.get("spend", []): + lv = s.get("level", "?") + agg = spend.setdefault(lv, {"calls": 0, "prompt_tokens": 0, "completion_tokens": 0}) + agg["calls"] += 1 + agg["prompt_tokens"] += s.get("prompt_tokens", 0) or 0 + agg["completion_tokens"] += s.get("completion_tokens", 0) or 0 + + import datetime + snapshot = { + "generated_at": datetime.datetime.now(datetime.timezone.utc).isoformat(), + "harness": "simbiot-harness", + "agent": "alex-shkor-6af5", + "portal": "https://cil.by", + "ladder": ladder, + "tasks": tasks_public, + "tasks_stats": { + "total": len(tasks_public), + "by_status": { + s: sum(1 for t in tasks_public if t["status"] == s) + for s in ("ready", "running", "blocked", "failed", "done", "closed") + }, + }, + "escalations_open": escalations, + "digests": digests[:20], + "aum_history": st.get("aum_history", []), + "spend_by_level": spend, + } + return snapshot + + +def main() -> int: + dest = sys.argv[1] if len(sys.argv) > 1 else "site" + os.makedirs(dest, exist_ok=True) + snap = build() + path = os.path.join(dest, "data.json") + with open(path, "w", encoding="utf-8") as f: + json.dump(snap, f, ensure_ascii=False, indent=1) + print(f"snapshot → {path} (tasks={snap['tasks_stats']['total']}, " + f"escalations_open={len(snap['escalations_open'])}, digests={len(snap['digests'])})") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/publish_dashboard.sh b/scripts/publish_dashboard.sh new file mode 100755 index 0000000..da22d3e --- /dev/null +++ b/scripts/publish_dashboard.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# ══════════════════════════════════════════════════════════════════════════ +# publish_dashboard.sh — снапшот стора харнеса → публичный дашборд. +# +# t-261018220236-7d5851 (деплой) + day-тик 2026-10-19 (автообновление): +# 1) scripts/export_dashboard_snapshot.py → /root/agent-dashboard/site/data.json +# 2) git commit+push (репо symbiot/agent-dashboard) +# 3) порталный deploy_app (docker compose + Caddy) → https://agent-dashboard.cil.by +# +# Идемпотентен: пустой дифф — коммита/деплоя нет, exit 0. +# Токен деплоя — агентский (SIMBIOT_AGENT_TOKEN из .env харнеса). +# ══════════════════════════════════════════════════════════════════════════ +set -euo pipefail + +HARNESS=/root/simbiot-harness +REPO=/root/agent-dashboard +LOG=/var/log/agent-dashboard-publish.log + +log() { echo "[$(date -u +%FT%TZ)] $*" >>"$LOG"; } + +# 1) снапшот +"$HARNESS/venv/bin/python" "$HARNESS/scripts/export_dashboard_snapshot.py" "$REPO/site/" + +# 2) push, если есть дифф +cd "$REPO" +if git diff --quiet -- site/data.json; then + log "no changes — skip" + exit 0 +fi +git add site/data.json +git -c user.name="simbiot" -c user.email="simbiot@cil.by" \ + commit -q -m "snapshot: $(date -u +%F\ %H:%M) UTC" +git push -q origin main +log "pushed" + +# 3) деплой через портал +TOKEN="$(sed -n 's/^SIMBIOT_AGENT_TOKEN=//p' "$HARNESS/.env")" +SIMBIOT_PORTAL_MCP_TOKEN="$TOKEN" "$HARNESS/venv/bin/python" "$HARNESS/cli.py" \ + mcp-call deploy_app '{"app":"agent-dashboard","repo":"symbiot/agent-dashboard"}' \ + >/dev/null 2>&1 || { log "deploy_app FAILED"; exit 3; } +log "deployed" +echo "dashboard published"