feat(tui): enter rated TBW from the dashboard

Add a b key and a no-baseline banner that open a form for rated TBW and an
optional source URL. The value is persisted as an unverified override through
fenris-monitor baseline set under the existing polkit action, and the outcome
is reported synchronously. Verified overrides stay CLI-only (ADR 0012 section 7).
This commit is contained in:
xavierk
2026-10-05 20:01:32 +05:30
parent e26a851115
commit ef61ececc1
3 changed files with 331 additions and 3 deletions
+116 -3
View File
@@ -11,6 +11,8 @@ Criteria: TUI-1, TUI-2, TUI-4, CI-1, CI-2, CI-4, IN-3, LC-6, LC-8.
"""
from __future__ import annotations
import json
import math
import sqlite3
import sys
from datetime import date, datetime, timedelta, timezone
@@ -37,6 +39,7 @@ from .activity_selection import (
from .activity_plot import VolumePoint, volume_plot
from .projection import (
BaselineTier,
ConfidenceState,
ProjectionResult,
ProjectionStage,
@@ -75,7 +78,7 @@ from .themes import THEMES, get_graph_colors
_RESUME_HINT = "r Resume — enable monitoring and future boots"
_ACTION_LEGEND = (
"p Pause · " + _RESUME_HINT + "\n"
"c Collect now · t Today · [ ] Day · g Date · m Motion · d Disclosures · ? Help"
"b Rated TBW · c Collect now · t Today · [ ] Day · g Date · m Motion · d Disclosures · ? Help"
)
_LOCAL_ACTIVITY_STATE_LABELS = {
"so_far": "totals so far",
@@ -1678,6 +1681,74 @@ class HelpScreen(ModalScreen[None]):
self.dismiss()
class BaselineScreen(ModalScreen["tuple[float, str | None] | None"]):
"""Rated TBW entry; always stored as an unverified override (ADR 0012 §7)."""
BINDINGS = [
Binding("escape", "cancel", "Cancel", show=False),
]
CSS = """
BaselineScreen {
align: center middle;
}
#baseline-content {
width: 64;
height: auto;
border: heavy $accent;
padding: 1 2;
}
#baseline-error {
width: 100%;
height: auto;
color: $error;
}
"""
def compose(self) -> ComposeResult:
yield Container(
Static(
"[bold]Enter rated TBW[/bold]\n"
"Saved as an [bold]unverified[/bold] user-supplied override. "
"Verified overrides need full provenance via the CLI.",
id="baseline-header",
),
Static("Rated TBW (terabytes written, required):"),
Input(placeholder="e.g. 600", id="tbw-input"),
Static("Source URL (optional):"),
Input(placeholder="https://…", id="source-input"),
Static("", id="baseline-error"),
Static("[dim]Enter Save · Esc Cancel[/dim]", id="baseline-footer"),
id="baseline-content",
)
def on_mount(self) -> None:
self.query_one("#tbw-input").focus()
def on_input_submitted(self, event: Any) -> None:
error = self.query_one("#baseline-error")
raw = self.query_one("#tbw-input").value.strip()
if not raw:
error.update("[red]Rated TBW is required[/red]")
return
try:
tbw = float(raw)
except ValueError:
error.update("[red]Rated TBW must be a number of terabytes[/red]")
return
if not math.isfinite(tbw) or tbw <= 0:
error.update("[red]Rated TBW must be a positive number[/red]")
return
source = self.query_one("#source-input").value.strip()
if source and not source.startswith(("http://", "https://")):
error.update("[red]Source URL must start with http:// or https://[/red]")
return
self.dismiss((tbw, source or None))
def action_cancel(self) -> None:
self.dismiss(None)
class DatePickerScreen(ModalScreen[str | None]):
"""Modal screen for entering a date to browse."""
@@ -1782,6 +1853,7 @@ class FenrisTuiApp(App):
#usage-history, #live-activity { height: 1fr; min-height: 6; }
#bar-render, #live-render { min-height: 4; }
#local-day { height: auto; min-height: 2; color: $text; }
#baseline-banner { display: none; height: auto; background: $accent 15%; padding: 0 1; }
#paused-banner { display: none; height: auto; background: $warning 15%; padding: 0 1; }
#status-area { height: auto; max-height: 35%; scrollbar-size: 1 1; }
#service-strip { height: auto; padding: 0 1; border-top: solid $primary 35%; }
@@ -1806,6 +1878,7 @@ class FenrisTuiApp(App):
BINDINGS = [
Binding("p", "pause", "Pause", show=False),
Binding("r", "resume", "Resume", show=False),
Binding("b", "baseline", "Rated TBW", show=False),
Binding("c", "collect", "Collect now", show=False),
Binding("d", "disclose", "Disclosures", show=False),
Binding("t", "today", "Today", show=False),
@@ -1873,6 +1946,11 @@ class FenrisTuiApp(App):
yield Static(_IDENTITY_FULL, id="identity")
yield Static("Open with fenris (no sudo). Actions authenticate via polkit. ? Help", id="auth-notice")
yield Static("", id="paused-banner")
yield Static(
"No endurance baseline — press [bold]b[/bold] to enter your drive's rated TBW "
"and unlock the lifespan.",
id="baseline-banner",
)
with VerticalScroll(id="dashboard-scroll"):
with Container(id="main-grid"):
with VerticalScroll(id="endurance-panel", classes="dashboard-pane"):
@@ -2024,8 +2102,12 @@ class FenrisTuiApp(App):
self.query_one("#service-strip").update("\n".join(lines))
self._render_paused_banner(comp)
def _show_baseline_banner(self, show: bool) -> None:
self.query_one("#baseline-banner").display = show
def _render_empty_or_fault(self, comp: StatusComposition) -> None:
"""Render empty history or the shared store-fault classification."""
self._show_baseline_banner(not (comp.newer_schema or comp.store_fault))
if comp.newer_schema or comp.store_fault:
message = comp.explanation[:1].upper() + comp.explanation[1:]
self._render_headline("[bold red]%s[/bold red]" % message)
@@ -2063,12 +2145,17 @@ class FenrisTuiApp(App):
"""Render all four regions from live store data."""
# --- Headline band (§7.2) ---
if comp.sample_count == 0 and comp.day_count == 0:
self._show_baseline_banner(
conn.execute("SELECT COUNT(*) FROM endurance_baseline").fetchone()[0] == 0
)
self._render_headline(
"[bold]No observations yet[/bold]\n\n[bold]%s[/bold]" % _RESUME_HINT
)
else:
self._show_baseline_banner(False)
try:
proj = compute_projection(conn, self._clock_now)
self._show_baseline_banner(proj.baseline_tier == BaselineTier.NONE)
headline = self._format_headline(proj)
confidence = self._format_confidence(proj)
scenario = self._format_scenario(proj)
@@ -2429,6 +2516,27 @@ class FenrisTuiApp(App):
"""Collect now — synchronous outcome (spec §8.7, LC-8)."""
self._run_helper("collect")
def action_baseline(self) -> None:
"""Enter a rated TBW as an unverified override (ADR 0012 §7)."""
self.push_screen(BaselineScreen(), callback=self._baseline_entered)
def _baseline_entered(self, entry: "tuple[float, str | None] | None") -> None:
if entry is None:
return
tbw, source_url = entry
payload = json.dumps({
"tbw_terabytes": tbw,
"source_url": source_url,
"document_revision": None,
"entry_date": date.today().isoformat(),
"model_string": None,
"nominal_capacity_bytes": None,
"validated_by": "user",
"verified": False,
})
if self._run_helper("baseline", ["set", payload]):
self.notify("Rated TBW saved as an unverified override.")
def action_disclose(self) -> None:
"""Show disclosures (spec §6.11, CI-4)."""
self.push_screen(DisclosuresScreen())
@@ -2441,15 +2549,20 @@ class FenrisTuiApp(App):
self,
operation: str,
extra_args: Optional[List[str]] = None,
) -> None:
"""Suspend the TUI while the shared control module owns the action."""
) -> bool:
"""Suspend the TUI while the shared control module owns the action.
Returns whether the helper succeeded; failures are reported here.
"""
try:
with self.suspend():
run_monitor(operation, *(extra_args or []), helper_path=self.helper_path)
except MonitorError as exc:
self.notify(str(exc), severity="error")
return False
finally:
self._refresh()
return True
def run_tui(