Declare every dashboard key as a Binding on the widget that owns it, and generate the footer, activity tool chips and help screen from those bindings so they cannot drift. The footer collapses to "? help · q quit" under 80x24. Add Home/End and PgUp/PgDn (one week) navigation, mouse-wheel selection on the graphs, click-to-focus on every panel, and clickable footer chips. Tab and Shift+Tab now visit only the dashboard panels.
362 lines
15 KiB
Markdown
362 lines
15 KiB
Markdown
# Fenris 🐺
|
||
|
||
*Observes an NVMe drive's real-world use and translates that history into an understandable endurance outlook.*
|
||
|
||
Fenris is a persistent TUI monitor backed by a short-lived privileged collector on the host's native scheduler. It reads SMART data every few minutes, stores compact observation history in SQLite, and recomputes a usage-adjusted theoretical lifespan on every screen render — no fairy dust, just your actual bytes.
|
||
|
||
---
|
||
|
||
## Requirements
|
||
|
||
- **Python ≥ 3.10** (verified at install time)
|
||
- **smartmontools** (`smartctl` — verified at install time)
|
||
- **systemd** or **runit**, with a polkit agent (the collector runs as root; elevation is exclusively polkit)
|
||
|
||
No other OS packages or Python dependencies beyond [Textual](https://textual.textualize.io/) (pinned in the lockfile).
|
||
|
||
## Install from package (recommended)
|
||
|
||
### Debian / Ubuntu (apt)
|
||
|
||
The Gitea instance Debian registry signs metadata with its own key. Verify the
|
||
instance key fingerprint (TOFU hardening):
|
||
|
||
```text
|
||
Fingerprint: F937E81D2FB0736B15BC611884BEBD586DFAC010
|
||
```
|
||
|
||
Add the instance key and repository:
|
||
|
||
```bash
|
||
sudo mkdir -p /etc/apt/keyrings
|
||
sudo curl -fsSL -o /etc/apt/keyrings/gitea-xavierk.asc \
|
||
https://git.bongbetic.com/api/packages/xavierk/debian/repository.key
|
||
|
||
echo "deb [signed-by=/etc/apt/keyrings/gitea-xavierk.asc] \
|
||
https://git.bongbetic.com/api/packages/xavierk/debian bookworm main" \
|
||
| sudo tee /etc/apt/sources.list.d/fenris.list
|
||
|
||
sudo apt update && sudo apt install fenris
|
||
```
|
||
|
||
Replace `bookworm` with your distribution codename (`bookworm`, `jammy`, or
|
||
`noble`).
|
||
|
||
### Fedora / openSUSE Tumbleweed (RPM)
|
||
|
||
Use the Fenris-owned repo file (not Gitea's auto-generated one):
|
||
|
||
```bash
|
||
sudo dnf config-manager --add-repo \
|
||
https://git.bongbetic.com/xavierk/Fenris/raw/branch/main/packaging/fenris.repo
|
||
|
||
sudo dnf install fenris
|
||
```
|
||
|
||
On openSUSE Tumbleweed, add the same standard RPM repository file and install
|
||
with zypper:
|
||
|
||
```bash
|
||
sudo zypper addrepo --refresh \
|
||
https://git.bongbetic.com/xavierk/Fenris/raw/branch/main/packaging/fenris.repo fenris
|
||
sudo zypper install fenris
|
||
```
|
||
|
||
The repo file sets `gpgcheck=1` against the Fenris packaging key (downloaded
|
||
from the raw URL in `gpgkey`) and `repo_gpgcheck=0` (metadata check left to
|
||
TLS).
|
||
|
||
### Void Linux (XBPS)
|
||
|
||
Void x86_64 with glibc and runit is the native target. Its signed XBPS channel
|
||
is available from the permanent repository below. Add it, refresh its
|
||
metadata, and install the released package:
|
||
|
||
```bash
|
||
sudo install -d -m 0755 /etc/xbps.d
|
||
echo 'repository=https://git.bongbetic.com/xavierk/Fenris-xbps/raw/branch/stable/x86_64' \
|
||
| sudo tee /etc/xbps.d/fenris.conf
|
||
sudo xbps-install -M -S fenris
|
||
```
|
||
|
||
XBPS requires remote repositories to be signed. On the first refresh it
|
||
displays the repository signing key embedded in the signed metadata; accept it
|
||
only when its RSA SHA256 fingerprint is
|
||
`SHA256:AvPMRlKMikPg75u0iKr8AUkxlfU/Ad4k/S4o2M9W4/w`. The public key is also
|
||
available at
|
||
`https://git.bongbetic.com/xavierk/Fenris-xbps/raw/branch/stable/keys/fenris-xbps-signing.pub`.
|
||
For later updates, always refresh first so XBPS fetches the current index:
|
||
|
||
```bash
|
||
sudo xbps-install -M -Syu
|
||
```
|
||
|
||
The `-M` flag bypasses XBPS's on-disk repodata cache. It is required when
|
||
checking for a newly published package through Gitea's cached raw-file URL.
|
||
|
||
The runit service remains dormant after installation. `fenris monitor resume`
|
||
creates `/var/service/fenris-collect`; pause removes that link and records a
|
||
deliberate disable in the observation history.
|
||
|
||
Fenris keeps the observation store root-written and readable by the `fenris`
|
||
group. Add each TUI user to that group, then start a new login session before
|
||
running Fenris:
|
||
|
||
```bash
|
||
sudo usermod -aG fenris "$USER"
|
||
```
|
||
|
||
### Package signature verification
|
||
|
||
The RPM payload is signed with the Fenris packaging key (RSA 3072).
|
||
Verification happens automatically via dnf's `gpgcheck=1`. For manual
|
||
verification of downloaded assets:
|
||
|
||
```bash
|
||
rpm -Kv fenris-*.x86_64.rpm # RPM payload signature
|
||
gpg --verify SHA256SUMS.asc SHA256SUMS # Clearsigned checksum manifest
|
||
sha256sum -c SHA256SUMS # Checksum match
|
||
```
|
||
|
||
The packaging public key is published in-repo — no keyservers. See
|
||
`packaging/keys/fenris-packaging.asc` and
|
||
`docs/install/signing-key-ceremony.md` for key lifecycle details.
|
||
|
||
### Dormant install
|
||
|
||
A fresh package install is fully dormant. Its native scheduler is present but
|
||
disabled; nothing runs. The only opt-in is the sanctioned toggle:
|
||
|
||
```bash
|
||
fenris monitor resume # enable scheduling + open first monitoring period
|
||
fenris monitor pause # close the period, disable scheduling
|
||
```
|
||
|
||
## Development install (make install)
|
||
|
||
For contributors building from source:
|
||
|
||
```bash
|
||
sudo make install
|
||
```
|
||
|
||
This builds a wheel, installs its locked pure-Python runtime packages into
|
||
`/opt/fenris/vendor`,
|
||
and places helpers, units, and the polkit policy. Units are dormant by default.
|
||
|
||
```bash
|
||
sudo make upgrade # re-sync wheel, units, schema
|
||
make uninstall # removes artifacts, preserves config and store
|
||
make purge # also removes /etc/fenris and /var/lib/fenris
|
||
```
|
||
|
||
## Upgrade
|
||
|
||
### Package upgrade
|
||
|
||
```bash
|
||
sudo apt update && sudo apt upgrade fenris # Debian/Ubuntu
|
||
sudo dnf upgrade fenris # Fedora
|
||
sudo xbps-install -Syu # Void Linux
|
||
```
|
||
|
||
### Development upgrade
|
||
|
||
```bash
|
||
sudo make upgrade
|
||
```
|
||
|
||
What it does:
|
||
1. Snapshots `observations.db` to a one-generation backup (`.bak`).
|
||
2. Replaces the locked runtime packages under `/opt/fenris/vendor`.
|
||
3. Syncs units and polkit against the manifest; runs `daemon-reload`.
|
||
4. Restarts the timer **only** if unit contents changed **and** it is active — a running collection run finishes on its mapped interpreter; the next run uses the new code.
|
||
5. Applies forward-only schema migrations (the store directory is never rebuilt; automatic downgrade does not exist).
|
||
|
||
Rollback: reinstall the previous version and restore `observations.db.bak`.
|
||
On Void, pause monitoring first, copy the compatible snapshot back to
|
||
`/var/lib/fenris/observations.db`, then force-install the matching older
|
||
package version. If that version is no longer indexed, add its retained XBPS
|
||
archive to a local repository with `xbps-rindex -a` and use
|
||
`xbps-install -R <local-repository> -f fenris-<version>`. Installing an older
|
||
package over a newer observation store is unsupported.
|
||
|
||
## Migration from make install
|
||
|
||
If Fenris was previously installed with `sudo make uninstall` first, then
|
||
installed from the package, existing config, store, and group survive by path
|
||
continuity. Over-installing the package over a `make install` is
|
||
**forbidden** — stale units shadow vendor placement. See
|
||
[docs/install/migrate-from-makeinstall.md](docs/install/migrate-from-makeinstall.md).
|
||
|
||
## Uninstall and purge
|
||
|
||
### Package removal
|
||
|
||
```bash
|
||
sudo apt remove fenris # preserves config and store
|
||
sudo apt purge fenris # also removes config and store
|
||
sudo dnf remove fenris # preserves config and store
|
||
sudo xbps-remove fenris # preserves config and store
|
||
```
|
||
|
||
### Development removal
|
||
|
||
```bash
|
||
make uninstall # removes artifacts, preserves config and observation history
|
||
make purge # also removes /etc/fenris and /var/lib/fenris
|
||
```
|
||
|
||
Uninstall performs the sanctioned disable first (`fenris-monitor disable --now`) — an open period closes `user_disabled` — then removes the runtime packages, helpers, units, polkit policy, and wrapper while keeping `/etc/fenris` and the observation store. Reinstalling resumes from the preserved store.
|
||
|
||
## Cadence drop-ins
|
||
|
||
The default collection cadence is **3 minutes** (`OnUnitInactiveSec=3min` in the timer unit). To change it, place a systemd drop-in:
|
||
|
||
```bash
|
||
sudo systemctl edit fenris-collect.timer
|
||
# Add:
|
||
# [Timer]
|
||
# OnUnitInactiveSec=10min
|
||
```
|
||
|
||
No interval key exists in `/etc/fenris/fenris.conf`. Cadence is a systemd concern, not a Fenris configuration key.
|
||
|
||
On Void, Fenris uses its native runit service instead: its initial collection
|
||
is delayed by two minutes and later collections run three minutes after the
|
||
previous run finishes. Inspect its state and diagnostics with:
|
||
|
||
```bash
|
||
sv status fenris-collect
|
||
sudo tail -n 50 /var/log/fenris-collect/current
|
||
```
|
||
|
||
`fenris status` also reports the separate boot-enabled, runtime-active,
|
||
collection outcome, and observation-store freshness facts. A failed collection
|
||
is retried at the next interval; it never fabricates missing observations.
|
||
|
||
## CLI reference
|
||
|
||
| Command | Behavior |
|
||
|---|---|
|
||
| `fenris` | Opens the TUI (no arguments). |
|
||
| `fenris status` | Projection facts, enabled/active state, last collect outcome, journal hint on failure or staleness. Never auto-samples. |
|
||
| `fenris sample` | On-demand collection via the privileged helper. Blocks until the run completes. |
|
||
| `fenris monitor pause` | Sanctioned disable — asks for confirmation, then disables native scheduling and closes the monitoring period. |
|
||
| `fenris monitor resume` | Sanctioned enable — enables native scheduling and opens a monitoring period. No confirmation. |
|
||
| `fenris baseline set <json>` | CLI-side validation, then polkit-guarded persistence. |
|
||
| `fenris baseline clear` | Remove the endurance baseline. |
|
||
| `fenris import <path>` | Idempotent single-transaction legacy import. |
|
||
| `fenris start` / `stop` / `run` | Rejected with a one-line migration pointer — never aliased. |
|
||
| `fenris --device` | Rejected with a pointer to the configuration file. |
|
||
|
||
## Reading the dashboard
|
||
|
||

|
||
|
||
Preview uses synthetic observations, not measurements from a real drive.
|
||
|
||
The Chalktone dashboard opens with a large **Live** activity plot. **Day** shows
|
||
hourly evidence for a selected date, and **History** shows daily evidence. Every
|
||
time is local, labelled with its timezone, and the selected-point readout adds
|
||
a secondary UTC line. Local-day totals keep their recorded timezone. Dotted traces show
|
||
measured read/write volumes, not transfer speed; missing evidence breaks the
|
||
trace. `?` marks a gap, `~` a partial total, and `u` unallocated daily volume.
|
||
Use the selected-point readout for exact values and evidence state.
|
||
|
||
Local-day totals use measured intervals inside each recorded local date. An
|
||
interval that crosses local midnight appears once as shared evidence and stays
|
||
outside both known totals. The dashboard labels current totals as so far and
|
||
shows incomplete or unavailable dates without treating them as zero. Older
|
||
UTC-only summaries cannot establish exact local-day totals.
|
||
|
||
- `v` cycles Live / Day / History; the tabs are also clickable.
|
||
- `←` / `→` inspect points, `Home` / `End` jump to the first or last point, and
|
||
`PgUp` / `PgDn` jump a week in Day and History (the live window is three
|
||
hours); the mouse wheel moves the selection too. `w`
|
||
switches read/write volume in every view.
|
||
- `[` / `]` browse dates, `g` enters a date, and `t` returns to today/live.
|
||
- `Tab` / `Shift+Tab` move focus between panels (clicking a panel focuses it);
|
||
`z` expands the focused panel, and `z` or `Esc` restores it. Monitoring status and controls remain visible.
|
||
- `s` cycles Chalktone, Amber, Nord, and High Contrast; saved theme preferences
|
||
survive upgrades. `m` toggles reduced motion.
|
||
|
||
On smaller terminals, textual summaries and scrollable panels keep evidence
|
||
accessible. Pause, resume, collect, disclosures, help, and quit remain available
|
||
in the fixed control row, which collapses to `? help · q quit` under 80×24. `?`
|
||
lists every key; the footer and help are generated from the same bindings.
|
||
|
||
Run `fenris` as your normal user to open the TUI dashboard. The dashboard does
|
||
not need `sudo`. Use `sudo` for package installation and system configuration;
|
||
pause, resume, and collect-now actions normally authenticate through polkit.
|
||
|
||
If the observation store is inaccessible, add your login user to the `fenris`
|
||
group with `sudo usermod -aG fenris "$USER"`, then log out and back in.
|
||
|
||
If polkit authentication is unavailable, quit the dashboard and run only the
|
||
required administrative action in your terminal:
|
||
|
||
```bash
|
||
sudo fenris monitor resume # Enable monitoring now and across reboots
|
||
sudo fenris monitor pause # Confirm a deliberate monitoring pause
|
||
sudo fenris sample # Request one collection run
|
||
```
|
||
|
||
Reopen the dashboard with `fenris` afterward. Press `?` for these instructions
|
||
and keyboard controls at any time; use the arrow keys to scroll and `Esc` to close.
|
||
|
||
The CLI and TUI share the same authenticated action path. Authentication and
|
||
waiting for collection have no separate dashboard deadline; the native collector
|
||
enforces its 90-second runtime limit. An interrupted or failed action is not
|
||
automatically retried—check `fenris status` before retrying.
|
||
|
||
- **Continuity** — the service strip's continuity line (and `fenris status`) reports whether monitoring survives reboots: `monitoring: active in background · persists across reboots`, or `monitoring: does not start on next boot`.
|
||
- **Paused vs. quit** — a full-width `monitoring: paused — deliberate disable` block means collection is stopped (`fenris monitor pause`); resume with `fenris monitor resume`. Pressing `q` only leaves the screen — monitoring keeps running in the background.
|
||
- **Auth banner** — the launch notice explains normal-user startup and polkit authentication, then clears on the first refresh. The `?` help screen remains available.
|
||
|
||
Per-release notes live on the [releases page](https://git.bongbetic.com/xavierk/Fenris/releases): each entry is the version's `CHANGELOG.md` section — what was added, changed, and fixed — plus standing install and verification instructions.
|
||
|
||
## Retired menu options
|
||
|
||
The legacy `fenris.sh` menu script and the `fenris.py` monolith have been removed. Here's where the old options went:
|
||
|
||
| Legacy option | Successor |
|
||
|---|---|
|
||
| 1) Start monitoring | `fenris monitor resume` |
|
||
| 2) Stop monitoring | `fenris monitor pause` |
|
||
| 3) Status / current wear stats | `fenris status` |
|
||
| 4) Take one sample right now | `fenris sample` |
|
||
| 5) Open dashboard URL | Removed — the HTML dashboard and HTTP server are gone; the TUI is the primary interface. |
|
||
|
||
## Configuration
|
||
|
||
`/etc/fenris/fenris.conf` holds exactly one key — the device selector:
|
||
|
||
```
|
||
device = /dev/disk/by-id/nvme-Samsung_SSD_980_PRO_2TB_S6BENS0Txxxxx
|
||
```
|
||
|
||
Use a stable `/dev/disk/by-id/` path. Raw `/dev/nvmeX` paths are warned against. The file is re-read every collection run.
|
||
|
||
## Where's my stuff?
|
||
|
||
| Artifact | Package install | make install |
|
||
|---|---|---|
|
||
| Wrapper | `/usr/bin/fenris` | `/usr/local/bin/fenris` |
|
||
| Helpers | `/usr/libexec/fenris/` | `/usr/libexec/fenris/` |
|
||
| Units | `/usr/lib/systemd/system/` (vendor) | `/etc/systemd/system/` |
|
||
| Polkit policy | `/usr/share/polkit-1/actions/` | `/usr/share/polkit-1/actions/` |
|
||
| sysusers/tmpfiles | `/usr/lib/{sysusers,tmpfiles}.d/fenris.conf` | managed by Makefile |
|
||
| Configuration | `/etc/fenris/fenris.conf` | `/etc/fenris/fenris.conf` |
|
||
| Observation store | `/var/lib/fenris/observations.db` | `/var/lib/fenris/observations.db` |
|
||
| Runtime packages | `/opt/fenris/vendor` | `/opt/fenris/vendor` |
|
||
| Legacy history | — | `./data/history.jsonl` (auto-imported) |
|
||
|
||
---
|
||
|
||
<p align="center">
|
||
<sub>Fenris 🐺 — by <a href="https://bongbetic.com">Bongbetic</a> · Be kind to your SSD and it'll be kind to you.</sub>
|
||
<br>
|
||
<img src="assets/bongbetic-brand/icon-dark-512.png" width="64" alt="Bongbetic icon">
|
||
</p>
|