mirror of
https://github.com/samjage/metro-warden.git
synced 2026-06-06 04:40:41 +00:00
Initial commit: Metro Warden TUI network operations center
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
Signals Tab — live scrolling monitor of all bus events.
|
||||
|
||||
Subscribes to every topic ("**") and appends new events to a scrollable
|
||||
RichLog widget, styled to resemble an oscilloscope or signal analyser.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Label, RichLog, Static
|
||||
|
||||
|
||||
def _preview(data: Any, max_len: int = 80) -> str:
|
||||
"""Produce a compact single-line preview of event data."""
|
||||
if data is None:
|
||||
return "null"
|
||||
try:
|
||||
raw = json.dumps(data, default=str)
|
||||
except Exception:
|
||||
raw = str(data)
|
||||
if len(raw) > max_len:
|
||||
raw = raw[:max_len] + "…"
|
||||
return raw
|
||||
|
||||
|
||||
class SignalsTab(Widget):
|
||||
"""
|
||||
Live event signal monitor.
|
||||
|
||||
Every event published on the bus appears here with:
|
||||
- Timestamp (HH:MM:SS.mmm)
|
||||
- Topic (colour-coded by namespace)
|
||||
- Data preview (truncated JSON)
|
||||
"""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
SignalsTab {
|
||||
layout: vertical;
|
||||
height: 1fr;
|
||||
padding: 1 2;
|
||||
}
|
||||
SignalsTab .tab-title {
|
||||
color: $primary;
|
||||
text-style: bold;
|
||||
height: 1;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
SignalsTab RichLog {
|
||||
height: 1fr;
|
||||
border: round $primary;
|
||||
background: $surface;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
SignalsTab .hint {
|
||||
height: 1;
|
||||
color: $text-muted;
|
||||
margin-top: 1;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("c", "clear_log", "Clear"),
|
||||
Binding("p", "toggle_pause", "Pause"),
|
||||
]
|
||||
|
||||
# Colour map per topic namespace
|
||||
TOPIC_COLOURS = {
|
||||
"network": "cyan",
|
||||
"dns": "blue",
|
||||
"firewall": "red",
|
||||
"system": "green",
|
||||
"state": "yellow",
|
||||
"registry": "magenta",
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self._paused = False
|
||||
self._event_count = 0
|
||||
self._sub_id: str | None = None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static("// SIGNALS — Live Event Monitor", classes="tab-title")
|
||||
yield RichLog(id="signal-log", highlight=True, markup=True, max_lines=500)
|
||||
yield Static(
|
||||
"Press [bold]c[/bold] to clear | [bold]p[/bold] to pause",
|
||||
classes="hint",
|
||||
)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
app = self.app
|
||||
if hasattr(app, "bus"):
|
||||
self._sub_id = app.bus.subscribe("**", self._on_any_event)
|
||||
|
||||
def on_unmount(self) -> None:
|
||||
app = self.app
|
||||
if hasattr(app, "bus") and self._sub_id:
|
||||
app.bus.unsubscribe(self._sub_id)
|
||||
|
||||
def _on_any_event(self, topic: str, data: Any) -> None:
|
||||
if self._paused:
|
||||
return
|
||||
self.call_from_thread(self._append_event, topic, data)
|
||||
|
||||
def _append_event(self, topic: str, data: Any) -> None:
|
||||
self._event_count += 1
|
||||
log_widget = self.query_one("#signal-log", RichLog)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
ts = now.strftime("%H:%M:%S") + f".{now.microsecond // 1000:03d}"
|
||||
|
||||
namespace = topic.split(".")[0]
|
||||
colour = self.TOPIC_COLOURS.get(namespace, "white")
|
||||
preview = _preview(data)
|
||||
|
||||
# Format: [dim]HH:MM:SS.mmm[/] [colour]topic[/] data
|
||||
line = (
|
||||
f"[dim]{ts}[/dim] "
|
||||
f"[{colour} bold]{topic:<35}[/{colour} bold] "
|
||||
f"[dim]{preview}[/dim]"
|
||||
)
|
||||
log_widget.write(line)
|
||||
|
||||
def action_clear_log(self) -> None:
|
||||
log_widget = self.query_one("#signal-log", RichLog)
|
||||
log_widget.clear()
|
||||
self._event_count = 0
|
||||
|
||||
def action_toggle_pause(self) -> None:
|
||||
self._paused = not self._paused
|
||||
log_widget = self.query_one("#signal-log", RichLog)
|
||||
status = "[yellow]PAUSED[/yellow]" if self._paused else "[green]LIVE[/green]"
|
||||
log_widget.write(f" ── {status} ──")
|
||||
Reference in New Issue
Block a user