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
+35 -25
View File
@@ -31,8 +31,8 @@ from cerbero_bite.gui.data_layer import (
load_engine_snapshot,
)
PAGE_TITLE = "Cerbero Bite — Dashboard"
PAGE_ICON = "🐺"
PAGE_TITLE = "Cerbero Bite — Cruscotto"
PAGE_ICON = str(Path(__file__).parent / "assets" / "cerbero_logo.png")
# ---------------------------------------------------------------------------
@@ -53,35 +53,38 @@ def _resolve_paths() -> tuple[Path, Path]:
_HEALTH_BADGES: dict[str, tuple[str, str]] = {
"running": ("🟢", "RUNNING"),
"degraded": ("🟡", "DEGRADED"),
"running": ("🟢", "ATTIVO"),
"degraded": ("🟡", "DEGRADATO"),
"killed": ("🔴", "KILL SWITCH"),
"stopped": ("", "STOPPED"),
"unknown": ("", "UNKNOWN"),
"stopped": ("", "FERMO"),
"unknown": ("", "SCONOSCIUTO"),
}
def _render_sidebar(db_path: Path, audit_path: Path) -> None:
snap = load_engine_snapshot(db_path=db_path)
icon, label = _HEALTH_BADGES.get(snap.health, ("", "UNKNOWN"))
icon, label = _HEALTH_BADGES.get(snap.health, ("", "SCONOSCIUTO"))
logo_path = Path(__file__).parent / "assets" / "cerbero_logo.png"
if logo_path.is_file():
st.sidebar.image(str(logo_path), use_container_width=True)
st.sidebar.markdown(f"### {icon} {label}")
if snap.kill_switch_armed:
st.sidebar.error(
f"**Kill switch armed**\n\n"
f"reason: {snap.kill_reason or ''}\n\n"
f"since: {humanize_dt(snap.kill_at)}"
f"**Kill switch armato**\n\n"
f"motivo: {snap.kill_reason or ''}\n\n"
f"da: {humanize_dt(snap.kill_at)}"
)
st.sidebar.metric(
"Last health check",
"Ultimo health check",
humanize_age(snap.last_health_check_age_s),
)
st.sidebar.metric("Open positions", snap.open_positions)
st.sidebar.metric("Posizioni aperte", snap.open_positions)
st.sidebar.caption(f"config: `{snap.config_version or ''}`")
st.sidebar.divider()
st.sidebar.caption("Read-only • localhost only")
st.sidebar.caption("Sola lettura • solo localhost")
st.sidebar.caption(f"db: `{db_path}`")
st.sidebar.caption(f"audit: `{audit_path}`")
@@ -102,34 +105,41 @@ def main() -> None:
db_path, audit_path = _resolve_paths()
_render_sidebar(db_path, audit_path)
st.title(f"{PAGE_ICON} Cerbero Bite")
logo_path = Path(__file__).parent / "assets" / "cerbero_logo.png"
header_cols = st.columns([1, 6])
if logo_path.is_file():
header_cols[0].image(str(logo_path), use_container_width=True)
header_cols[1].title("Cerbero Bite")
st.caption(
"Rule-based ETH credit-spread engine — read-only dashboard"
"Motore rule-based per credit spread su ETH — cruscotto in sola lettura"
)
st.markdown(
"""
Use the sidebar to navigate:
Usa la barra laterale per navigare:
- **Status** — engine health, kill switch, open positions, audit anchor
- **Audit** — live audit log stream + chain integrity verification
- **Stato** — salute del motore, kill switch, posizioni aperte, ancora audit
- **Audit** — streaming del registro audit + verifica integrità della catena
- **Equity** — P&L cumulato, drawdown, distribuzione per chiusura, statistiche mensili
- **Storico** — trade chiusi con filtri, KPI, esportazione CSV
- **Posizione** — drilldown sulla singola posizione con grafico payoff
The dashboard reads `data/state.sqlite` and `data/audit.log` directly;
it never calls MCP services or the broker. All write actions remain
on the CLI for now.
Il cruscotto legge `data/state.sqlite` e `data/audit.log` direttamente;
non interroga mai i servizi MCP né il broker. L'unico canale di
scrittura è la coda `manual_actions` per arm/disarm del kill switch.
"""
)
snap = load_engine_snapshot(db_path=db_path)
cols = st.columns(4)
cols[0].metric("Health", _HEALTH_BADGES[snap.health][1])
cols[0].metric("Salute motore", _HEALTH_BADGES[snap.health][1])
cols[1].metric(
"Kill switch",
"ARMED" if snap.kill_switch_armed else "DISARMED",
"ARMATO" if snap.kill_switch_armed else "DISARMATO",
)
cols[2].metric("Open positions", snap.open_positions)
cols[2].metric("Posizioni aperte", snap.open_positions)
cols[3].metric(
"Last health check",
"Ultimo health check",
humanize_age(snap.last_health_check_age_s),
)