feat(gui): traduzione italiana, logo Cerbero, saldi live e Forza ciclo
* 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>
This commit is contained in:
@@ -3,13 +3,17 @@
|
||||
The GUI (and other out-of-band tooling) records operator intent in the
|
||||
SQLite ``manual_actions`` table; this consumer pulls those rows and
|
||||
dispatches them through the same primitives the engine uses internally
|
||||
(``KillSwitch.arm`` / ``disarm``) so the audit chain remains the single
|
||||
source of truth for state transitions.
|
||||
(``KillSwitch.arm`` / ``disarm``, ``Orchestrator.run_*``) so the audit
|
||||
chain remains the single source of truth for state transitions.
|
||||
|
||||
Currently supported kinds:
|
||||
Supported kinds:
|
||||
|
||||
* ``arm_kill`` — payload ``{"reason": str}``; arms the kill switch.
|
||||
* ``disarm_kill`` — payload ``{"reason": str}``; disarms it.
|
||||
* ``run_cycle`` — payload ``{"cycle": "entry"|"monitor"|"health"}``;
|
||||
forces an immediate run of the named cycle. Only available when the
|
||||
consumer is invoked with a ``cycle_runners`` mapping (the orchestrator
|
||||
populates it at scheduler-install time).
|
||||
|
||||
Future kinds (``force_close``, ``approve_proposal``,
|
||||
``reject_proposal``) are recognised by the ``ManualAction`` schema but
|
||||
@@ -21,6 +25,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -30,7 +35,10 @@ from cerbero_bite.state import connect, transaction
|
||||
if TYPE_CHECKING:
|
||||
from cerbero_bite.runtime.dependencies import RuntimeContext
|
||||
|
||||
__all__ = ["consume_manual_actions"]
|
||||
__all__ = ["CycleRunner", "consume_manual_actions"]
|
||||
|
||||
|
||||
CycleRunner = Callable[[], Awaitable[object]]
|
||||
|
||||
|
||||
_log = logging.getLogger("cerbero_bite.runtime.manual_actions")
|
||||
@@ -48,7 +56,10 @@ def _parse_payload(raw: str | None) -> dict[str, object]:
|
||||
|
||||
|
||||
async def consume_manual_actions(
|
||||
ctx: RuntimeContext, *, now: datetime | None = None
|
||||
ctx: RuntimeContext,
|
||||
*,
|
||||
cycle_runners: dict[str, CycleRunner] | None = None,
|
||||
now: datetime | None = None,
|
||||
) -> int:
|
||||
"""Drain the queue. Return the number of actions processed.
|
||||
|
||||
@@ -83,6 +94,19 @@ async def consume_manual_actions(
|
||||
elif action.kind == "disarm_kill":
|
||||
reason = str(payload.get("reason", "manual via GUI"))
|
||||
ctx.kill_switch.disarm(reason=reason, source="manual_gui")
|
||||
elif action.kind == "run_cycle":
|
||||
cycle = str(payload.get("cycle", "")).strip().lower()
|
||||
if cycle_runners is None:
|
||||
result = "not_supported"
|
||||
_log.warning(
|
||||
"run_cycle dispatched without cycle_runners; "
|
||||
"falling back to not_supported"
|
||||
)
|
||||
elif cycle not in cycle_runners:
|
||||
result = f"error: unknown cycle '{cycle}'"
|
||||
else:
|
||||
await cycle_runners[cycle]()
|
||||
result = f"ok: ran {cycle}"
|
||||
else:
|
||||
result = "not_supported"
|
||||
_log.warning(
|
||||
|
||||
@@ -234,7 +234,14 @@ class Orchestrator:
|
||||
|
||||
async def _manual_actions() -> None:
|
||||
async def _do() -> None:
|
||||
await consume_manual_actions(self._ctx)
|
||||
await consume_manual_actions(
|
||||
self._ctx,
|
||||
cycle_runners={
|
||||
"entry": self.run_entry,
|
||||
"monitor": self.run_monitor,
|
||||
"health": self.run_health,
|
||||
},
|
||||
)
|
||||
|
||||
await _safe("manual_actions", _do)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user