feat(gui+runtime): Phase D — kill-switch arm/disarm from the dashboard
Wires the GUI's first write path through the manual_actions queue: * runtime/manual_actions_consumer.py — drains the queue and dispatches arm_kill / disarm_kill via KillSwitch (preserving the audit chain). Unsupported kinds (force_close, approve/reject_proposal) are marked result="not_supported" so they don't sit forever. * runtime/orchestrator.py — adds a `manual_actions` job at */1 cron to the canonical scheduler manifest. * gui/data_layer.py — write helpers enqueue_arm_kill / enqueue_disarm_kill (the only write path the GUI uses) plus load_pending_manual_actions for the pending strip. * gui/pages/1_📊_Status.py — kill-switch arm/disarm panel with typed confirmation ("yes I am sure") + reason field; pending-actions table rendered when the queue is non-empty. End-to-end smoke against the testnet state.sqlite: GUI enqueue → consumer dispatch → KillSwitch transition → audit chain hash linkage holds, "source":"manual_gui" recorded. 7 new unit tests for the consumer (arm, disarm, drain, unsupported, default-reason, KillSwitchError handling, empty queue); 360/360 pass. ruff clean; mypy strict src clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ from cerbero_bite.runtime.dependencies import RuntimeContext, build_runtime
|
||||
from cerbero_bite.runtime.entry_cycle import EntryCycleResult, run_entry_cycle
|
||||
from cerbero_bite.runtime.health_check import HealthCheck, HealthCheckResult
|
||||
from cerbero_bite.runtime.lockfile import EngineLock
|
||||
from cerbero_bite.runtime.manual_actions_consumer import consume_manual_actions
|
||||
from cerbero_bite.runtime.monitor_cycle import MonitorCycleResult, run_monitor_cycle
|
||||
from cerbero_bite.runtime.recovery import recover_state
|
||||
from cerbero_bite.runtime.scheduler import JobSpec, build_scheduler
|
||||
@@ -45,6 +46,7 @@ _CRON_ENTRY = "0 14 * * MON"
|
||||
_CRON_MONITOR = "0 2,14 * * *"
|
||||
_CRON_HEALTH = "*/5 * * * *"
|
||||
_CRON_BACKUP = "0 * * * *"
|
||||
_CRON_MANUAL_ACTIONS = "*/1 * * * *"
|
||||
_BACKUP_RETENTION_DAYS = 30
|
||||
|
||||
|
||||
@@ -191,6 +193,7 @@ class Orchestrator:
|
||||
monitor_cron: str = _CRON_MONITOR,
|
||||
health_cron: str = _CRON_HEALTH,
|
||||
backup_cron: str = _CRON_BACKUP,
|
||||
manual_actions_cron: str = _CRON_MANUAL_ACTIONS,
|
||||
backup_dir: Path | None = None,
|
||||
backup_retention_days: int = _BACKUP_RETENTION_DAYS,
|
||||
) -> AsyncIOScheduler:
|
||||
@@ -229,12 +232,23 @@ class Orchestrator:
|
||||
|
||||
await _safe("backup", _do)
|
||||
|
||||
async def _manual_actions() -> None:
|
||||
async def _do() -> None:
|
||||
await consume_manual_actions(self._ctx)
|
||||
|
||||
await _safe("manual_actions", _do)
|
||||
|
||||
self._scheduler = build_scheduler(
|
||||
[
|
||||
JobSpec(name="entry", cron=entry_cron, coro_factory=_entry),
|
||||
JobSpec(name="monitor", cron=monitor_cron, coro_factory=_monitor),
|
||||
JobSpec(name="health", cron=health_cron, coro_factory=_health),
|
||||
JobSpec(name="backup", cron=backup_cron, coro_factory=_backup),
|
||||
JobSpec(
|
||||
name="manual_actions",
|
||||
cron=manual_actions_cron,
|
||||
coro_factory=_manual_actions,
|
||||
),
|
||||
]
|
||||
)
|
||||
return self._scheduler
|
||||
|
||||
Reference in New Issue
Block a user