feat(gui): Phase A — read-only Streamlit dashboard (Status + Audit)
Implements the foundation of the local observation dashboard described in docs/11-gui-streamlit.md: * gui/data_layer.py — read-only wrappers over Repository (system_state, open positions) and audit_log (tail iteration, chain verify). The GUI never imports runtime/ nor calls MCP services. * gui/main.py — Streamlit entry point with sidebar (engine health badge, kill switch banner, last health check age), home overview. * gui/pages/1_📊_Status.py — engine status with colored health banner, kill switch detail, audit anchor, open positions table. * gui/pages/2_🔍_Audit.py — live audit log stream (newest-first), event filters, hash-chain integrity verify button. * cli.py gui — replaces the placeholder with os.execvpe to `python -m streamlit run` bound to 127.0.0.1, --browser.gatherUsageStats false; --db / --audit paths exported via env to the GUI process. * pyproject.toml — N999 ignore for src/cerbero_bite/gui/pages/* (Streamlit auto-discovers pages whose filename contains numbers and emoji icons). Smoke test: GUI launches, HTTP 200 on / and /_stcore/health, data layer correctly reflects current testnet state (engine=running, kill_switch disarmed, 0 open positions, audit chain integra 7 entries). 353/353 tests still pass; ruff clean; mypy strict src clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
"""Streamlit entry point for the Cerbero Bite dashboard.
|
||||
|
||||
Launch with::
|
||||
|
||||
cerbero-bite gui
|
||||
|
||||
or directly::
|
||||
|
||||
uv run streamlit run src/cerbero_bite/gui/main.py \
|
||||
--server.address 127.0.0.1 \
|
||||
--server.port 8765 \
|
||||
--server.headless true
|
||||
|
||||
The dashboard is **read-mostly**: it reads SQLite + the audit log and
|
||||
never imports ``runtime/`` modules. Each Streamlit page is in
|
||||
``gui/pages/`` and Streamlit auto-discovers them.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import streamlit as st
|
||||
|
||||
from cerbero_bite.gui.data_layer import (
|
||||
DEFAULT_AUDIT_PATH,
|
||||
DEFAULT_DB_PATH,
|
||||
humanize_age,
|
||||
humanize_dt,
|
||||
load_engine_snapshot,
|
||||
)
|
||||
|
||||
PAGE_TITLE = "Cerbero Bite — Dashboard"
|
||||
PAGE_ICON = "🐺"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Path resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _resolve_paths() -> tuple[Path, Path]:
|
||||
"""Read DB / audit paths from env (settable by ``cerbero-bite gui``)."""
|
||||
db_path = Path(os.environ.get("CERBERO_BITE_GUI_DB", DEFAULT_DB_PATH))
|
||||
audit_path = Path(os.environ.get("CERBERO_BITE_GUI_AUDIT", DEFAULT_AUDIT_PATH))
|
||||
return db_path, audit_path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sidebar
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
_HEALTH_BADGES: dict[str, tuple[str, str]] = {
|
||||
"running": ("🟢", "RUNNING"),
|
||||
"degraded": ("🟡", "DEGRADED"),
|
||||
"killed": ("🔴", "KILL SWITCH"),
|
||||
"stopped": ("⚫", "STOPPED"),
|
||||
"unknown": ("⚪", "UNKNOWN"),
|
||||
}
|
||||
|
||||
|
||||
def _render_sidebar(db_path: Path, audit_path: Path) -> None:
|
||||
snap = load_engine_snapshot(db_path=db_path)
|
||||
icon, label = _HEALTH_BADGES.get(snap.health, ("⚪", "UNKNOWN"))
|
||||
|
||||
st.sidebar.markdown(f"### {icon} {label}")
|
||||
if snap.kill_switch_armed:
|
||||
st.sidebar.error(
|
||||
f"**Kill switch armed**\n\n"
|
||||
f"reason: {snap.kill_reason or '—'}\n\n"
|
||||
f"since: {humanize_dt(snap.kill_at)}"
|
||||
)
|
||||
|
||||
st.sidebar.metric(
|
||||
"Last health check",
|
||||
humanize_age(snap.last_health_check_age_s),
|
||||
)
|
||||
st.sidebar.metric("Open positions", snap.open_positions)
|
||||
st.sidebar.caption(f"config: `{snap.config_version or '—'}`")
|
||||
|
||||
st.sidebar.divider()
|
||||
st.sidebar.caption("Read-only • localhost only")
|
||||
st.sidebar.caption(f"db: `{db_path}`")
|
||||
st.sidebar.caption(f"audit: `{audit_path}`")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Home page
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def main() -> None:
|
||||
st.set_page_config(
|
||||
page_title=PAGE_TITLE,
|
||||
page_icon=PAGE_ICON,
|
||||
layout="wide",
|
||||
initial_sidebar_state="expanded",
|
||||
)
|
||||
|
||||
db_path, audit_path = _resolve_paths()
|
||||
_render_sidebar(db_path, audit_path)
|
||||
|
||||
st.title(f"{PAGE_ICON} Cerbero Bite")
|
||||
st.caption(
|
||||
"Rule-based ETH credit-spread engine — read-only dashboard"
|
||||
)
|
||||
|
||||
st.markdown(
|
||||
"""
|
||||
Use the sidebar to navigate:
|
||||
|
||||
- **Status** — engine health, kill switch, open positions, audit anchor
|
||||
- **Audit** — live audit log stream + chain integrity verification
|
||||
|
||||
The dashboard reads `data/state.sqlite` and `data/audit.log` directly;
|
||||
it never calls MCP services or the broker. All write actions remain
|
||||
on the CLI for now.
|
||||
"""
|
||||
)
|
||||
|
||||
snap = load_engine_snapshot(db_path=db_path)
|
||||
cols = st.columns(4)
|
||||
cols[0].metric("Health", _HEALTH_BADGES[snap.health][1])
|
||||
cols[1].metric(
|
||||
"Kill switch",
|
||||
"ARMED" if snap.kill_switch_armed else "DISARMED",
|
||||
)
|
||||
cols[2].metric("Open positions", snap.open_positions)
|
||||
cols[3].metric(
|
||||
"Last health check",
|
||||
humanize_age(snap.last_health_check_age_s),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user