feat(gui): traduzione italiana, logo Cerbero, saldi live e Forza ciclo

* Localizzazione italiana di tutte le pagine (Stato, Audit, Equity,
  Storico, Posizione) e della home; date relative ("5s fa", "12m fa").
* Logo Cerbero (cane a tre teste) in src/cerbero_bite/gui/assets/
  cerbero_logo.png — sostituisce l'emoji 🐺 (lupo, semanticamente
  errata) sia come favicon (`page_icon`) sia in sidebar e header.
* Caricamento automatico di `.env` dal CWD all'avvio della CLI (skip
  sotto pytest tramite PYTEST_CURRENT_TEST), evitando di doversi
  esportare manualmente le 4 URL MCP. Aggiunto python-dotenv come
  dipendenza, `.env.example` committato come template, `.env` resta
  ignorato da git.
* Pagina Stato: nuovo pannello "Saldi exchange" che fa fetch live
  via gateway MCP (Deribit USDC + USDT, Hyperliquid USDC + opzionale
  USDT spot) con cache TTL 60s e bottone refresh; tile riassuntivi
  totale USD / EUR / cambio.
* Pagina Stato: nuovo pannello "Forza ciclo" con tre bottoni
  (entry/monitor/health) che accodano azioni `run_cycle` nella tabella
  manual_actions; il consumer dell'engine — quando in esecuzione —
  dispatcha al `Orchestrator.run_*` corrispondente.
* manual_actions: nuovo `kind="run_cycle"` nello schema
  ManualAction; consumer accetta dict di cycle_runners che
  l'orchestrator popola in install_scheduler. 3 nuovi test (dispatch
  entry, ciclo sconosciuto, fallback senza runner).
* gui/live_data.py — modulo dedicato al fetch MCP dalla GUI
  (relax controllato della regola "no MCP from GUI" solo per i saldi,
  non per i dati di trading).

363/363 tests pass; ruff clean; mypy strict src clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-30 14:11:40 +02:00
parent da88e7f746
commit 63d1aa4262
17 changed files with 730 additions and 222 deletions
+18 -17
View File
@@ -27,8 +27,8 @@ def _resolve_paths() -> tuple[Path, Path]:
def render() -> None:
st.title("🔍 Audit")
st.caption(
"Append-only hash-chained audit log "
"(`data/audit.log`). Reading is non-mutating."
"Registro audit append-only con hash chain "
"(`data/audit.log`). La lettura non modifica nulla."
)
_, audit_path = _resolve_paths()
@@ -36,13 +36,13 @@ def render() -> None:
col_l, col_r = st.columns([1, 2])
with col_l:
st.subheader("Chain integrity")
if st.button("Verify chain", type="primary"):
with st.spinner("Walking the chain"):
st.subheader("Integrità catena")
if st.button("Verifica catena", type="primary"):
with st.spinner("Sto percorrendo la catena"):
status = load_audit_chain_status(audit_path=audit_path)
if status.ok:
st.success(
f"✅ chain integra fino a {status.entries_verified} eventi"
f"✅ catena integra fino a {status.entries_verified} eventi"
)
else:
st.error(
@@ -50,14 +50,15 @@ def render() -> None:
)
else:
st.caption(
"Click to recompute every line's hash and verify the prev-hash "
"linkage. Mismatch → CRITICAL alert in production."
"Premi per ricalcolare l'hash di ogni riga e verificare il "
"collegamento prev-hash. Mismatch → alert CRITICAL in "
"produzione."
)
with col_r:
st.subheader("Filters")
st.subheader("Filtri")
limit = st.slider(
"Last N entries",
"Ultimi N eventi",
min_value=10,
max_value=500,
value=100,
@@ -67,8 +68,8 @@ def render() -> None:
all_recent = load_audit_tail(audit_path=audit_path, limit=limit)
events_present = sorted({e.event for e in all_recent})
event_filter = st.selectbox(
"Event filter",
options=["(all)", *events_present],
"Filtro per evento",
options=["(tutti)", *events_present],
index=0,
)
@@ -83,16 +84,16 @@ def render() -> None:
st.divider()
# Filtered tail
# Tail filtrata
filtered = (
all_recent
if event_filter == "(all)"
if event_filter == "(tutti)"
else [e for e in all_recent if e.event == event_filter]
)
st.subheader(f"Last entries ({len(filtered)} shown)")
st.subheader(f"Ultimi eventi ({len(filtered)} mostrati)")
if not filtered:
st.info("No matching audit entries.")
st.info("Nessun evento corrisponde ai filtri.")
return
rows = []
@@ -106,7 +107,7 @@ def render() -> None:
rows.append(
{
"timestamp": humanize_dt(entry.timestamp),
"event": entry.event,
"evento": entry.event,
"payload": payload_pretty,
"hash": (
f"{entry.hash[:8]}{entry.hash[-8:]}"