hermes-dashboard/app/main.py

108 lines
4.9 KiB
Python

import asyncio, json, ssl, time, urllib.request
from datetime import datetime, timezone
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
import os
app = FastAPI(title="Hermes Mission Control Dashboard")
# ── Config ─────────────────────────────────────────────────────────────────
BR_TOKEN = "52yUWNDZzBNJo31nzwTEzYrrb1yuMlMw"
BR_URL = "http://Baserow/api/database/rows/table/1064/?user_field_names=true&size=50"
BR_HEADER = "baserow.bolbol.tn"
CTX = ssl.create_default_context()
INSTANCES = {
"hermes-tt": {"label": "Hermes TT", "color": "#3b82f6", "container": "hermes-agent-tt"},
"hermes-nyora": {"label": "Hermes Nyora", "color": "#22c55e", "container": "hermes-agent-nyora"},
"hermes-perso": {"label": "Hermes Perso", "color": "#f97316", "container": "hermes-agent-perso"},
}
CRONS = [
{"instance": "hermes-tt", "name": "Brief RLA", "schedule": "Lun-Ven 08h00"},
{"instance": "hermes-nyora", "name": "Veille IA", "schedule": "Lun-Ven 12h30"},
]
# ── Cache ───────────────────────────────────────────────────────────────────
_cache = {"missions": [], "ts": 0}
def fetch_missions():
global _cache
if time.time() - _cache["ts"] < 30:
return _cache["missions"]
try:
req = urllib.request.Request(BR_URL, headers={
"Authorization": f"Token {BR_TOKEN}", "Host": BR_HEADER})
with urllib.request.urlopen(req, timeout=8) as r:
data = json.loads(r.read())
rows = data.get("results", [])
missions = []
for row in rows:
inst_obj = row.get("Instance") or {}
stat_obj = row.get("Statut") or {}
missions.append({
"id": row.get("id"),
"mission": row.get("Mission", ""),
"instance": inst_obj.get("value", "") if isinstance(inst_obj, dict) else "",
"statut": stat_obj.get("value", "") if isinstance(stat_obj, dict) else "",
"modele": row.get("Modele", ""),
"debut": (row.get("Debut") or "")[:16],
"fin": (row.get("Fin") or "")[:16],
"cout": row.get("Cout_DT", 0),
"resultat": (row.get("Resultat") or "")[:120],
"actif": row.get("Actif", False),
})
_cache = {"missions": missions, "ts": time.time()}
return missions
except Exception as e:
return _cache.get("missions", [])
def build_status(missions):
status = {}
for key, cfg in INSTANCES.items():
label = cfg["label"]
instance_missions = [m for m in missions if m["instance"] == key]
active = [m for m in instance_missions if m["actif"]]
last = instance_missions[0] if instance_missions else None
status[key] = {
"label": label,
"color": cfg["color"],
"active_count": len(active),
"last_mission": last["mission"] if last else "",
"last_statut": last["statut"] if last else "",
"last_time": last["fin"] or last["debut"] if last else "",
"last_modele": last["modele"] if last else "",
"total": len(instance_missions),
"current": active[0]["mission"] if active else None,
}
return status
# ── Routes ──────────────────────────────────────────────────────────────────
@app.get("/api/data")
def api_data():
missions = fetch_missions()
return {"status": build_status(missions), "missions": missions[:20], "crons": CRONS, "ts": int(time.time())}
@app.post("/webhook/baserow")
async def webhook(request: Request):
_cache["ts"] = 0 # invalide le cache
return {"ok": True}
@app.get("/events")
async def sse(request: Request):
async def gen():
while True:
if await request.is_disconnected():
break
missions = fetch_missions()
payload = {"status": build_status(missions), "missions": missions[:20], "ts": int(time.time())}
yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
await asyncio.sleep(30)
return StreamingResponse(gen(), media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
@app.get("/", response_class=HTMLResponse)
def index():
return HTMLResponse(open("/app/templates/index.html").read())