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:
+23
-10
@@ -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`
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+183
-134
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user