Files
Fenris/docs/adr/0003-service-lifecycle-and-sanctioned-toggle.md
T
xavierk e12f4a574c Plot live drive activity every three minutes (#91)
- 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)
2026-09-18 13:17:18 +05:30

7.3 KiB
Raw Blame History

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

  1. Units. Two system units only: fenris-collect.timer (WantedBy=timers.target) and fenris-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/fenris coordination surface: systemd serializes runs, the observation store holds state, and failures go to the journal per ADR 0001.
  2. 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=90s so 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.
  3. Configuration. /etc/fenris/fenris.conf holds 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; status and the TUI also read the world-readable file directly and surface a configuration error: <reason> fact.
  4. Entry points. Two privileged binaries: /usr/libexec/fenris/fenris-collect (device interrogation and store writes; the unit's ExecStart) and /usr/libexec/fenris/fenris-monitor (fixed operations enable and disable with optional --now, plus the collect trigger, monitoring-period bookkeeping, and baseline set/baseline clear persistence for the CLI-validated endurance baseline; the only binary the polkit policy authorizes). One unprivileged fenris for humans: no arguments opens the TUI; subcommands (status, sample, monitor pause, monitor resume) are the CLI.
  5. Sanctioned toggle. Pause = disable --now; Resume = enable --now; both executed by fenris-monitor, which performs the systemctl operation and the monitoring-period bookkeeping in one step, under polkit action com.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.
  6. 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 stop intervened — 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 it user_disabled at the pause moment; a pause otherwise is a no-op. A raw stop or disable outside the helper is an unexplained gap, never user_disabled: only the sanctioned path can record intent.
  7. On-demand collection. fenris sample and the TUI's collect-now route through fenris-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 outside fenris-collect touches the device; the TUI never samples in-process; no confirmation is required.
  8. 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.
  9. CLI compatibility. status is a pure read-only composition of the observation store and allow-listed systemctl show properties: projection facts, enabled/active, last collect outcome, and a journalctl -u fenris-collect.service hint on failure or staleness; it never auto-samples and never prompts. sample is retained via the helper path; --device is rejected with a pointer to the configuration file. start, stop, and run are rejected with one-line migration pointers, not aliased — an alias would silently change meaning. fenris.sh is retired: not shipped, removed from the repository, and the README maps its five menu options to their successors.
  10. 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/fenris stays 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.