"""Dashboard Jinja2 + HTMX (server-rendered, zero build). Schelet cu stari explicite: empty (coada goala), banner alerta blocate, worker viu/mort, ultimul login RAR. Editor mapari + browser nomenclator + export CSV + stare "RAR indisponibil" = de adaugat (plan.md sect. 4 + design-review). """ from __future__ import annotations from datetime import datetime, timezone from pathlib import Path from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from .. import __version__ from ..config import get_settings from ..db import get_connection, read_heartbeat router = APIRouter(tags=["web"]) templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent / "templates")) _BLOCKED = ("error", "needs_data", "needs_mapping") def _status_counts(conn) -> dict[str, int]: rows = conn.execute("SELECT status, COUNT(*) AS n FROM submissions GROUP BY status").fetchall() return {r["status"]: int(r["n"]) for r in rows} def _worker_alive(hb) -> bool: if hb is None or not hb["last_beat"]: return False try: last = datetime.fromisoformat(hb["last_beat"]) except ValueError: return False age = (datetime.now(timezone.utc) - last).total_seconds() return age <= get_settings().worker_heartbeat_stale_s @router.get("/", response_class=HTMLResponse) def dashboard(request: Request) -> HTMLResponse: conn = get_connection() try: counts = _status_counts(conn) hb = read_heartbeat(conn) blocked = sum(counts.get(s, 0) for s in _BLOCKED) ctx = { "request": request, "rar_env": get_settings().rar_env, "version": __version__, "counts": counts, "blocked": blocked, "worker_alive": _worker_alive(hb), "last_login": hb["last_rar_login_ok"] if hb else None, } return templates.TemplateResponse("dashboard.html", ctx) finally: conn.close() @router.get("/_fragments/banner", response_class=HTMLResponse) def fragment_banner(request: Request) -> HTMLResponse: conn = get_connection() try: counts = _status_counts(conn) blocked = sum(counts.get(s, 0) for s in _BLOCKED) return templates.TemplateResponse("_banner.html", {"request": request, "blocked": blocked}) finally: conn.close() @router.get("/_fragments/submissions", response_class=HTMLResponse) def fragment_submissions(request: Request) -> HTMLResponse: conn = get_connection() try: rows = conn.execute( "SELECT id, status, id_prezentare, rar_status_code, rar_error, retry_count, updated_at " "FROM submissions ORDER BY id DESC LIMIT 100" ).fetchall() return templates.TemplateResponse("_submissions.html", {"request": request, "rows": rows}) finally: conn.close()