63d1aa4262
* 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>
123 lines
3.5 KiB
Python
123 lines
3.5 KiB
Python
"""Audit page — live audit log stream + chain integrity verification."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
import streamlit as st
|
|
|
|
from cerbero_bite.gui.data_layer import (
|
|
DEFAULT_AUDIT_PATH,
|
|
DEFAULT_DB_PATH,
|
|
humanize_dt,
|
|
load_audit_chain_status,
|
|
load_audit_tail,
|
|
)
|
|
|
|
|
|
def _resolve_paths() -> tuple[Path, Path]:
|
|
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
|
|
|
|
|
|
def render() -> None:
|
|
st.title("🔍 Audit")
|
|
st.caption(
|
|
"Registro audit append-only con hash chain "
|
|
"(`data/audit.log`). La lettura non modifica nulla."
|
|
)
|
|
|
|
_, audit_path = _resolve_paths()
|
|
|
|
col_l, col_r = st.columns([1, 2])
|
|
|
|
with col_l:
|
|
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"✅ catena integra fino a {status.entries_verified} eventi"
|
|
)
|
|
else:
|
|
st.error(
|
|
f"❌ tampering rilevato\n\n```\n{status.error}\n```"
|
|
)
|
|
else:
|
|
st.caption(
|
|
"Premi per ricalcolare l'hash di ogni riga e verificare il "
|
|
"collegamento prev-hash. Mismatch → alert CRITICAL in "
|
|
"produzione."
|
|
)
|
|
|
|
with col_r:
|
|
st.subheader("Filtri")
|
|
limit = st.slider(
|
|
"Ultimi N eventi",
|
|
min_value=10,
|
|
max_value=500,
|
|
value=100,
|
|
step=10,
|
|
)
|
|
# Build event list from the available tail
|
|
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(
|
|
"Filtro per evento",
|
|
options=["(tutti)", *events_present],
|
|
index=0,
|
|
)
|
|
|
|
st.divider()
|
|
|
|
# Statistics strip
|
|
counter: Counter[str] = Counter(e.event for e in all_recent)
|
|
if counter:
|
|
cols = st.columns(min(len(counter), 6))
|
|
for col, (event, count) in zip(cols, counter.most_common(6), strict=False):
|
|
col.metric(event, count)
|
|
|
|
st.divider()
|
|
|
|
# Tail filtrata
|
|
filtered = (
|
|
all_recent
|
|
if event_filter == "(tutti)"
|
|
else [e for e in all_recent if e.event == event_filter]
|
|
)
|
|
|
|
st.subheader(f"Ultimi eventi ({len(filtered)} mostrati)")
|
|
if not filtered:
|
|
st.info("Nessun evento corrisponde ai filtri.")
|
|
return
|
|
|
|
rows = []
|
|
for entry in filtered:
|
|
try:
|
|
payload_pretty = json.dumps(
|
|
entry.payload, ensure_ascii=False, sort_keys=True
|
|
)
|
|
except (TypeError, ValueError):
|
|
payload_pretty = str(entry.payload)
|
|
rows.append(
|
|
{
|
|
"timestamp": humanize_dt(entry.timestamp),
|
|
"evento": entry.event,
|
|
"payload": payload_pretty,
|
|
"hash": (
|
|
f"{entry.hash[:8]}…{entry.hash[-8:]}"
|
|
if len(entry.hash) > 16
|
|
else entry.hash
|
|
),
|
|
}
|
|
)
|
|
st.dataframe(rows, use_container_width=True, hide_index=True)
|
|
|
|
|
|
render()
|