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