feat(tui): clarify monitoring continuity and quitting
This commit is contained in:
+72
-16
@@ -23,7 +23,7 @@ from typing import Any, Dict, List, Optional
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.containers import Container, Horizontal, VerticalScroll
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Static
|
||||
|
||||
@@ -43,6 +43,9 @@ from .status import (
|
||||
format_disclosures,
|
||||
freshness_age_human,
|
||||
grade_freshness,
|
||||
deliberate_pause_lines,
|
||||
is_deliberately_paused,
|
||||
monitoring_continuity,
|
||||
open_store_readonly,
|
||||
query_service_state,
|
||||
read_config,
|
||||
@@ -229,6 +232,7 @@ def _query_service_facts(conn: sqlite3.Connection, clock_now: datetime) -> Dict[
|
||||
"freshness": freshness,
|
||||
"freshness_age_s": None,
|
||||
"period": period_info,
|
||||
"deliberately_paused": is_deliberately_paused(conn, svc),
|
||||
**svc,
|
||||
}
|
||||
|
||||
@@ -292,14 +296,32 @@ class FenrisTuiApp(App):
|
||||
CSS = """
|
||||
#main-grid {
|
||||
layout: grid;
|
||||
grid-size: 2 3;
|
||||
grid-size: 2 4;
|
||||
grid-columns: 3fr 2fr;
|
||||
grid-rows: 8 1fr 7;
|
||||
height: 1fr;
|
||||
grid-rows: 8 10 7 3;
|
||||
height: auto;
|
||||
}
|
||||
#main-grid.paused {
|
||||
grid-size: 2 5;
|
||||
grid-rows: 8 5 10 7 3;
|
||||
}
|
||||
#dashboard-scroll { height: 1fr; }
|
||||
#headline-band { column-span: 2; }
|
||||
#service-strip { column-span: 2; }
|
||||
.pane { border: round #555555; padding: 0 1; }
|
||||
#paused-banner {
|
||||
column-span: 2;
|
||||
display: none;
|
||||
background: $error 20%;
|
||||
color: $text;
|
||||
height: 100%;
|
||||
}
|
||||
#service-strip { column-span: 2; height: 100%; }
|
||||
#quit-rail {
|
||||
column-span: 2;
|
||||
border: heavy $accent;
|
||||
content-align: center middle;
|
||||
height: 100%;
|
||||
}
|
||||
.pane { border: round #555555; padding: 0 1; height: 100%; }
|
||||
#confirm-text { padding: 1 2; }
|
||||
#disc-text { padding: 1 2; }
|
||||
"""
|
||||
@@ -330,11 +352,14 @@ class FenrisTuiApp(App):
|
||||
self._clock_now = datetime.now(timezone.utc)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="main-grid"):
|
||||
yield Static("", id="headline-band", classes="pane")
|
||||
yield Static("", id="usage-history", classes="pane")
|
||||
yield Static("", id="drive-health", classes="pane")
|
||||
yield Static("", id="service-strip", classes="pane")
|
||||
with VerticalScroll(id="dashboard-scroll"):
|
||||
with Container(id="main-grid"):
|
||||
yield Static("", id="headline-band", classes="pane")
|
||||
yield Static("", id="paused-banner")
|
||||
yield Static("", id="usage-history", classes="pane")
|
||||
yield Static("", id="drive-health", classes="pane")
|
||||
yield Static("", id="service-strip", classes="pane")
|
||||
yield Static("q QUIT TUI", id="quit-rail")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Set border titles and render initial state."""
|
||||
@@ -391,6 +416,7 @@ class FenrisTuiApp(App):
|
||||
|
||||
def _render_empty_or_fault(self) -> None:
|
||||
"""Render empty store greeting or store fault."""
|
||||
self._hide_paused_banner()
|
||||
if not self.store_path.exists():
|
||||
# Empty store — greeting with enable hint (IN-3)
|
||||
self._render_headline(
|
||||
@@ -402,7 +428,9 @@ class FenrisTuiApp(App):
|
||||
self.query_one("#service-strip").update(
|
||||
"boot: disabled · timer: inactive · last collect: unknown · freshness: empty · "
|
||||
"[dim]by Bongbetic[/dim]\n"
|
||||
"p pause · r resume · c collect · d disclosures · q quit"
|
||||
"[bold]CONTINUITY[/bold] %s\n"
|
||||
"p pause · r resume · c collect · d disclosures"
|
||||
% monitoring_continuity({"boot_enabled": False})
|
||||
)
|
||||
else:
|
||||
# Store fault (FL-4)
|
||||
@@ -414,7 +442,7 @@ class FenrisTuiApp(App):
|
||||
self.query_one("#drive-health").update("")
|
||||
self.query_one("#service-strip").update(
|
||||
"[dim]by Bongbetic[/dim]\n"
|
||||
"p pause · r resume · c collect · d disclosures · q quit"
|
||||
"p pause · r resume · c collect · d disclosures"
|
||||
)
|
||||
|
||||
def _render_all_regions(self, conn: sqlite3.Connection) -> None:
|
||||
@@ -473,17 +501,45 @@ class FenrisTuiApp(App):
|
||||
self.query_one("#service-strip").update(
|
||||
"boot: %s · timer: %s · last collect: %s · freshness: %s · "
|
||||
"[dim]by Bongbetic[/dim]\n"
|
||||
"[bold]CONTINUITY[/bold] %s\n"
|
||||
"%s\n"
|
||||
"p pause · r resume · c collect · d disclosures · q quit"
|
||||
% (boot, activity, collect, freshness, svc.get("period", ""))
|
||||
"p pause · r resume · c collect · d disclosures"
|
||||
% (
|
||||
boot, activity, collect, freshness,
|
||||
monitoring_continuity(svc), svc.get("period", ""),
|
||||
)
|
||||
)
|
||||
self._render_paused_banner(svc)
|
||||
except Exception:
|
||||
self._hide_paused_banner()
|
||||
self.query_one("#service-strip").update(
|
||||
"boot: unknown · timer: unknown · last collect: unknown · freshness: unknown · "
|
||||
"[dim]by Bongbetic[/dim]\n"
|
||||
"p pause · r resume · c collect · d disclosures · q quit"
|
||||
"p pause · r resume · c collect · d disclosures"
|
||||
)
|
||||
|
||||
def _render_paused_banner(self, service: Dict[str, Any]) -> None:
|
||||
"""Show the high-contrast Deliberate disable block only when sanctioned."""
|
||||
banner = self.query_one("#paused-banner")
|
||||
if service.get("deliberately_paused"):
|
||||
banner.update(
|
||||
"[bold black on red]%s[/bold black on red]\n%s"
|
||||
% tuple(deliberate_pause_lines())
|
||||
)
|
||||
banner.styles.display = "block"
|
||||
main_grid = self.query_one("#main-grid")
|
||||
main_grid.add_class("paused")
|
||||
main_grid.refresh(layout=True)
|
||||
else:
|
||||
self._hide_paused_banner()
|
||||
|
||||
def _hide_paused_banner(self) -> None:
|
||||
"""Ensure an unavailable store cannot retain a stale paused presentation."""
|
||||
self.query_one("#paused-banner").styles.display = "none"
|
||||
main_grid = self.query_one("#main-grid")
|
||||
main_grid.remove_class("paused")
|
||||
main_grid.refresh(layout=True)
|
||||
|
||||
def _format_headline(self, proj: ProjectionResult) -> str:
|
||||
"""Format the lifespan headline (spec §6.11)."""
|
||||
if proj.headline_remaining_seconds is None:
|
||||
|
||||
Reference in New Issue
Block a user