"""Streamlit entry point for the Cerbero Bite dashboard. Launch with:: cerbero-bite gui or directly:: uv run streamlit run src/cerbero_bite/gui/main.py \ --server.address 127.0.0.1 \ --server.port 8765 \ --server.headless true The dashboard is **read-mostly**: it reads SQLite + the audit log and never imports ``runtime/`` modules. Each Streamlit page is in ``gui/pages/`` and Streamlit auto-discovers them. """ from __future__ import annotations import os from pathlib import Path import streamlit as st from cerbero_bite.gui.data_layer import ( DEFAULT_AUDIT_PATH, DEFAULT_DB_PATH, humanize_age, humanize_dt, load_engine_snapshot, ) PAGE_TITLE = "Cerbero Bite — Cruscotto" PAGE_ICON = str(Path(__file__).parent / "assets" / "cerbero_logo.png") # --------------------------------------------------------------------------- # Path resolution # --------------------------------------------------------------------------- def _resolve_paths() -> tuple[Path, Path]: """Read DB / audit paths from env (settable by ``cerbero-bite gui``).""" db_path = Path(os.environ.get("CERBERO_BITE_GUI_DB", DEFAULT_DB_PATH)) audit_path = Path(os.environ.get("CERBERO_BITE_GUI_AUDIT", DEFAULT_AUDIT_PATH)) return db_path, audit_path # --------------------------------------------------------------------------- # Sidebar # --------------------------------------------------------------------------- _HEALTH_BADGES: dict[str, tuple[str, str]] = { "running": ("🟢", "ATTIVO"), "degraded": ("🟡", "DEGRADATO"), "killed": ("🔴", "KILL SWITCH"), "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, ("⚪", "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 armato**\n\n" f"motivo: {snap.kill_reason or '—'}\n\n" f"da: {humanize_dt(snap.kill_at)}" ) st.sidebar.metric( "Ultimo health check", humanize_age(snap.last_health_check_age_s), ) st.sidebar.metric("Posizioni aperte", snap.open_positions) st.sidebar.caption(f"config: `{snap.config_version or '—'}`") st.sidebar.divider() st.sidebar.caption("Sola lettura • solo localhost") st.sidebar.caption(f"db: `{db_path}`") st.sidebar.caption(f"audit: `{audit_path}`") # --------------------------------------------------------------------------- # Home page # --------------------------------------------------------------------------- def main() -> None: st.set_page_config( page_title=PAGE_TITLE, page_icon=PAGE_ICON, layout="wide", initial_sidebar_state="expanded", ) db_path, audit_path = _resolve_paths() _render_sidebar(db_path, audit_path) 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( "Motore rule-based per credit spread su ETH — cruscotto in sola lettura" ) st.markdown( """ Usa la barra laterale per navigare: - **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 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("Salute motore", _HEALTH_BADGES[snap.health][1]) cols[1].metric( "Kill switch", "ARMATO" if snap.kill_switch_armed else "DISARMATO", ) cols[2].metric("Posizioni aperte", snap.open_positions) cols[3].metric( "Ultimo health check", humanize_age(snap.last_health_check_age_s), ) if __name__ == "__main__": main()