- Change default collection cadence from 5 minutes to 3 minutes (CADENCE_DEFAULT_S=180, systemd OnUnitInactiveSec=3min, runit CADENCE=180) - Update freshness threshold to 450s (2×180 + AccuracySec + 60) - Add _query_live_graph_data(): queries raw samples from the last 3 hours and computes interval byte deltas with actual timestamps - Add LiveActivityGraph widget: vertical bar chart of interval volumes with read/write toggle (w key), arrow key inspection, and click support - Wire live graph into TUI layout (full-width row between daily graph and drive health), refresh cycle, and CSS grid - Replace t theme binding with t today/live binding; theme selection via preferences file - Add w binding for read/write toggle on live graph - Update action legend, help screen, and grid layout for new live-activity row - Add 20 tests covering cadence constants, live query, widget rendering, toggle, and TUI integration - Update all cadence documentation (README, ADR 0003, acceptance criteria LC-2, fenris-redesign constants table, CHANGELOG)
7.3 KiB
3. Service lifecycle: timer-driven collection with a sanctioned control path
Status
Accepted — resolves Define the collector, service, and CLI lifecycle on the Wayfinder map. Amends the toggle mechanism of Verify systemd lifecycle and privilege constraints; its spirit — scoped, explicit, authenticated, no generic manage-unit-files grant — is intact.
Amended by Define endurance-baseline provenance and validation: the helper gains a baseline verb that persists the CLI-validated endurance-baseline row — same fixed-operation, polkit-mediated pattern.
Context
Fenris's current single process combines daemonization, a PID file, an HTTP dashboard, and control (fenris.py start/stop/status/sample) over checkout-relative state. ADR 0001 fixed the observation store, including monitoring_periods whose user_disabled end cause records deliberate pauses, and the systemd lifecycle research fixed the timer + oneshot architecture, standard paths, journal diagnostics, allow-listed status reads, and polkit-mediated startup toggles — while leaving cadence mechanics, the configuration surface, CLI compatibility, staleness thresholds, and the mechanism that records a deliberate disable open. In particular, systemctl enable/disable cannot write a monitoring-period row, so a direct-systemctl toggle cannot satisfy the store's semantics.
Decision
- Units. Two system units only:
fenris-collect.timer(WantedBy=timers.target) andfenris-collect.service(Type=oneshot, root,ExecStart=/usr/libexec/fenris/fenris-collect; no listener, no UI code). The TUI and CLI are ordinary unprivileged processes and never units. There is no/run/fenriscoordination surface: systemd serializes runs, the observation store holds state, and failures go to the journal per ADR 0001. - Cadence. Default three minutes:
OnBootSec=2min,OnUnitInactiveSec=3min(measured from run completion; drift accepted because hours are the evidence grain),AccuracySec=30s,Persistent=no, no suspend catch-up (absent hours classify through power-on-hours evidence),TimeoutStartSec=90sso a hung interrogation fails visibly. Cadence changes are documented drop-ins on the timer unit (systemctl edit+ daemon-reload); no interval key exists in configuration. - Configuration.
/etc/fenris/fenris.confholds exactly one key: the device selector, a stable/dev/disk/by-id/…path (raw nodes accepted with an instability warning), validated at collection time. The oneshot re-reads it every run, so there is no reload path to design. An invalid selector is a bounded failed run — journal plus failed unit result, retried next interval;statusand the TUI also read the world-readable file directly and surface aconfiguration error: <reason>fact. - Entry points. Two privileged binaries:
/usr/libexec/fenris/fenris-collect(device interrogation and store writes; the unit'sExecStart) and/usr/libexec/fenris/fenris-monitor(fixed operationsenableanddisablewith optional--now, plus the collect trigger, monitoring-period bookkeeping, andbaseline set/baseline clearpersistence for the CLI-validated endurance baseline; the only binary the polkit policy authorizes). One unprivilegedfenrisfor humans: no arguments opens the TUI; subcommands (status,sample,monitor pause,monitor resume) are the CLI. - Sanctioned toggle. Pause =
disable --now; Resume =enable --now; both executed byfenris-monitor, which performs the systemctl operation and the monitoring-period bookkeeping in one step, under polkit actioncom.bongbetic.fenris.monitor(auth_admin, covering the collect trigger too). Root invokes the helpers directly; where no polkit agent exists the operation fails cleanly and prints the root equivalent. This amends the research's direct-systemctl toggle: a period boundary cannot be recorded by systemctl, so the toggle must be Fenris's own fixed operation. - Period rows. Idempotent matrix: a first-ever enable opens a period at the enable moment (hours before the first successful sample are unknown-but-inside, correctly so when the device errors); a resume with an open period — a raw
systemctl stopintervened — changes no row, the gap remaining inside as unknown seconds; a resume with no open period opens a new row at the resume moment; a pause with an open period closes ituser_disabledat the pause moment; a pause otherwise is a no-op. A raw stop or disable outside the helper is an unexplained gap, neveruser_disabled: only the sanctioned path can record intent. - On-demand collection.
fenris sampleand the TUI's collect-now route throughfenris-monitor→systemctl start fenris-collect.service, which blocks until the oneshot exits, and the outcome (freshness line or journal hint) is reported synchronously. No code path outsidefenris-collecttouches the device; the TUI never samples in-process; no confirmation is required. - TUI controls. Pause asks for confirmation; Resume does not (benign — friction invites raw-systemctl escapes). Boot enablement and current runtime activity are always displayed as separate facts, next to last collect outcome and freshness. No bare start/stop exists anywhere.
- CLI compatibility.
statusis a pure read-only composition of the observation store and allow-listedsystemctl showproperties: projection facts, enabled/active, last collect outcome, and ajournalctl -u fenris-collect.servicehint on failure or staleness; it never auto-samples and never prompts.sampleis retained via the helper path;--deviceis rejected with a pointer to the configuration file.start,stop, andrunare rejected with one-line migration pointers, not aliased — an alias would silently change meaning.fenris.shis retired: not shipped, removed from the repository, and the README maps its five menu options to their successors. - Freshness constants. Documented once, consumed by TUI and CLI alike: fresh means the newest sample is within 2× cadence +
AccuracySec+ 60 s; between that and 48 h the store is missed (a contributing fact); at ≥ 48 h it is stale, matching ADR 0002's evidence gate; an empty store reads "no observations yet" with an enable hint.
Consequences
- Polkit ships one Fenris-specific policy authorizing exactly one fixed-operation binary; the collector itself is never polkit-reachable.
- Monitoring-period boundaries are exact at toggle moments; approximation never enters the habit record.
- Interval tuning is a systemd drop-in documented in the README;
/etc/fenrisstays a one-key file. - Headless administration has full parity: every TUI action has a CLI twin.
- The TUI must run privileged operations through a terminal-attached subprocess so the platform polkit agent can prompt; the TUI prototype ticket validates this in practice.
- Nothing survives of the prototype's daemonization, PID files, or HTTP server; their commands fail with pointers instead of quiet behavior changes.