docs: align 05/06/09/11 with implemented GUI Phases A–D

* docs/11-gui-streamlit.md — replaces the original spec with what was
  actually built: implementation status table, real page filenames
  (1_Status, 2_Audit, 3_Equity, 4_History, 5_Position), per-page
  inventory of implemented vs deferred sections, GUI ↔ engine table
  showing arm_kill/disarm_kill via manual_actions and the
  not_supported markers for force_close + approve/reject_proposal,
  consumer signature with cron */1, lock model clarified (no GUI
  lockfile), DoD updated with current state.
* docs/05-data-model.md — manual_actions is no longer "pianificata":
  populated by gui/data_layer.py, drained by the manual_actions job;
  per-kind status table (arm/disarm OK, others not_supported).
* docs/09-development-roadmap.md — Phase 4.5 marked implemented with
  per-task / markers for the deferred items (auto-refresh,
  AppTest, force-close hook).
* docs/06-operational-flow.md — adds Flusso 5b describing the
  manual_actions consumer pattern (enqueue → KillSwitch transition →
  audit log linkage).

360/360 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-30 13:31:25 +02:00
parent e8345a29c8
commit da88e7f746
4 changed files with 251 additions and 160 deletions
+23 -10
View File
@@ -152,27 +152,40 @@ CREATE TABLE dvol_history (
### `manual_actions` ### `manual_actions`
Coda di azioni manuali generate dalla GUI Streamlit (vedi Coda di azioni manuali generate dalla GUI Streamlit (vedi
`11-gui-streamlit.md`). Schema previsto in vista della Fase 4.5; al `11-gui-streamlit.md`). La tabella è popolata dal layer
momento la GUI non è implementata e la tabella resta vuota. `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 ```sql
CREATE TABLE manual_actions ( CREATE TABLE manual_actions (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL, -- approve_proposal, reject_proposal, kind TEXT NOT NULL, -- arm_kill, disarm_kill,
-- force_close, arm_kill, disarm_kill -- force_close, approve_proposal, reject_proposal
proposal_id TEXT, -- NULL se l'azione non è legata a una proposta 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, created_at TEXT NOT NULL,
consumed_at TEXT, -- NULL = ancora da processare consumed_at TEXT, -- NULL = ancora da processare
consumed_by TEXT, consumed_by TEXT, -- "engine" quando applicata dal consumer
result TEXT result TEXT -- "ok" / "not_supported" / "error: ..."
); );
CREATE INDEX idx_manual_actions_unconsumed ON manual_actions(consumed_at); CREATE INDEX idx_manual_actions_unconsumed ON manual_actions(consumed_at);
``` ```
Le `manual_actions` non bypassano i risk control: il consumer Stato implementativo per `kind`:
(quando esisterà) applicherà gli stessi check di
`safety.system_healthy()` prima di eseguire. | `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` ### `system_state`
+20
View File
@@ -154,6 +154,26 @@ Trigger: ogni 5 minuti.
Il dead-man (`scripts/dead_man.sh`) sorveglia che `HEALTH_OK` venga Il dead-man (`scripts/dead_man.sh`) sorveglia che `HEALTH_OK` venga
scritto: silenzio > 15 min → kill switch via SQLite e alert. 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 ## Flusso 6 — Recovery dopo crash
All'avvio o dopo un riavvio del container: All'avvio o dopo un riavvio del container:
+25 -16
View File
@@ -126,29 +126,38 @@ Definition of Done:
- Engine può girare in `--dry-run` per 24h senza errori - Engine può girare in `--dry-run` per 24h senza errori
- I log sono leggibili e completi - 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 **Obiettivo:** dashboard locale per osservazione e azioni manuali. Spec
dettagliata in `11-gui-streamlit.md`. dettagliata in `11-gui-streamlit.md`.
Tasks: Implementata in quattro round (AD):
1. Setup `gui/main.py` + sidebar nav + auto-refresh 1. `gui/main.py` + sidebar nav (auto-refresh attivo non cablato; il
2. Pagina Status (engine, capitale, MCP health, kill switch panel) re-render Streamlit è sufficiente per la frequenza tipica)
3. Pagina Equity (curve, drawdown, monthly stats) 2. Pagina Status (engine state, kill switch panel con typed
4. Pagina Position (legs, payoff plotly, decision history, force-close) confirmation, audit anchor, open positions)
5. Pagina History (filtri, KPI, export CSV) 3. Pagina Equity (cumulative P&L, drawdown, P&L distribution per
6. Pagina Audit (log live, verify chain, search) close reason, per-month stats)
7. Tabella `manual_actions` + consumer job APScheduler nell'engine 4. ✅ Pagina Position (legs from entry snapshot, payoff plotly per
8. Test integration con `streamlit.testing.v1.AppTest` 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` - `cerbero-bite gui` lancia la dashboard su `127.0.0.1:8765`
- Tutte le 5 pagine raggiungibili e popolate - Tutte le 5 pagine raggiungibili e popolate
- Disarm da GUI loggato in audit chain ed effettivo entro 30 sec - Disarm da GUI loggato in audit chain (`source="manual_gui"`) ed
- Force-close da GUI consumato dall'engine entro 30 sec effettivo entro ~1 minuto
- Test integration su ogni pagina passing - ⏳ 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) ## Fase 5 — Reporting e UX (3-5 giorni)
+183 -134
View File
@@ -46,129 +46,152 @@ uv run streamlit run src/cerbero_bite/gui/main.py \
--browser.gatherUsageStats false --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 ## Layout cartelle
``` ```
src/cerbero_bite/gui/ src/cerbero_bite/gui/
├── __init__.py ├── __init__.py
├── main.py # entry point streamlit, sidebar nav ├── main.py # entry Streamlit, sidebar, home
├── pages/ ├── data_layer.py # wrapper read-only + write helpers
│ ├── 1_📊_status.py └── pages/
├── 2_📈_equity.py ├── 1_📊_Status.py # health, kill switch, audit anchor
├── 3_💼_position.py ├── 2_🔍_Audit.py # log stream + chain integrity
├── 4_📜_history.py ├── 3_📈_Equity.py # cumulative P&L + drawdown
── 5_🔍_audit.py ── 4_📜_History.py # closed trades + KPI + CSV
├── components/ └── 5_💼_Position.py # drilldown + payoff plotly
│ ├── 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
``` ```
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 ## Pagine
### 1. 📊 Status (home) ### 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), - **Engine status banner** colorato in base alla health derivata dalla
uptime, ultimo health check, kill_switch state, kill_reason se armato. combinazione `system_state.kill_switch` + età di `last_health_check`
- **Capitale**: equity corrente da `cerbero-portfolio` (cache ultimo (`running`/`degraded`/`stopped`/`killed`/`unknown`).
valore noto + timestamp), variazione % vs giorno prima, vs settimana, - **Top metric tiles**: posizioni aperte, età ultimo health check,
vs mese. `started_at`, `config_version`.
- **Posizione attiva**: card con riepilogo (proposal_id, expiry, credit, - **Kill switch controls**: form arm/disarm con typed confirmation
P&L unrealized stimato, days_to_expiry) o "nessuna posizione aperta". (`"yes I am sure"`) + reason obbligatoria. La submission scrive
- **MCP health grid**: 8 box, uno per server, con latenza ms e semaforo. un'azione in `manual_actions`; il consumer la applica entro un minuto.
- **Pending action**: se l'engine ha una proposta in attesa di conferma - **Pending manual actions**: tabella delle azioni in coda non ancora
e il timeout Telegram è scaduto, qui appare una card con `Approve`/`Reject`. consumate (visibile solo se la coda è non vuota).
Effetto: la decisione viene scritta in coda e il decision orchestrator - **Audit anchor**: hash chain head persistito in `system_state`.
la legge al prossimo health-check. - **Open positions table**: spread type, contracts, credit, max loss,
- **Big buttons**: `🟢 Disarm` / `🔴 Arm Kill Switch` (con conferma strikes, status, opened/expiry.
typed `"yes I am sure"`).
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 - **Chain integrity verify**: bottone che richiama `verify_chain` e
tracking. Risoluzione giornaliera. Sovrapposizione opzionale: riporta numero di entries verificate o l'errore di mismatch.
- banda Monte Carlo P5/P50/P95 (statica, dal documento) - **Filtri**: limit (10500) + event filter (auto-popolato dagli event
- DVOL nel tempo (asse Y secondario) effettivamente presenti nella tail).
- eventi macro (vertical lines sui giorni FOMC/CPI) - **Event-count strip**: `Counter` dei tipi di evento nella finestra.
- **Drawdown rolling** (sotto curve): area chart del DD% corrente. - **Tail table**: timestamp, event, payload JSON canonico, hash
- **P&L distribution** (histogram): trade chiusi raggruppati per outcome abbreviato — newest-first.
(profit_take, stop_loss, vol_stop, time_stop, ecc.).
- **Tabella mensile**: per ogni mese — n trade, win rate, P&L, max DD.
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: Window picker: All time, last 30/90 giorni, year-to-date. Banda Monte
Carlo, overlay DVOL e linee eventi macro non sono ancora implementati.
- **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.
### 4. 📜 History ### 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. - **Window picker**: All time, last 7/30/90 giorni, year-to-date.
- **Tabella trade chiusi** (`st.dataframe` sortable): proposal_id, - **Filtri di dettaglio**: multiselect su `close_reason`, radio
opened_at, closed_at, expiry, n_contracts, credit_usd, debit_paid_usd, vincitori/perdenti/tutti.
pnl_usd, outcome, days_held. - **KPI strip a sei tile**: trades, win rate, total P&L, avg win,
- **KPI strip**: n trade, win rate, avg win, avg loss, edge per trade, avg loss, edge per trade.
edge cumulato. - **Tabella trade chiusi**: proposal_id (short), spread type, asset,
- **Confronto Monte Carlo**: side-by-side delle metriche reali vs contracts, strikes, credit/max_loss, P&L, close_reason, days_held,
attese da simulazione, con delta in %. opened/closed/expiry.
- **Export CSV**: bottone download per uso fiscale. - **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`. - **Position selector** con label `proposal_id · spread_type ·
Auto-refresh 5 sec. short/long · status`. Supporta deep-link via query string
- **Audit chain status**: bottone `Verify`. Mostra "✅ chain integra `?proposal_id=…`.
fino a 14.382 eventi" o "❌ tampering rilevato a evento N". - **Header tiles**: status, spread, contracts, credit USD; caption con
- **Search**: ricerca testuale negli ultimi 30 giorni di log. proposal_id pieno + opened/expiry.
- **Stats engine**: numero kill switch armati nell'ultimo mese, MCP - **Distance metrics**: short strike OTM%, days-to-expiry, days-held,
failure count per server, average decision loop latency. delta at entry, width % of spot.
- **Export log**: download `.jsonl.gz` per analisi forensica. - **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 ## Comunicazione GUI ↔ Engine
@@ -177,53 +200,70 @@ MCP. Tutto passa via:
| Azione GUI | Effetto | | Azione GUI | Effetto |
|---|---| |---|---|
| Visualizzazione stato | Read da `state/repository.py` (SQLite) | | Visualizzazione stato | Read da `state/repository.py` (SQLite) tramite `gui/data_layer.py` |
| Equity / storico | Read da SQLite + `data/log/*.jsonl` | | Equity / storico | Read da SQLite (`positions` con `status='closed'`) + audit log |
| MCP health | Read da `state.system_state.last_health_check` (l'engine fa il check) | | MCP health | Read indiretto da `system_state.last_health_check` (l'engine fa il probe) |
| **Disarm kill switch** | Write su `system_state` con `kill_switch=0`; l'engine al prossimo health check rileva e log `KILL_SWITCH_DISARMED` | | **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** | Write su `system_state` con `kill_switch=1, kill_reason="manual via GUI"` | | **Arm kill switch** | `enqueue_arm_kill(reason)` → riga `kind="arm_kill"`; consumer chiama `KillSwitch.arm` |
| **Force close** | Insert riga in tabella `manual_actions` (nuova) con `kind="force_close", proposal_id=...`; l'engine al prossimo monitor cycle la consuma | | Force close | Pianificato: `kind="force_close"`. Oggi il consumer marca `result="not_supported"`; richiede l'hook `Orchestrator.handle_force_close` |
| **Approve pending proposal** | Insert riga in `manual_actions` con `kind="approve_proposal", proposal_id=...` | | 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 ```sql
CREATE TABLE manual_actions ( CREATE TABLE manual_actions (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL, -- approve_proposal, reject_proposal, force_close, etc. kind TEXT NOT NULL,
proposal_id TEXT, proposal_id TEXT,
payload_json TEXT, payload_json TEXT,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
consumed_at TEXT, -- NULL = ancora da processare consumed_at TEXT,
consumed_by TEXT, consumed_by TEXT,
result TEXT result TEXT
); );
CREATE INDEX idx_manual_actions_unconsumed ON manual_actions(consumed_at); 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 ```python
async def consume_manual_actions(): # src/cerbero_bite/runtime/manual_actions_consumer.py — sintesi
actions = state.fetch_unconsumed_manual_actions() async def consume_manual_actions(ctx, *, now=None):
for a in actions: while (action := ctx.repository.next_unconsumed_action(...)) is not None:
if a.kind == "force_close": if action.kind == "arm_kill":
await orchestrator.handle_force_close(a.proposal_id, a.payload) ctx.kill_switch.arm(reason=payload.get("reason"), source="manual_gui")
elif a.kind == "approve_proposal": elif action.kind == "disarm_kill":
await orchestrator.handle_proposal_approved(a.proposal_id) ctx.kill_switch.disarm(reason=payload.get("reason"), source="manual_gui")
# etc. else:
state.mark_action_consumed(a.id, result="ok") result = "not_supported"
ctx.repository.mark_action_consumed(...)
``` ```
Le azioni write **non bypassano** i risk control: una `force_close` deve Le azioni write **non bypassano** i risk control: la transizione passa
comunque passare dal `safety.system_healthy()` e da una conferma typed sempre per `KillSwitch.arm/disarm`, che valida lo stato e logga in
nella GUI prima di essere scritta in coda. audit. La typed confirmation (`"yes I am sure"`) è gating lato GUI
prima dell'enqueue.
## Lock e concorrenza ## Lock e concorrenza
- L'engine tiene `data/.lockfile` esclusivo. - L'engine tiene `data/.lockfile` esclusivo via `runtime/lockfile.py`.
- La GUI tiene `data/.gui-lockfile` esclusivo (impedisce due tab/Streamlit aperti). - La GUI **non** acquisisce un lock dedicato; più tab Streamlit
- Entrambi possono leggere SQLite (modalità WAL). 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 - Le `manual_actions` sono il **canale di scrittura** condiviso, con
primary key auto-increment e flag `consumed_at` per consumo idempotente. 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** Telegram resta il canale primario; la GUI è canale di **fallback**
per quando Adriano è davanti al laptop e non al telefono. 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 (AD). 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 | | Task | Giorni stimati | Stato |
|---|---| |---|---|---|
| Setup `gui/main.py` + sidebar nav + autorefresh | 0.5 | | Setup `gui/main.py` + sidebar nav + autorefresh | 0.5 | ✅ (autorefresh non attivo) |
| Pagina Status + MCP health grid + kill_switch panel | 0.5 | | Pagina Status + kill_switch panel | 0.5 | ✅ (MCP health grid non implementata) |
| Pagina Equity + drawdown + plot mensili | 0.5 | | Pagina Equity + drawdown + plot mensili | 0.5 | ✅ |
| Pagina Position + payoff plotly + decision history | 1.0 | | Pagina Position + payoff plotly + decision history | 1.0 | ✅ (greche live e force-close differiti) |
| Pagina History + filtri + export CSV | 0.5 | | Pagina History + filtri + export CSV | 0.5 | ✅ |
| Pagina Audit + search log + verify chain | 0.5 | | Pagina Audit + verify chain | 0.5 | ✅ (search e export gz differiti) |
| `manual_actions` table + consumer job APScheduler | 0.5 | | `manual_actions` consumer + APScheduler | 0.5 | ✅ (arm/disarm; force_close = `not_supported`) |
| Test integration (Streamlit AppTest framework) | 0.5 | | Test integration (Streamlit AppTest) | 0.5 | ⏳ |
| **Totale** | **~4 giorni** | | **Totale stimato** | **~4 giorni** | |
Definition of Done: Definition of Done — stato attuale:
- `cerbero-bite gui` lancia la dashboard - ✅ `cerbero-bite gui` lancia la dashboard su `127.0.0.1:8765`
- Tutte le 5 pagine raggiungibili e popolate (anche con dati fake) - ✅ Tutte le 5 pagine raggiungibili e popolate dai dati di runtime
- Disarm da GUI loggato in audit chain ed effettivo entro 30 sec - ✅ Disarm da GUI loggato in audit chain (`source="manual_gui"`) ed
- Force-close da GUI consumato dall'engine entro 30 sec effettivo entro ~1 minuto (cron `*/1`)
- Test integration con `streamlit.testing.v1.AppTest` per ogni pagina - ⏳ 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.