diff --git a/app/web/routes.py b/app/web/routes.py
index d2a16c8..91dcb09 100644
--- a/app/web/routes.py
+++ b/app/web/routes.py
@@ -20,7 +20,7 @@ from pathlib import Path
from typing import Any
from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile
-from fastapi.responses import HTMLResponse
+from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from .. import __version__
@@ -113,6 +113,18 @@ def _status_counts(conn, account_id: int) -> dict[str, int]:
return {r["status"]: int(r["n"]) for r in rows}
+def _trimiteri_versiune(conn, account_id: int) -> str:
+ """Semnatura ieftina a starii trimiterilor contului: numar randuri + cel mai recent
+ updated_at. Se schimba la orice insert/update/delete -> nudge-ul "Date noi" o compara
+ fara a re-randa tabelul."""
+ row = conn.execute(
+ "SELECT COUNT(*) AS n, COALESCE(MAX(updated_at), '') AS m FROM submissions "
+ "WHERE (account_id = ? OR (? = 1 AND account_id IS NULL))",
+ (account_id, account_id),
+ ).fetchone()
+ return f"{row['n']}:{row['m']}"
+
+
def _account_active(conn, account_id: int) -> bool:
"""True daca contul e activ (sau legacy cu NULL/absent active)."""
row = conn.execute("SELECT active FROM accounts WHERE id=?", (account_id,)).fetchone()
@@ -196,6 +208,10 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
"are_trimiteri": are_trimiteri,
"are_cheie_folosita": are_cheie_folosita,
"blocate_total": blocate_total,
+ # Pill-uri de filtrare a starii, randate in bara de filtre (nu in bara de status).
+ "pills_categorii": _pills_categorii(counts),
+ # Semnatura datelor: nudge-ul "Date noi" o compara la fiecare poll usor.
+ "versiune_trimiteri": _trimiteri_versiune(conn, account_id),
# US-002: Acasa include caseta de upload -> are nevoie de csrf_token
"csrf_token": get_csrf_token(request),
}
@@ -212,7 +228,9 @@ def _render_panel_acasa(request: Request, conn=None, account_id: int = 1, status
{"request": request, "csrf_token": get_csrf_token(request)}
)
ctx = _get_acasa_context(request, conn, account_id)
- ctx["status_filtru"] = status
+ # `status or ""`: campul hidden de filtru ar randa literal "None" cu un None Python
+ # (Jinja `default('')` inlocuieste doar undefined), trimitand status=None la poll.
+ ctx["status_filtru"] = status or ""
return templates.get_template("_acasa.html").render(ctx)
@@ -621,6 +639,19 @@ def fragment_status(request: Request) -> HTMLResponse:
conn.close()
+@router.get("/_fragments/trimiteri-versiune", response_class=JSONResponse)
+def fragment_trimiteri_versiune(request: Request) -> JSONResponse:
+ """Semnatura curenta a trimiterilor contului (JSON usor). Pollerul "Date noi" o
+ compara cu versiunea cu care s-a randat tabelul; daca difera, arata nudge-ul de
+ reincarcare — tabelul nu se mai schimba singur."""
+ account_id = require_login(request)
+ conn = get_connection()
+ try:
+ return JSONResponse({"v": _trimiteri_versiune(conn, account_id)})
+ finally:
+ conn.close()
+
+
def _iso_date_prefix(value: object) -> str | None:
"""Intoarce primele 10 caractere (YYYY-MM-DD) daca incep cu o data ISO valida, altfel None.
@@ -795,6 +826,10 @@ def fragment_submissions(
"f_vehicul": vehicul_q or "",
"f_data_de": data_de or "",
"f_data_pana": data_pana or "",
+ # Pill-uri (OOB) + stare activa + versiune pentru nudge-ul "Date noi".
+ "pills_categorii": _pills_categorii(_status_counts(conn, account_id)),
+ "status_filtru": status or "",
+ "versiune_trimiteri": _trimiteri_versiune(conn, account_id),
})
finally:
conn.close()
@@ -823,6 +858,9 @@ def _render_submissions(request: Request, conn, account_id: int) -> HTMLResponse
"rows": view,
"filtru_activ": False,
"csrf_token": get_csrf_token(request),
+ "pills_categorii": _pills_categorii(_status_counts(conn, account_id)),
+ "status_filtru": "",
+ "versiune_trimiteri": _trimiteri_versiune(conn, account_id),
})
diff --git a/app/web/templates/_coada.html b/app/web/templates/_coada.html
index 6cec9fa..5f97312 100644
--- a/app/web/templates/_coada.html
+++ b/app/web/templates/_coada.html
@@ -21,19 +21,16 @@
-
+
-
+
+
+ Sunt trimiteri actualizate.
+
+
+
+
se incarca…
diff --git a/app/web/templates/_pills.html b/app/web/templates/_pills.html
new file mode 100644
index 0000000..b9c0687
--- /dev/null
+++ b/app/web/templates/_pills.html
@@ -0,0 +1,15 @@
+{# Pill-uri de filtrare a starii, randate in bara de filtre (_coada.html) si re-randate
+ prin OOB la fiecare reincarcare a tabelului (_submissions.html). Stare activa =
+ status_filtru. "Toate" reseteaza filtrul; categoriile apar doar cand au n>0. #}
+
+{% for pill in pills_categorii %}
+
+{% endfor %}
diff --git a/app/web/templates/_status.html b/app/web/templates/_status.html
index 7249d09..f0ed3bc 100644
--- a/app/web/templates/_status.html
+++ b/app/web/templates/_status.html
@@ -1,15 +1,3 @@
-
-
- {% if pills_categorii %}
-
-
- Necesita atentie:
- {% for pill in pills_categorii %}
-
- {% endfor %}
- {# Buton "Toate" — reseteaza filtrul de categorie #}
-
-
-
- {% endif %}
+ {# Pill-urile de stare s-au mutat in bara de filtre din sectiunea Trimiteri (_coada.html). #}
diff --git a/app/web/templates/_submissions.html b/app/web/templates/_submissions.html
index 0744ac8..eee4f13 100644
--- a/app/web/templates/_submissions.html
+++ b/app/web/templates/_submissions.html
@@ -5,6 +5,13 @@
#}
+{# OOB: re-randeaza pill-urile de stare (in bara de filtre, in afara #submissions-wrap) cu
+ contoarele si starea activa proaspete la fiecare reincarcare a tabelului. #}
+{% include '_pills.html' %}
+
+{# Versiunea datelor cu care s-a randat tabelul; pollerul "Date noi" o compara. #}
+
+
{% if rows %}
{# US-011: form de stergere bulk. Selectia opereaza DOAR pe randuri blocate
(gestionabil); sent/sending/queued nu au checkbox (read-only). #}
diff --git a/app/web/templates/base.html b/app/web/templates/base.html
index 12c2386..007d858 100644
--- a/app/web/templates/base.html
+++ b/app/web/templates/base.html
@@ -123,7 +123,7 @@
background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased; }
/* US-012c (PRD 5.10): grila 3 coloane — stanga (logo ROMFAST) | centru (titlu+env) | dreapta (controale). */
header { padding:16px 24px; border-bottom:1px solid var(--line);
- display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:8px; }
+ display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:8px; min-height:92px; }
.header-left { display:flex; align-items:center; }
.header-center { display:flex; flex-direction:column; align-items:center; text-align:center; }
.header-right { display:flex; align-items:center; justify-content:flex-end; gap:8px; }
@@ -131,7 +131,8 @@
32px inaltime — usor mai mare decat in header-center (28px) pentru vizibilitate ca brand anchor.
margin:0 — aliniat stanga, NU centrat (era `margin:3px auto 0` cand era sub titlu).
Logo transparent: ok pe dark/light/petrol fara filtre de culoare. */
- .brand-logo { height:32px; width:auto; display:block; margin:0; }
+ /* Logo ROMFAST la dimensiunea de pe romfast.ro (~60px inaltime), aliniat stanga. */
+ .brand-logo { height:60px; width:auto; display:block; margin:0; }
/* Env badge mic sub titlu in header-center (US-012c): nu mai echilibreaza optic dreapta
(logo-ul face asta), ci identifica mediul langa titlu. Pastrat mic, color:var(--muted). */
.header-center .env { font-size:11px; margin-top:2px; }
@@ -149,6 +150,28 @@
th { color:var(--muted); font-weight:500; font-size:12px; text-transform:uppercase; letter-spacing:.04em; }
.empty { color:var(--muted); padding:24px; text-align:center; }
.pill { font-size:12px; padding:2px 8px; border-radius:99px; border:1px solid var(--line); }
+ /* Pill-uri de filtrare a starii (bara de filtre Trimiteri). Inactiv = contur+text pe
+ culoarea categoriei (injectata inline); activ = umplere pe acea culoare. */
+ .pills-categorii { display:inline-flex; gap:8px; flex-wrap:wrap; align-items:center; }
+ .pill-cat { display:inline-flex; align-items:center; gap:5px; padding:4px 11px; border-radius:99px;
+ font-size:12px; font-weight:600; cursor:pointer; background:transparent;
+ border:1.5px solid var(--line); color:var(--muted); min-height:30px;
+ transition:background .15s, color .15s; }
+ .pill-cat:hover { filter:brightness(1.1); }
+ .pill-cat:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
+ .pill-cat-n { font-size:11px; font-weight:700; color:var(--card); padding:0 5px;
+ border-radius:99px; min-width:18px; text-align:center; }
+ .pill-cat[aria-pressed="true"] { background:currentColor; color:var(--card); border-color:currentColor; }
+ .pill-cat[aria-pressed="true"] .pill-cat-n { background:var(--card) !important; color:currentColor; }
+ .pill-cat-reset[aria-pressed="true"] { background:var(--accent); color:#fff; border-color:var(--accent); }
+ /* Nudge "Date noi": apare doar cand pollerul usor detecteaza schimbari; tabelul nu se
+ schimba singur niciodata, utilizatorul reincarca cand vrea. */
+ #nudge-trimiteri { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin:0 0 12px;
+ padding:8px 12px; border-radius:8px; font-size:13px;
+ border:1px solid var(--accent);
+ background:color-mix(in srgb, var(--accent) 12%, var(--card)); }
+ #nudge-trimiteri[hidden] { display:none; }
+ #nudge-trimiteri button { font-size:13px; padding:5px 12px; min-height:32px; }
.s-queued{color:var(--accent);} .s-sending{color:var(--warn);} .s-sent{color:var(--ok);}
.s-error,.s-needs_data,.s-needs_mapping{color:var(--err);}
.s-ok{color:var(--ok);}
@@ -349,7 +372,8 @@
/* Header + nav colapsate: pe mobil trece de la grid la flex wrap.
Randul 1: [logo ROMFAST stanga] [controale dreapta] (margin-left:auto pe .header-right).
Randul 2: [titlu + env mic centrat, full-width]. Fara scroll orizontal, tinte >=44px. */
- header { display:flex; flex-wrap:wrap; padding:12px 16px; gap:8px; align-items:center; }
+ header { display:flex; flex-wrap:wrap; padding:12px 16px; gap:8px; align-items:center; min-height:0; }
+ .brand-logo { height:44px; }
.header-left { order:0; flex:0 0 auto; }
.header-center { order:2; width:100%; text-align:center; }
.header-right { order:1; margin-left:auto; flex:0 0 auto; }
@@ -789,45 +813,49 @@
})();