mirror of
https://github.com/samjage/metro-warden.git
synced 2026-06-06 04:30:42 +00:00
Initial commit: Metro Warden TUI network operations center
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user