Initial commit: Metro Warden TUI network operations center

This commit is contained in:
2026-03-22 21:33:40 -04:00
commit 98a17d9b7e
45 changed files with 4215 additions and 0 deletions
+260
View File
@@ -0,0 +1,260 @@
"""
Settings Tab — application configuration form.
Reads from the StateStore and writes back on change.
Supports editing poll intervals, theme options, and plugin enable/disable.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict
from textual.app import ComposeResult
from textual.binding import Binding
from textual.widget import Widget
from textual.widgets import Button, Input, Label, Select, Static, Switch
CONFIG_PATH = Path(__file__).parent.parent.parent / "config" / "defaults.toml"
class SettingRow(Widget):
"""A single label + control row in the settings form."""
DEFAULT_CSS = """
SettingRow {
layout: horizontal;
height: 3;
align: left middle;
margin-bottom: 1;
}
SettingRow .setting-label {
width: 28;
color: $text;
content-align: left middle;
}
SettingRow .setting-control {
width: 24;
}
SettingRow .setting-desc {
width: 1fr;
color: $text-muted;
content-align: left middle;
padding-left: 2;
}
"""
def __init__(self, label: str, control: Widget, description: str = "", **kwargs) -> None:
super().__init__(**kwargs)
self._label = label
self._control = control
self._description = description
def compose(self) -> ComposeResult:
yield Static(self._label, classes="setting-label")
yield self._control
if self._description:
yield Static(self._description, classes="setting-desc")
class SettingsTab(Widget):
"""
Application settings form.
Changes are applied immediately to the state store and persisted
to config/defaults.toml on save.
"""
DEFAULT_CSS = """
SettingsTab {
layout: vertical;
height: 1fr;
padding: 1 2;
overflow-y: auto;
}
SettingsTab .tab-title {
color: $primary;
text-style: bold;
height: 1;
margin-bottom: 1;
}
SettingsTab .section-header {
color: $accent;
text-style: bold;
height: 1;
margin-top: 2;
margin-bottom: 1;
}
SettingsTab .button-row {
layout: horizontal;
height: 3;
margin-top: 2;
}
SettingsTab Button {
margin-right: 2;
}
SettingsTab .status-label {
height: 1;
color: $success;
margin-top: 1;
}
"""
BINDINGS = [Binding("s", "save_settings", "Save")]
def compose(self) -> ComposeResult:
yield Static("// SETTINGS — Configuration", classes="tab-title")
yield Static("Polling Intervals", classes="section-header")
yield SettingRow(
"Network poll interval (s):",
Input(value="5", id="cfg-network-interval", classes="setting-control"),
"How often to refresh interface stats",
)
yield SettingRow(
"DNS poll interval (s):",
Input(value="30", id="cfg-dns-interval", classes="setting-control"),
"How often to check DNS resolver health",
)
yield SettingRow(
"System poll interval (s):",
Input(value="3", id="cfg-system-interval", classes="setting-control"),
"How often to sample CPU / memory",
)
yield SettingRow(
"Firewall poll interval (s):",
Input(value="60", id="cfg-firewall-interval", classes="setting-control"),
"How often to re-read firewall rules",
)
yield Static("Plugins", classes="section-header")
yield SettingRow(
"Network plugin:",
Switch(value=True, id="cfg-plugin-network", classes="setting-control"),
"Monitor network interfaces",
)
yield SettingRow(
"DNS plugin:",
Switch(value=True, id="cfg-plugin-dns", classes="setting-control"),
"Monitor DNS resolver health",
)
yield SettingRow(
"System plugin:",
Switch(value=True, id="cfg-plugin-system", classes="setting-control"),
"Monitor CPU / memory / disk",
)
yield SettingRow(
"Firewall plugin:",
Switch(value=True, id="cfg-plugin-firewall", classes="setting-control"),
"Read firewall rules",
)
yield Static("Display", classes="section-header")
yield SettingRow(
"Signal history limit:",
Input(value="500", id="cfg-signal-limit", classes="setting-control"),
"Max events shown in Signals tab",
)
yield SettingRow(
"Chronicle buffer:",
Input(value="2000", id="cfg-chronicle-buffer", classes="setting-control"),
"Max events retained in Chronicle",
)
with Widget(classes="button-row"):
yield Button("Save", id="btn-save", variant="primary")
yield Button("Reset to defaults", id="btn-reset", variant="default")
yield Static("", id="settings-status", classes="status-label")
def on_mount(self) -> None:
self._load_from_state()
def _load_from_state(self) -> None:
app = self.app
if not hasattr(app, "state"):
return
mappings = {
"settings.network_interval": "cfg-network-interval",
"settings.dns_interval": "cfg-dns-interval",
"settings.system_interval": "cfg-system-interval",
"settings.firewall_interval": "cfg-firewall-interval",
"settings.signal_limit": "cfg-signal-limit",
"settings.chronicle_buffer": "cfg-chronicle-buffer",
}
for state_key, widget_id in mappings.items():
val = app.state.get(state_key)
if val is not None:
try:
widget = self.query_one(f"#{widget_id}", Input)
widget.value = str(val)
except Exception:
pass
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "btn-save":
self.action_save_settings()
elif event.button.id == "btn-reset":
self._reset_to_defaults()
def action_save_settings(self) -> None:
app = self.app
status = self.query_one("#settings-status", Static)
if not hasattr(app, "state"):
status.update("[red]No state store available[/red]")
return
try:
mappings = {
"settings.network_interval": ("cfg-network-interval", float),
"settings.dns_interval": ("cfg-dns-interval", float),
"settings.system_interval": ("cfg-system-interval", float),
"settings.firewall_interval": ("cfg-firewall-interval", float),
"settings.signal_limit": ("cfg-signal-limit", int),
"settings.chronicle_buffer": ("cfg-chronicle-buffer", int),
}
for state_key, (widget_id, cast) in mappings.items():
widget = self.query_one(f"#{widget_id}", Input)
app.state.set(state_key, cast(widget.value))
# Persist to TOML
self._persist_toml()
status.update("[green]Settings saved.[/green]")
except Exception as exc:
status.update(f"[red]Error: {exc}[/red]")
def _reset_to_defaults(self) -> None:
defaults = {
"cfg-network-interval": "5",
"cfg-dns-interval": "30",
"cfg-system-interval": "3",
"cfg-firewall-interval": "60",
"cfg-signal-limit": "500",
"cfg-chronicle-buffer": "2000",
}
for widget_id, value in defaults.items():
try:
self.query_one(f"#{widget_id}", Input).value = value
except Exception:
pass
self.query_one("#settings-status", Static).update("[yellow]Defaults restored. Press Save to apply.[/yellow]")
def _persist_toml(self) -> None:
try:
import toml
data = {
"polling": {
"network_interval": float(self.query_one("#cfg-network-interval", Input).value),
"dns_interval": float(self.query_one("#cfg-dns-interval", Input).value),
"system_interval": float(self.query_one("#cfg-system-interval", Input).value),
"firewall_interval": float(self.query_one("#cfg-firewall-interval", Input).value),
},
"display": {
"signal_limit": int(self.query_one("#cfg-signal-limit", Input).value),
"chronicle_buffer": int(self.query_one("#cfg-chronicle-buffer", Input).value),
},
}
CONFIG_PATH.write_text(toml.dumps(data))
except Exception as exc:
pass # non-fatal