Публичный дашборд агента: экспорт снапшота стора + publish-пайплайн (export→push→deploy_app, cron hourly) — t-261018220236-7d5851, e-261018230338-f228b9

This commit is contained in:
simbiot 2026-10-19 00:11:49 +00:00
parent 43d11820e9
commit cbf03ffac2
2 changed files with 175 additions and 0 deletions

View file

@ -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())

42
scripts/publish_dashboard.sh Executable file
View file

@ -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"