init: FastAPI SSE Baserow polling

This commit is contained in:
bolbol 2026-06-09 20:01:29 +00:00
parent 8e3d4ae5eb
commit c638f8eada
1 changed files with 107 additions and 0 deletions

107
app/main.py Normal file
View File

@ -0,0 +1,107 @@
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())