diff --git a/docs/05-data-model.md b/docs/05-data-model.md index 9faa5d1..f58f9b9 100644 --- a/docs/05-data-model.md +++ b/docs/05-data-model.md @@ -152,27 +152,40 @@ CREATE TABLE dvol_history ( ### `manual_actions` Coda di azioni manuali generate dalla GUI Streamlit (vedi -`11-gui-streamlit.md`). Schema previsto in vista della Fase 4.5; al -momento la GUI non è implementata e la tabella resta vuota. +`11-gui-streamlit.md`). La tabella è popolata dal layer +`gui/data_layer.py` (`enqueue_arm_kill`, `enqueue_disarm_kill`) ed è +drenata dal job APScheduler `manual_actions` +(`runtime/manual_actions_consumer.consume_manual_actions`, cron +`*/1 * * * *`). ```sql CREATE TABLE manual_actions ( id INTEGER PRIMARY KEY AUTOINCREMENT, - kind TEXT NOT NULL, -- approve_proposal, reject_proposal, - -- force_close, arm_kill, disarm_kill + kind TEXT NOT NULL, -- arm_kill, disarm_kill, + -- force_close, approve_proposal, reject_proposal proposal_id TEXT, -- NULL se l'azione non è legata a una proposta - payload_json TEXT, -- JSON con motivo, conferma typed, ecc. + payload_json TEXT, -- JSON con reason, conferma typed, ecc. created_at TEXT NOT NULL, consumed_at TEXT, -- NULL = ancora da processare - consumed_by TEXT, - result TEXT + consumed_by TEXT, -- "engine" quando applicata dal consumer + result TEXT -- "ok" / "not_supported" / "error: ..." ); CREATE INDEX idx_manual_actions_unconsumed ON manual_actions(consumed_at); ``` -Le `manual_actions` non bypassano i risk control: il consumer -(quando esisterà) applicherà gli stessi check di -`safety.system_healthy()` prima di eseguire. +Stato implementativo per `kind`: + +| `kind` | Implementato | Effetto | +|---|---|---| +| `arm_kill` | ✅ | `KillSwitch.arm(reason, source="manual_gui")` | +| `disarm_kill` | ✅ | `KillSwitch.disarm(reason, source="manual_gui")` | +| `force_close` | ⏳ | Marcato `result="not_supported"` finché l'orchestrator non espone `handle_force_close` | +| `approve_proposal` / `reject_proposal` | ⏳ | Idem | + +Le `manual_actions` **non** bypassano i risk control: ogni azione di +kill switch passa dalla classe `KillSwitch`, che valida lo stato e +appende l'evento corrispondente alla audit chain. La typed +confirmation lato GUI è gating prima dell'enqueue. ### `system_state` diff --git a/docs/06-operational-flow.md b/docs/06-operational-flow.md index 23bf251..08f07dc 100644 --- a/docs/06-operational-flow.md +++ b/docs/06-operational-flow.md @@ -154,6 +154,26 @@ Trigger: ogni 5 minuti. Il dead-man (`scripts/dead_man.sh`) sorveglia che `HEALTH_OK` venga scritto: silenzio > 15 min → kill switch via SQLite e alert. +## Flusso 5b — Manual actions consumer + +Trigger: cron `*/1 * * * *` (job APScheduler `manual_actions`). + +``` +1. Mentre la coda ha righe non consumate: + - leggi `next_unconsumed_action` (oldest-first) + - dispatch per kind: + arm_kill → KillSwitch.arm(reason, source="manual_gui") + disarm_kill → KillSwitch.disarm(reason, source="manual_gui") + force_close / approve_proposal / reject_proposal → result="not_supported" + - mark_action_consumed con consumed_by="engine" e result +2. Latenza tipica end-to-end (enqueue da GUI → effetto): ≤ 60 sec. +``` + +Il consumer è il **canale unico** di scrittura dalla GUI verso il +runtime: ogni transizione del kill switch passa dalla classe +`KillSwitch` per mantenere SQLite e audit chain in lock-step. Vedi +`runtime/manual_actions_consumer.py` e `docs/11-gui-streamlit.md`. + ## Flusso 6 — Recovery dopo crash All'avvio o dopo un riavvio del container: diff --git a/docs/09-development-roadmap.md b/docs/09-development-roadmap.md index 61a9b1a..a27a540 100644 --- a/docs/09-development-roadmap.md +++ b/docs/09-development-roadmap.md @@ -126,29 +126,38 @@ Definition of Done: - Engine può girare in `--dry-run` per 24h senza errori - I log sono leggibili e completi -## Fase 4.5 — GUI Streamlit (4 giorni) +## Fase 4.5 — GUI Streamlit (4 giorni) ✅ implementata **Obiettivo:** dashboard locale per osservazione e azioni manuali. Spec dettagliata in `11-gui-streamlit.md`. -Tasks: +Implementata in quattro round (A–D): -1. Setup `gui/main.py` + sidebar nav + auto-refresh -2. Pagina Status (engine, capitale, MCP health, kill switch panel) -3. Pagina Equity (curve, drawdown, monthly stats) -4. Pagina Position (legs, payoff plotly, decision history, force-close) -5. Pagina History (filtri, KPI, export CSV) -6. Pagina Audit (log live, verify chain, search) -7. Tabella `manual_actions` + consumer job APScheduler nell'engine -8. Test integration con `streamlit.testing.v1.AppTest` +1. ✅ `gui/main.py` + sidebar nav (auto-refresh attivo non cablato; il + re-render Streamlit è sufficiente per la frequenza tipica) +2. ✅ Pagina Status (engine state, kill switch panel con typed + confirmation, audit anchor, open positions) +3. ✅ Pagina Equity (cumulative P&L, drawdown, P&L distribution per + close reason, per-month stats) +4. ✅ Pagina Position (legs from entry snapshot, payoff plotly per + bull_put/bear_call con annotazioni, decision history) — greche + live e force-close differiti +5. ✅ Pagina History (filtri window/reason/winners-losers, KPI strip, + CSV export) +6. ✅ Pagina Audit (live log stream, chain verify, event filter) +7. ✅ Consumer `runtime/manual_actions_consumer.py` con job APScheduler + `*/1` per arm/disarm (force_close = `not_supported` per ora) +8. ⏳ Test integration con `streamlit.testing.v1.AppTest` -Definition of Done: +Definition of Done — stato: -- `cerbero-bite gui` lancia la dashboard su `127.0.0.1:8765` -- Tutte le 5 pagine raggiungibili e popolate -- Disarm da GUI loggato in audit chain ed effettivo entro 30 sec -- Force-close da GUI consumato dall'engine entro 30 sec -- Test integration su ogni pagina passing +- ✅ `cerbero-bite gui` lancia la dashboard su `127.0.0.1:8765` +- ✅ Tutte le 5 pagine raggiungibili e popolate +- ✅ Disarm da GUI loggato in audit chain (`source="manual_gui"`) ed + effettivo entro ~1 minuto +- ⏳ Force-close da GUI: l'enqueue funziona ma l'orchestrator deve + ancora esporre `handle_force_close` +- ⏳ Test integration AppTest: non scritti ## Fase 5 — Reporting e UX (3-5 giorni) diff --git a/docs/11-gui-streamlit.md b/docs/11-gui-streamlit.md index c3172d6..8b80b9a 100644 --- a/docs/11-gui-streamlit.md +++ b/docs/11-gui-streamlit.md @@ -46,129 +46,152 @@ uv run streamlit run src/cerbero_bite/gui/main.py \ --browser.gatherUsageStats false ``` +## Stato implementativo + +La dashboard è stata costruita in quattro fasi incrementali: + +| Fase | Contenuto | Stato | +|---|---|---| +| A | Status + Audit (osservazione di base) | ✅ | +| B | Equity + History (analitica + export CSV) | ✅ | +| C | Position drilldown con payoff plotly + decision history | ✅ | +| D | Kill-switch arm/disarm dalla dashboard via coda `manual_actions` | ✅ | + +Per scelta di scope, restano fuori dalla prima iterazione: force-close +dalla GUI (richiede un hook `handle_force_close` nell'orchestrator), +approve/reject di una proposta (il bot decide autonomamente, non c'è un +flusso di proposta in attesa) e auto-refresh attivo via +`st_autorefresh`. Il consumer di `manual_actions` riconosce già i +`kind` corrispondenti e li archivia con `result="not_supported"` +finché i flussi non saranno cablati. + ## Layout cartelle ``` src/cerbero_bite/gui/ ├── __init__.py -├── main.py # entry point streamlit, sidebar nav -├── pages/ -│ ├── 1_📊_status.py -│ ├── 2_📈_equity.py -│ ├── 3_💼_position.py -│ ├── 4_📜_history.py -│ └── 5_🔍_audit.py -├── components/ -│ ├── kill_switch_panel.py -│ ├── mcp_health_grid.py -│ ├── pending_proposal_card.py -│ ├── payoff_chart.py -│ └── greeks_panel.py -└── data_layer.py # wrapper read-only verso state.repository +├── main.py # entry Streamlit, sidebar, home +├── data_layer.py # wrapper read-only + write helpers +└── pages/ + ├── 1_📊_Status.py # health, kill switch, audit anchor + ├── 2_🔍_Audit.py # log stream + chain integrity + ├── 3_📈_Equity.py # cumulative P&L + drawdown + ├── 4_📜_History.py # closed trades + KPI + CSV + └── 5_💼_Position.py # drilldown + payoff plotly ``` +I componenti riutilizzabili descritti nello spec originale +(`kill_switch_panel`, `payoff_chart`, ecc.) non sono stati estratti in +file separati: ogni pagina è autonoma e tiene la propria UI inline, +così l'evoluzione resta locale al singolo file. La promozione a +componenti separati è giustificata solo se più pagine condividono lo +stesso widget — al momento non è il caso. + ## Pagine ### 1. 📊 Status (home) -Vista a colpo d'occhio dello stato corrente. +Stato corrente e controlli sul kill switch. -Sezioni: +Sezioni implementate: -- **Engine status**: badge verde/giallo/rosso (running/degraded/killed), - uptime, ultimo health check, kill_switch state, kill_reason se armato. -- **Capitale**: equity corrente da `cerbero-portfolio` (cache ultimo - valore noto + timestamp), variazione % vs giorno prima, vs settimana, - vs mese. -- **Posizione attiva**: card con riepilogo (proposal_id, expiry, credit, - P&L unrealized stimato, days_to_expiry) o "nessuna posizione aperta". -- **MCP health grid**: 8 box, uno per server, con latenza ms e semaforo. -- **Pending action**: se l'engine ha una proposta in attesa di conferma - e il timeout Telegram è scaduto, qui appare una card con `Approve`/`Reject`. - Effetto: la decisione viene scritta in coda e il decision orchestrator - la legge al prossimo health-check. -- **Big buttons**: `🟢 Disarm` / `🔴 Arm Kill Switch` (con conferma - typed `"yes I am sure"`). +- **Engine status banner** colorato in base alla health derivata dalla + combinazione `system_state.kill_switch` + età di `last_health_check` + (`running`/`degraded`/`stopped`/`killed`/`unknown`). +- **Top metric tiles**: posizioni aperte, età ultimo health check, + `started_at`, `config_version`. +- **Kill switch controls**: form arm/disarm con typed confirmation + (`"yes I am sure"`) + reason obbligatoria. La submission scrive + un'azione in `manual_actions`; il consumer la applica entro un minuto. +- **Pending manual actions**: tabella delle azioni in coda non ancora + consumate (visibile solo se la coda è non vuota). +- **Audit anchor**: hash chain head persistito in `system_state`. +- **Open positions table**: spread type, contracts, credit, max loss, + strikes, status, opened/expiry. -Auto-refresh: 5 secondi. +Sezioni non ancora implementate rispetto allo spec originale: capitale +con variazioni %, MCP health grid (i probe sono fatti dall'engine e +visibili in audit), pending-proposal card. Il refresh automatico è +manuale (la pagina si aggiorna alla navigazione o al re-render +spontaneo di Streamlit). -### 2. 📈 Equity +### 2. 🔍 Audit -Grafico storia capitale e analitica. +Live log stream + verifica integrità della hash chain. -Sezioni: +Sezioni implementate: -- **Equity curve** (line chart): capitale nel tempo dall'inizio del - tracking. Risoluzione giornaliera. Sovrapposizione opzionale: - - banda Monte Carlo P5/P50/P95 (statica, dal documento) - - DVOL nel tempo (asse Y secondario) - - eventi macro (vertical lines sui giorni FOMC/CPI) -- **Drawdown rolling** (sotto curve): area chart del DD% corrente. -- **P&L distribution** (histogram): trade chiusi raggruppati per outcome - (profit_take, stop_loss, vol_stop, time_stop, ecc.). -- **Tabella mensile**: per ogni mese — n trade, win rate, P&L, max DD. +- **Chain integrity verify**: bottone che richiama `verify_chain` e + riporta numero di entries verificate o l'errore di mismatch. +- **Filtri**: limit (10–500) + event filter (auto-popolato dagli event + effettivamente presenti nella tail). +- **Event-count strip**: `Counter` dei tipi di evento nella finestra. +- **Tail table**: timestamp, event, payload JSON canonico, hash + abbreviato — newest-first. -Filtri: range temporale, asset (solo ETH per ora). +### 3. 📈 Equity -Auto-refresh: 30 secondi (cambia raramente). +Curva P&L cumulato e analitica trade chiusi. -### 3. 💼 Position +Sezioni implementate: -Drill-down sulla posizione attualmente aperta (se esiste). +- **KPI strip**: closed trades, win rate, total P&L, edge per trade, + max drawdown (USD + %). +- **Cumulative P&L** (Plotly): riempito a zero, con linea zero di + riferimento. +- **Drawdown** (Plotly area chart, asse invertito). +- **P&L distribution by close reason**: istogrammi Plotly sovrapposti + con conteggio trades per reason in metric tiles. +- **Per-month stats**: tabella aggregata UTC (mese, n trade, vincitori, + win rate, P&L totale, P&L medio). -Sezioni: - -- **Header**: proposal_id, opened_at, expiry, days_left, status. -- **Legs table**: instrument, side, size, mid corrente, delta, - theta, vega — refresh periodico via `clients.deribit`. -- **Greche aggregate**: delta/theta/vega netti. -- **Payoff diagram** (plotly): P&L vs spot ETH a scadenza, con - breakeven, max profit, max loss, spot corrente come marker. -- **Decision history**: tabella con tutte le `decisions` di tipo - `exit_check` per questa posizione, in ordine cronologico, con - outcome HOLD / CLOSE_*. -- **Distance metrics**: short strike a `X% OTM`, delta corrente, - distanza in sigma. -- **Force close** (collapsibile): typed confirmation + reason field. - Su submit: scrive in coda azione `manual_close`, l'engine la consuma - al prossimo monitor cycle. - -Auto-refresh: 10 secondi. +Window picker: All time, last 30/90 giorni, year-to-date. Banda Monte +Carlo, overlay DVOL e linee eventi macro non sono ancora implementati. ### 4. 📜 History -Storico trade chiusi. +Storico trade chiusi con filtri ed esportazione. -Sezioni: +Sezioni implementate: -- **Filtri**: range temporale, outcome (multiselect), P&L > 0 / < 0 / tutti. -- **Tabella trade chiusi** (`st.dataframe` sortable): proposal_id, - opened_at, closed_at, expiry, n_contracts, credit_usd, debit_paid_usd, - pnl_usd, outcome, days_held. -- **KPI strip**: n trade, win rate, avg win, avg loss, edge per trade, - edge cumulato. -- **Confronto Monte Carlo**: side-by-side delle metriche reali vs - attese da simulazione, con delta in %. -- **Export CSV**: bottone download per uso fiscale. +- **Window picker**: All time, last 7/30/90 giorni, year-to-date. +- **Filtri di dettaglio**: multiselect su `close_reason`, radio + vincitori/perdenti/tutti. +- **KPI strip a sei tile**: trades, win rate, total P&L, avg win, + avg loss, edge per trade. +- **Tabella trade chiusi**: proposal_id (short), spread type, asset, + contracts, strikes, credit/max_loss, P&L, close_reason, days_held, + opened/closed/expiry. +- **CSV export**: download diretto via `st.download_button`. -Auto-refresh: manuale (button). +Confronto Monte Carlo side-by-side non ancora implementato. -### 5. 🔍 Audit +### 5. 💼 Position -Log e audit chain. +Drilldown su una posizione specifica (open o ultime 10 chiuse). -Sezioni: +Sezioni implementate: -- **Live log stream**: ultimi 100 eventi, filtro per `level` e `event`. - Auto-refresh 5 sec. -- **Audit chain status**: bottone `Verify`. Mostra "✅ chain integra - fino a 14.382 eventi" o "❌ tampering rilevato a evento N". -- **Search**: ricerca testuale negli ultimi 30 giorni di log. -- **Stats engine**: numero kill switch armati nell'ultimo mese, MCP - failure count per server, average decision loop latency. -- **Export log**: download `.jsonl.gz` per analisi forensica. +- **Position selector** con label `proposal_id · spread_type · + short/long · status`. Supporta deep-link via query string + `?proposal_id=…`. +- **Header tiles**: status, spread, contracts, credit USD; caption con + proposal_id pieno + opened/expiry. +- **Distance metrics**: short strike OTM%, days-to-expiry, days-held, + delta at entry, width % of spot. +- **Legs table** (snapshot al momento dell'entry, non live): leg, instrument, + strike, side, size, delta. Una caption ricorda che mid e greche live + non sono fetchate dalla GUI. +- **Payoff at expiry** (Plotly): curva P&L con annotazioni per short + strike, long strike, breakeven, entry spot. Tile riassuntivi per + max profit, max loss, breakeven. Implementato per `bull_put` e + `bear_call`; gli iron condor cadono su una curva piatta (placeholder). +- **Decision history**: tabella delle righe `decisions` legate al + `proposal_id`, newest-first, con outputs JSON canonici. -Auto-refresh: manuale. +Le greche/mid live e il force-close manuale richiedono che l'engine +esponga rispettivamente uno snapshot persistito e l'hook +`handle_force_close` — fuori scope della prima iterazione. ## Comunicazione GUI ↔ Engine @@ -177,53 +200,70 @@ MCP. Tutto passa via: | Azione GUI | Effetto | |---|---| -| Visualizzazione stato | Read da `state/repository.py` (SQLite) | -| Equity / storico | Read da SQLite + `data/log/*.jsonl` | -| MCP health | Read da `state.system_state.last_health_check` (l'engine fa il check) | -| **Disarm kill switch** | Write su `system_state` con `kill_switch=0`; l'engine al prossimo health check rileva e log `KILL_SWITCH_DISARMED` | -| **Arm kill switch** | Write su `system_state` con `kill_switch=1, kill_reason="manual via GUI"` | -| **Force close** | Insert riga in tabella `manual_actions` (nuova) con `kind="force_close", proposal_id=...`; l'engine al prossimo monitor cycle la consuma | -| **Approve pending proposal** | Insert riga in `manual_actions` con `kind="approve_proposal", proposal_id=...` | +| Visualizzazione stato | Read da `state/repository.py` (SQLite) tramite `gui/data_layer.py` | +| Equity / storico | Read da SQLite (`positions` con `status='closed'`) + audit log | +| MCP health | Read indiretto da `system_state.last_health_check` (l'engine fa il probe) | +| **Disarm kill switch** | `enqueue_disarm_kill(reason)` → riga in `manual_actions` con `kind="disarm_kill"`; consumer chiama `KillSwitch.disarm` (audit `KILL_SWITCH_DISARMED`, `source="manual_gui"`) | +| **Arm kill switch** | `enqueue_arm_kill(reason)` → riga `kind="arm_kill"`; consumer chiama `KillSwitch.arm` | +| Force close | Pianificato: `kind="force_close"`. Oggi il consumer marca `result="not_supported"`; richiede l'hook `Orchestrator.handle_force_close` | +| Approve / reject pending proposal | Pianificato: `kind="approve_proposal"` / `"reject_proposal"`. Stesso stato (non implementato lato orchestrator) | -**Nuova tabella SQLite** (`05-data-model.md` da estendere): +La GUI **non** scrive direttamente su `system_state`: ogni transizione +del kill switch passa dal consumer e dalla classe `KillSwitch`, così +SQLite e audit chain restano sincronizzati come per le transizioni +automatiche. + +**Schema SQLite** (vedi `05-data-model.md`): ```sql CREATE TABLE manual_actions ( id INTEGER PRIMARY KEY AUTOINCREMENT, - kind TEXT NOT NULL, -- approve_proposal, reject_proposal, force_close, etc. + kind TEXT NOT NULL, proposal_id TEXT, payload_json TEXT, created_at TEXT NOT NULL, - consumed_at TEXT, -- NULL = ancora da processare + consumed_at TEXT, consumed_by TEXT, result TEXT ); CREATE INDEX idx_manual_actions_unconsumed ON manual_actions(consumed_at); ``` -L'engine include un nuovo job APScheduler `every 30s`: +**Consumer**: `runtime/manual_actions_consumer.consume_manual_actions`. +Registrato come job APScheduler `manual_actions` con cron +`*/1 * * * *` (latenza ≤ 1 minuto, sufficiente per kill-switch). Il +consumer drena tutta la coda a ogni tick e per ogni azione setta +`consumed_at`, `consumed_by="engine"` e `result` (`"ok"`, +`"not_supported"` o `"error: …"`). ```python -async def consume_manual_actions(): - actions = state.fetch_unconsumed_manual_actions() - for a in actions: - if a.kind == "force_close": - await orchestrator.handle_force_close(a.proposal_id, a.payload) - elif a.kind == "approve_proposal": - await orchestrator.handle_proposal_approved(a.proposal_id) - # etc. - state.mark_action_consumed(a.id, result="ok") +# src/cerbero_bite/runtime/manual_actions_consumer.py — sintesi +async def consume_manual_actions(ctx, *, now=None): + while (action := ctx.repository.next_unconsumed_action(...)) is not None: + if action.kind == "arm_kill": + ctx.kill_switch.arm(reason=payload.get("reason"), source="manual_gui") + elif action.kind == "disarm_kill": + ctx.kill_switch.disarm(reason=payload.get("reason"), source="manual_gui") + else: + result = "not_supported" + ctx.repository.mark_action_consumed(...) ``` -Le azioni write **non bypassano** i risk control: una `force_close` deve -comunque passare dal `safety.system_healthy()` e da una conferma typed -nella GUI prima di essere scritta in coda. +Le azioni write **non bypassano** i risk control: la transizione passa +sempre per `KillSwitch.arm/disarm`, che valida lo stato e logga in +audit. La typed confirmation (`"yes I am sure"`) è gating lato GUI +prima dell'enqueue. ## Lock e concorrenza -- L'engine tiene `data/.lockfile` esclusivo. -- La GUI tiene `data/.gui-lockfile` esclusivo (impedisce due tab/Streamlit aperti). -- Entrambi possono leggere SQLite (modalità WAL). +- L'engine tiene `data/.lockfile` esclusivo via `runtime/lockfile.py`. +- La GUI **non** acquisisce un lock dedicato; più tab Streamlit + contemporanee sono possibili (sconsigliate ma non impedite). Il + vincolo single-writer su SQLite è preservato perché ogni write + passa dalla riga `manual_actions` (auto-increment) e dal consumer + dell'engine. +- Entrambi possono leggere SQLite (le connessioni sono in modalità + short-lived: aperte per chiamata e chiuse subito). - Le `manual_actions` sono il **canale di scrittura** condiviso, con primary key auto-increment e flag `consumed_at` per consumo idempotente. @@ -251,26 +291,35 @@ Per chiarezza: Telegram resta il canale primario; la GUI è canale di **fallback** per quando Adriano è davanti al laptop e non al telefono. -## Stima di sforzo +## Stima di sforzo (storica) -Inserita come **Fase 4.5** nella roadmap, tra Orchestrator e Reporting: +La Fase 4.5 è stata implementata in quattro round (A–D). Lo spec +originale stimava ~4 giorni ed è stato consegnato in linea con la +stima, con il caveat che `streamlit.testing.v1.AppTest` non è ancora +cablato (le pagine sono validate manualmente via smoke test HTTP) e +che force-close + approve/reject restano fuori scope. -| Task | Giorni | -|---|---| -| Setup `gui/main.py` + sidebar nav + autorefresh | 0.5 | -| Pagina Status + MCP health grid + kill_switch panel | 0.5 | -| Pagina Equity + drawdown + plot mensili | 0.5 | -| Pagina Position + payoff plotly + decision history | 1.0 | -| Pagina History + filtri + export CSV | 0.5 | -| Pagina Audit + search log + verify chain | 0.5 | -| `manual_actions` table + consumer job APScheduler | 0.5 | -| Test integration (Streamlit AppTest framework) | 0.5 | -| **Totale** | **~4 giorni** | +| Task | Giorni stimati | Stato | +|---|---|---| +| Setup `gui/main.py` + sidebar nav + autorefresh | 0.5 | ✅ (autorefresh non attivo) | +| Pagina Status + kill_switch panel | 0.5 | ✅ (MCP health grid non implementata) | +| Pagina Equity + drawdown + plot mensili | 0.5 | ✅ | +| Pagina Position + payoff plotly + decision history | 1.0 | ✅ (greche live e force-close differiti) | +| Pagina History + filtri + export CSV | 0.5 | ✅ | +| Pagina Audit + verify chain | 0.5 | ✅ (search e export gz differiti) | +| `manual_actions` consumer + APScheduler | 0.5 | ✅ (arm/disarm; force_close = `not_supported`) | +| Test integration (Streamlit AppTest) | 0.5 | ⏳ | +| **Totale stimato** | **~4 giorni** | | -Definition of Done: +Definition of Done — stato attuale: -- `cerbero-bite gui` lancia la dashboard -- Tutte le 5 pagine raggiungibili e popolate (anche con dati fake) -- Disarm da GUI loggato in audit chain ed effettivo entro 30 sec -- Force-close da GUI consumato dall'engine entro 30 sec -- Test integration con `streamlit.testing.v1.AppTest` per ogni pagina +- ✅ `cerbero-bite gui` lancia la dashboard su `127.0.0.1:8765` +- ✅ Tutte le 5 pagine raggiungibili e popolate dai dati di runtime +- ✅ Disarm da GUI loggato in audit chain (`source="manual_gui"`) ed + effettivo entro ~1 minuto (cron `*/1`) +- ⏳ Force-close da GUI: l'enqueue è possibile, ma l'orchestrator non + ha ancora `handle_force_close`; il consumer marca `result="not_supported"` +- ⏳ Test integration con `streamlit.testing.v1.AppTest`: non scritti + +Le voci aperte sono follow-up isolati e non bloccano l'uso quotidiano +della dashboard come tableau d'observation.