feat(T5/dashboard): import DBF idempotent + nomenclator browser + audit CSV + stare RAR

T5 (tools/import_dbf.py): citire prestatii_rar.DBF / mapare_prestatii.DBF cu
dbfread, raport dry-run (randuri valide/duplicate/goale, mapari orfane = cod
necunoscut in nomenclator), --commit cu upsert idempotent in tranzactie.

Dashboard: browser nomenclator, indicator stare RAR (indisponibil? derivat din
ultimul login < 30h, coada arata ultima stare locala), export audit CSV
(/v1/audit/export?status=sent|all&date_from&date_to, b64Image exclus,
coloana purge_after pentru retentia 90z).

Verify: 11 teste noi (test_import_dbf 6, test_dashboard 5), suita 111 pass,
dry-run real pe DBF-urile din repo + smoke live dashboard/CSV.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-15 20:32:26 +00:00
parent 6fb92466cb
commit 6ab22ea0fb
8 changed files with 728 additions and 20 deletions

View File

@@ -41,6 +41,25 @@ def _worker_alive(hb) -> bool:
return age <= get_settings().worker_heartbeat_stale_s
def _rar_state(hb, worker_alive: bool) -> str:
"""Eticheta de disponibilitate RAR, derivata din ultimul login reusit.
Nu interogam RAR live aici (dashboard-ul degradeaza la ultima stare cunoscuta
a cozii). JWT TTL = 30h: un login mai vechi de atat inseamna ca nu mai stim
sigur ca RAR raspunde -> "indisponibil?". Fara niciun login -> necunoscut.
"""
if not worker_alive:
return "necunoscut (worker oprit)"
last = hb["last_rar_login_ok"] if hb else None
if not last:
return "fara login reusit inca"
try:
age = (datetime.now(timezone.utc) - datetime.fromisoformat(last)).total_seconds()
except (ValueError, TypeError):
return "necunoscut"
return "indisponibil?" if age > 108000 else "ok"
@router.get("/", response_class=HTMLResponse)
def dashboard(request: Request) -> HTMLResponse:
conn = get_connection()
@@ -48,20 +67,37 @@ def dashboard(request: Request) -> HTMLResponse:
counts = _status_counts(conn)
hb = read_heartbeat(conn)
blocked = sum(counts.get(s, 0) for s in _BLOCKED)
worker_alive = _worker_alive(hb)
ctx = {
"request": request,
"rar_env": get_settings().rar_env,
"version": __version__,
"counts": counts,
"blocked": blocked,
"worker_alive": _worker_alive(hb),
"worker_alive": worker_alive,
"last_login": hb["last_rar_login_ok"] if hb else None,
"rar_state": _rar_state(hb, worker_alive),
}
return templates.TemplateResponse("dashboard.html", ctx)
finally:
conn.close()
@router.get("/_fragments/nomenclator", response_class=HTMLResponse)
def fragment_nomenclator(request: Request) -> HTMLResponse:
"""Browser nomenclator RAR (cache local upsert-at de worker la fiecare login)."""
conn = get_connection()
try:
rows = conn.execute(
"SELECT cod_prestatie, nume_prestatie, updated_at FROM nomenclator_rar ORDER BY cod_prestatie"
).fetchall()
return templates.TemplateResponse(
"_nomenclator.html", {"request": request, "rows": rows}
)
finally:
conn.close()
@router.get("/_fragments/banner", response_class=HTMLResponse)
def fragment_banner(request: Request) -> HTMLResponse:
conn = get_connection()

View File

@@ -0,0 +1,16 @@
{% if rows %}
<table>
<thead><tr><th>Cod</th><th>Denumire</th><th>Actualizat</th></tr></thead>
<tbody>
{% for r in rows %}
<tr>
<td><span class="pill">{{ r.cod_prestatie }}</span></td>
<td>{{ r.nume_prestatie }}</td>
<td class="muted">{{ r.updated_at }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty">Nomenclator gol. Worker-ul il umple la primul login RAR reusit.</div>
{% endif %}

View File

@@ -11,11 +11,17 @@
<div style="display:flex; gap:24px; flex-wrap:wrap;">
<div><div class="muted">Worker</div><div class="{{ 's-sent' if worker_alive else 's-error' }}">
{{ 'viu' if worker_alive else 'mort' }}</div></div>
<div><div class="muted">RAR</div><div class="{{ 's-sent' if rar_state == 'ok' else 's-error' if 'indisponibil' in rar_state else 'muted' }}">{{ rar_state }}</div></div>
<div><div class="muted">Ultimul login RAR</div><div>{{ last_login or '—' }}</div></div>
<div><div class="muted">In coada</div><div>{{ counts.get('queued', 0) }}</div></div>
<div><div class="muted">Trimise</div><div class="s-sent">{{ counts.get('sent', 0) }}</div></div>
<div><div class="muted">Blocate</div><div class="{{ 's-error' if blocked else '' }}">{{ blocked }}</div></div>
</div>
{% if rar_state != 'ok' %}
<p class="muted" style="margin:12px 0 0; font-size:12px;">
RAR posibil indisponibil — coada de mai jos arata ultima stare cunoscuta (local), nu live din RAR.
</p>
{% endif %}
</div>
<!-- incarcat o data; NU poll (sa nu stergem o selectie in curs). Se re-randeaza la salvare. -->
@@ -24,10 +30,23 @@
</div>
<div class="card">
<h2 style="font-size:14px; margin:0 0 12px;">Coada submissions</h2>
<div style="display:flex; align-items:center; gap:12px; margin:0 0 12px;">
<h2 style="font-size:14px; margin:0;">Coada submissions</h2>
<a href="/v1/audit/export?status=sent" style="margin-left:auto; font-size:13px;" download>export audit CSV (trimise)</a>
<a href="/v1/audit/export?status=all" style="font-size:13px;" download>tot</a>
</div>
<div hx-get="/_fragments/submissions" hx-trigger="load, every 10s" hx-swap="innerHTML">
<div class="empty">se incarca…</div>
</div>
</div>
<div class="card">
<details>
<summary style="cursor:pointer; font-size:14px; font-weight:600;">Nomenclator RAR (coduri prestatii)</summary>
<div style="margin-top:12px;" hx-get="/_fragments/nomenclator" hx-trigger="load" hx-swap="innerHTML">
<div class="empty">se incarca…</div>
</div>
</details>
</div>
{% endblock %}