docs: quirky README with Bongbetic branding, drop internal plan from remote

- Rewrite README in a playful, plain-English voice and add Bongbetic
  wordmarks/glyph/icons so Gitea renders the brand nicely (light/dark).
- Vendor assets/bongbetic-brand/* from /mnt/Toto/Documents/bongbetic/Logo.
- Remove plan-dash-changes.md from tracking (internal only) and gitignore it.
This commit is contained in:
xavierk
2026-08-23 18:52:57 +05:30
parent 53cc6c81b9
commit 47e1eaaf12
10 changed files with 149 additions and 310 deletions
+1
View File
@@ -5,3 +5,4 @@ data/fenris.pid
data/fenris.log data/fenris.log
data/history.jsonl data/history.jsonl
data/hourly.jsonl data/hourly.jsonl
plan-dash-changes.md
+108 -91
View File
@@ -1,89 +1,79 @@
# Fenris — NVMe Wear Monitor & Live Dashboard <p align="center">
<picture>
<source srcset="assets/bongbetic-brand/wordmark-light.png" media="(prefers-color-scheme: dark)">
<img src="assets/bongbetic-brand/wordmark-dark.png" alt="Bongbetic" width="260">
</picture>
<br>
<sub>crafted with stubborn curiosity by <a href="https://bongbetic.com">Bongbetic</a></sub>
</p>
Created by Bongbetic. <p align="center">
<img src="assets/bongbetic-brand/b_glyph.svg" width="48" alt="Fenris glyph">
</p>
Periodically reads your NVMe drive's SMART health data, logs it over time, and serves a self-contained HTML dashboard estimating SSD lifespan from your actual daily usage trend. <h1 align="center">Fenris 🐺 — Your SSD's Tell-All Diary</h1>
## Requirements <p align="center">
<em>Your NVMe drive has been keeping secrets. Fenris makes it confess — in real time.</em>
<br>
<em>How much did you write today? How long until it taps out? No fairy dust — just your actual bytes.</em>
</p>
---
Fenris is a tiny, stubborn daemon that eavesdrops on your NVMe drive's SMART gossip, writes it down every few minutes, and serves you a live dashboard that actually means something. Not "vibes" — **real GB written in the last 24 hours, real GB/hour, and a real countdown in hours, days, and years until your drive's endurance runs out**.
> Think of it as a Fitbit for your SSD. Except it doesn't nag you to drink water.
## What it actually does (no hand-waving)
- **Listens** — polls `smartctl -j` on your NVMe device (default every 5 minutes, you pick).
- **Remembers** — appends every sample to `data/history.jsonl` and rolls up per-hour totals into `data/hourly.jsonl` (survives restarts, rebuilds itself if you yank the power).
- **Calculates** — rolling 24-hour window: *exact* bytes written in the last 24h, GB/h, GB/day, implied total TBW from `percentage_used`, remaining TB, and a projected life-remaining breakdown. Warming-up badge until it has 24h of coverage — no fake confidence.
- **Shows off** — dense, live dashboard with wear-over-time + trailing-24h per-hour bars, sticky header, live countdown, and stale warnings if the daemon dozes off.
## You need
- **Python 3.7+** - **Python 3.7+**
- **smartmontools** (`smartctl`) installed - **smartmontools** (`smartctl`)
- Root access to read NVMe SMART logs - Root-ish access to read NVMe SMART (passwordless `smartctl` or just run with `sudo` — your call)
### Setting up passwordless smartctl ### The sudo dance (one time)
Fenris runs `sudo -n smartctl ...` (no-prompt sudo). Either run with sudo or allow passwordless access: Fenris runs `sudo -n smartctl ...` so it doesn't get stuck asking for a password mid-nap:
```bash ```bash
sudo visudo sudo visudo
# Add this line (replace youruser with your username): # add this line (swap in your username):
youruser ALL=(root) NOPASSWD: /usr/sbin/smartctl youruser ALL=(root) NOPASSWD: /usr/sbin/smartctl
``` ```
## Quick Start No sudo? Run the whole thing with `sudo` and it'll still behave.
### Interactive Menu ## Get it running — 30 seconds
### The cozy way
```bash ```bash
./fenris.sh ./fenris.sh
# pick 1) Start monitoring → choose device / interval / port → done
``` ```
### CLI ### The no-nonsense way
```bash ```bash
# Start daemon + dashboard in background python3 fenris.py start # defaults: /dev/nvme0, every 300s, port 8420
python3 fenris.py start python3 fenris.py start --interval 60 --port 9000 # if you're impatient
python3 fenris.py status # "are we live? how's the drive?"
# Check status and latest wear stats python3 fenris.py sample # one sneaky sample right now
python3 fenris.py status python3 fenris.py stop # tuck it back in
# Take one sample now
python3 fenris.py sample
# Stop the daemon
python3 fenris.py stop
``` ```
Dashboard available at: `http://localhost:8420` Dashboard lives at **http://localhost:8420** (or whatever port you chose).
## CLI Reference ## The menu, demystified
### fenris.py start Run `./fenris.sh` and you'll get:
Start monitoring in background (daemon + dashboard).
```bash
python3 fenris.py start [OPTIONS]
Options:
--device PATH NVMe device (default: auto-detect, e.g. /dev/nvme0)
--interval SEC Seconds between samples (default: 300)
--port PORT Dashboard HTTP port (default: 8420)
```
### fenris.py stop
Stop background monitoring and clean up PID file.
### fenris.py status
Show daemon status and latest wear statistics.
### fenris.py sample
Take one sample immediately and print it.
```bash
python3 fenris.py sample [--device /dev/nvme0]
```
### fenris.py run
Run in foreground (used internally by `start`). Not intended for direct use.
## Interactive Menu
Run `./fenris.sh` for a guided interface:
``` ```
1) Start monitoring (background daemon + dashboard) 1) Start monitoring (background daemon + dashboard)
@@ -93,53 +83,80 @@ Run `./fenris.sh` for a guided interface:
5) Open dashboard URL 5) Open dashboard URL
--- ---
h) Help / how this works h) Help / how this works
q) Exit q) Exit (go touch grass)
``` ```
## Data Collected ## What Fenris jots down
| Field | Description | | Field | What's the gossip? |
|-------|-------------| |-------|---------------------|
| `percentage_used` | SSD's own wear indicator (0-100%) | | `percentage_used` | The drive's own wear-o-meter (0–100%) |
| `bytes_written` / `bytes_read` | Total data written/read | | `bytes_written` / `bytes_read` | Lifetime totals — the receipts |
| `available_spare` | Remaining spare capacity (%) | | `available_spare` | Spare blocks left (%) |
| `media_errors` | Number of uncorrectable errors | | `media_errors` | Uncorrectable boo-boos |
| `power_on_hours` | Total power-on hours | | `power_on_hours` | How long it's been awake |
| `temperature_c` | Current temperature | | `temperature_c` | Is it sweating? |
| `critical_warning` | NVMe critical warning flags | | `critical_warning` | NVMe's panic flags |
## Dashboard Features Hourly rollups also stash `bytes_written` per hour, `pct_start`/`pct_end`, and temp peaks — so the 24h math stays honest.
- Real-time wear level + projected life remaining in **hours / days / years** from the actual **rolling-24h write rate** and implied TBW endurance ## The dashboard — what's on screen
- Exact **GB written in the last 24 hours** + GB/h and GB/day rate (updates every poll interval)
- Wear-over-time chart + **trailing-24h per-hour write bars**
- Dense layout with sticky header, ETag-cached polling synced to the daemon interval, countdown and live badge, stale/preliminary banners
- API: `GET /api/data`, `/api/hourly`, `/api/summary`, `/api/config`, `/api/status`
## File Structure - **Hero card: Projected life remaining** — big, friendly `361 d 2 h` (plus `≈ 361 days · ≈ 8666 hours · ≈ 0.99 years`), backed by `~280 GB/day` and `~101 TB left of ~202 TB total` on the test box.
- **Data written (24h)** — exact GB in the rolling window + coverage (`10.4h of 24h` until warmed up).
- **Write rate** — GB/h and GB/day, live.
- **Wear, spare, temp, errors, power-on** — the usual suspects, with progress bars and polite color-coding.
- **Two charts, side by side:** wear over time + trailing-24h hourly write bars (with a cheeky "now" bar for the current partial hour).
- **Live plumbing:** polling synced to your interval, ETag-cached, countdown to next sample, warming-up + stale banners, pauses when you hide the tab (saves your battery, you're welcome).
**API for the tinkerers:** `GET /api/data` · `/api/hourly` · `/api/summary` · `/api/config` · `/api/status` — all JSON, all friendly.
## Where's my stuff?
``` ```
fenris/ fenris/
├── fenris.py # Main Python script ├── fenris.py # the whole show — daemon + server + math
├── fenris.sh # Interactive menu wrapper ├── fenris.sh # the cozy menu
├── README.md ├── README.md # hi — you're here
├── assets/bongbetic-brand/ # Bongbetic wordmarks & glyphs (for Gitea + dashboard)
└── data/ └── data/
├── history.jsonl # Raw sample log (JSONL) ├── history.jsonl # raw samples (JSONL, append-only)
├── hourly.jsonl # Per-hour aggregates (rebuilt from history on restart) ├── hourly.jsonl # per-hour rollups (auto-rebuilt on restart)
├── fenris.pid # Daemon PID file ├── fenris.pid # daemon PID
└── fenris.log # Daemon log output └── fenris.log # daemon chatter
``` ```
## Troubleshooting ## CLI cheat sheet
```bash
python3 fenris.py start [--device /dev/nvme0] [--interval 300] [--port 8420]
python3 fenris.py stop
python3 fenris.py status
python3 fenris.py sample [--device /dev/nvme0]
python3 fenris.py run # foreground mode — what `start` spawns internally
```
## Oops — troubleshooting without the tears
**"smartctl not found"** **"smartctl not found"**
```bash ```bash
sudo apt install smartmontools # Debian/Ubuntu sudo apt install smartmontools # Debian/Ubuntu
sudo pacman -S smartmontools # Arch sudo pacman -S smartmontools # Arch — you already knew
``` ```
**"needs root" / permission denied** **"needs root" / permission denied**
Set up passwordless sudo (see Requirements) or run with sudo. Set up the passwordless line above, or just `sudo ./fenris.sh`.
**Dashboard shows "stale"** **Dashboard says "stale"**
Daemon not running. Check with `python3 fenris.py status` and restart if needed. Daemon napped or crashed. `python3 fenris.py status` will tell you. Kick it again with `start`.
**Only 10 hours of data and it says "preliminary"?**
That's honesty, not a bug. It needs 24h of real writes to give a tight estimate. Let it simmer — the number gets sharper every hour.
---
<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>
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="383.692000pt" height="497.251424pt" viewBox="0 0 383.692000 497.251424"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.16, written by Peter Selinger 2001-2019
</metadata>
<g transform="translate(2.765143,475.709540) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M805 4456 c-17 -13 -60 -15 -267 -16 l-248 0 -15 -24 c-14 -21 -16
-60 -14 -270 1 -163 6 -251 13 -262 10 -12 52 -16 258 -20 292 -6 293 -6 283
66 -9 62 1 67 120 64 50 -1 117 -2 151 -3 107 -2 96 119 95 -1067 -2 -998 -1
-1045 18 -1150 22 -124 38 -174 77 -241 55 -95 160 -217 212 -247 11 -6 44
-25 74 -43 77 -46 219 -83 317 -83 95 0 243 35 310 73 65 37 185 149 226 212
162 244 128 577 -82 790 -99 100 -229 157 -362 159 -67 1 -75 -1 -92 -24 -17
-23 -54 -172 -66 -266 -11 -84 32 -144 102 -144 38 0 100 -35 130 -74 57 -74
56 -163 -2 -227 -70 -77 -187 -84 -288 -17 -55 37 -87 81 -115 162 -19 56 -20
83 -20 471 0 332 3 415 14 430 12 17 31 18 309 17 265 -2 305 -4 379 -23 176
-44 411 -202 498 -333 14 -21 37 -52 51 -69 41 -51 114 -195 143 -282 91 -280
68 -628 -57 -864 -89 -167 -237 -343 -350 -415 -92 -59 -220 -126 -256 -136
-20 -5 -55 -16 -78 -24 -122 -45 -380 -60 -545 -31 -306 54 -544 214 -721 484
-68 102 -90 147 -121 240 -21 63 -42 144 -47 180 -4 36 -9 394 -9 794 -1 707
-2 729 -21 748 -17 17 -34 19 -212 19 -175 0 -196 -2 -210 -18 -16 -17 -17
-84 -17 -756 0 -697 1 -744 20 -859 12 -67 37 -169 57 -227 72 -212 256 -521
363 -610 24 -19 70 -60 102 -90 120 -110 283 -206 502 -294 193 -78 608 -108
831 -60 39 8 91 19 117 24 61 12 243 85 275 110 14 11 33 20 43 20 9 0 35 13
57 29 22 16 53 36 69 45 58 33 133 93 237 189 86 80 123 125 204 245 206 306
274 537 274 927 0 186 -3 214 -42 375 -39 161 -142 376 -243 508 -39 51 -84
110 -99 130 -45 59 -155 160 -239 219 -42 30 -80 63 -83 74 -5 14 8 36 49 83
71 81 110 153 143 268 24 84 25 101 21 227 -6 158 -20 211 -84 337 -57 110
-116 193 -186 261 -62 60 -233 176 -284 193 -20 7 -48 18 -62 26 -15 8 -34 14
-43 14 -9 0 -36 7 -60 15 -129 46 -152 47 -834 52 -576 4 -652 3 -670 -11z
m1445 -484 c114 -41 154 -65 225 -136 93 -91 117 -143 119 -251 2 -121 -28
-188 -121 -274 -61 -57 -82 -70 -150 -91 -68 -22 -96 -25 -223 -25 -126 0
-155 3 -219 24 -48 16 -90 38 -120 64 -54 46 -116 133 -132 185 -6 20 -15 138
-20 262 -7 197 -6 228 8 249 l15 24 282 -6 c246 -5 288 -8 336 -25z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

-219
View File
@@ -1,219 +0,0 @@
# Plan: Fenris Live Dashboard — Dynamic Polling + Bongbetic Brand + shadcn/ui
> Status: PLAN ONLY — no code touched. **Option A approved.** Data-management removed; cumulative write chart X = hours-in-day (0–24); dense layout; hourly diagnostics + real-time forecast model.
## 1. Context & Goals
**User request (consolidated):**
- Dashboard live/dynamic, updating at each poll interval without refresh — **Option A (zero-build).**
- Bongbetic branding + logo everywhere (`~/Documents/bongbetic/Logo`).
- shadcn/ui styling (CSS parity, no React build).
- **Remove data-management feature** (backup/restore/backups/repair/purge).
- **Cumulative data-written graph X-axis = hours in a day (0–24).**
- **Graphs smaller, side-by-side, flexible.** **Cards tighter/dense.**
- **Estimated life remaining = real-time, based on live hourly GB usage.** After 24h of run, app must keep recording hourly, store diagnostics, log GB/hour, and forecast remaining days from current hourly rate.
**Current state (verified 2026-08-19):**
- `fenris.py` single `DASHBOARD_HTML` via `ThreadingHTTPServer`. Hand-rolled dark CSS, 2× `<canvas>` stacked vertically (`height=220`, full-width), `setInterval(render,30000)` hardcoded, full `innerHTML` replace.
- `GET /api/data` → full `history.jsonl`; no `/api/config`; interval not exposed.
- Forecast: `linearForecast()` on `percentage_used` trend (simple linear regression, whole history). No hourly bucket, no GB/hour model, no diagnostics persistence beyond raw history.
- Cards: `minmax(220px,1fr)` gap 1rem, padding `1rem 1.2rem`, value `1.7rem` — loose.
- No asset vendoring, no Tailwind/shadcn.
**Goals:**
1. Interval-synced live refresh (no reload), ETag/diff, pulse.
2. Bongbetic header/logo/footer + shadcn tokens.
3. Dense cards + two graphs side-by-side, responsive, reduced height.
4. After 24h: hourly diagnostics log (`GB/hour`), real-time days-remaining forecast from live hourly write rate (not just wear %).
## 2. Non-Goals & Explicit Removals
- No auth/multi-tenant, no SSE/WebSocket v1, no Vite/React (Option B rejected).
- **Data-management removed:** delete `BACKUP_DIR`, 5 commands, parsers, `data/backups/`, `fenris.sh` items 6–10. Data = append-only `data/history.jsonl` (+ new `data/hourly.jsonl` per §5.2); orphan `backups/` logs “safe to delete”.
## 3. Logo / Brand Audit
```
bongbetic-logo-dark.svg (751×220, #F7F1E7 currentColor) — dark bg
bongbetic-symbol-dark.svg (192×192) — favicon/header mark
bongbetic-brand/favicon-32.png, icon-dark-512.png, site.webmanifest, b_glyph.svg
```
Vendor into `assets/` and serve via `Handler` static branch; inline SVG so `currentColor` follows `text-foreground`.
## 4. Target Architecture — Option A (Approved)
Zero-build: Tailwind CDN + shadcn CSS variables (Slate/Zinc dark). Semantic HTML + Tailwind mimics `Card/Badge/Progress/Alert/Skeleton`. Canvas charts wrapped in `Card`; wear + write side-by-side flex grid (see §6.1). Python exposes `/api/config`, `/api/status`, `/api/hourly`, static `/assets/*`.
## 5. Live / Dynamic Behavior
### 5.0 Polling (unchanged from prior plan)
- `GET /api/config → {interval, device, port, version}` → `POLL_MS = interval*1000` (clamp 5s–3600s, 30s fallback).
- Diff: hash `last_ts+length`; skip render if unchanged; else patch cards (textContent morph + `ring-2` pulse), append chart point via `requestAnimationFrame`.
- `visibilitychange` pause/resume, `navigator.onLine` backoff 1/2/4…60s, header badge “Live • every 5m • next in 03:42”.
- `GET /api/data` gains `ETag` (mtime+length) + 304.
### 5.1 Cumulative Write Chart — Hours-in-Day (Requirement)
- **Intraday 0–24h view:** `hours = h + m/60 + s/3600` (0–23.99). `todayRows = rows.filter(same DateString)` → `[hours, (bytes_written - midnightBaseline)/1e9]`.
- Axis: `minX=0, maxX=24`, ticks `00:00/06:00/12:00/18:00/24:00` via `opts.xIsHoursInDay`. Title “Cumulative written today (GB) — hours of day”. Empty → “No samples today yet”. Midnight → reset to 0 GB, caption “Resets at midnight — today only”.
- Wear chart stays absolute-time (weeks trend). Both canvases slimmed (see §6.1).
### 5.2 Real-Time Hourly Diagnostics & Forecast Model (New — Core Requirement)
**Problem with current forecast:** `linearForecast(points)` regresses `percentage_used` over full history; single slope, insensitive to bursty hourly writes, no GB/hour visibility.
**Required model:**
- Real-time on every poll, not daily batch.
- After 24h wall time since first sample, switch to **hourly GB/hour regime**; before 24h, show warming-up estimate.
- Persist per-hour diagnostics and GB/hour log.
**Design:**
1. **Raw source stays `data/history.jsonl`** (poll interval samples, e.g., every 300s).
2. **New derived log `data/hourly.jsonl`** (append-only, one record per wall hour):
```json
{"hour":"2026-08-19T14:00:00Z","samples":12,"gb_written":4.21,"gb_read":1.03,
"pct_start":1.2,"pct_end":1.21,"pct_delta":0.01,
"temp_avg":42.1,"temp_max":48,"spare":98,"media_errors":0}
```
Fields: hour bucket start (UTC), count, deltas from first/last sample in hour, averages. Written by `collector_loop` helper `flush_hourly()`.
3. **Hourly rollup logic (Python):**
- In-memory `current_hour_bucket`; on each `sample()`, accumulate `bytes_written` delta vs bucket start.
- On hour boundary (or every 60min since daemon start if clock not trusted), `append_hourly(rec);` also `append_sample(raw)`.
- On daemon (re)start, rebuild missing hours by scanning `history.jsonl` and aggregating by `hour = ts truncated to hour` (idempotent — dedupe by hour string).
- `/api/hourly` returns parsed `hourly.jsonl` (array, sorted). Add ETag similarly.
4. **Forecast — two complementary signals, UI shows primary “days remaining (hourly write model)” + secondary wear model:**
- **GB/hour → TBW model (primary, per user ask “present hourly usage → days”):**
```
hourly_avg = mean(last 24 hourly gb_written) // rolling 24, or EWMA α=0.3 if <24h
// derive endurance from vendor wear if available:
if pct_used>0: endurance_TB = bytes_written / (pct_used/100) // total TBW implied
else: endurance_TB = capacity_bytes * 600 // fallback: ~600× capacity (conservative), or mark unknown
remaining_TB = max(endurance_TB - bytes_written/1e12, 0)
days_remaining = remaining_TB / (hourly_avg *24) // hourly_avg in TB
```
If endurance derivable, show; else show wear-based only and badge “TBW unknown — using wear rate”.
- **Wear/hour model (secondary, cross-check):**
```
wear_per_hour = mean(last 24 pct_delta per hour)
hours_to_100 = (100 - pct_now) / wear_per_hour
days_wear = hours_to_100/24
```
Shown as tooltip / small “also ~X days at current wear rate”.
- **<24h warming up:** `hourly_avg` over available hours (n<24); badge “Warming up — Xh to confident forecast (now ~Y days, n=Nh)”. `days_remaining` still computed but flagged `preliminary`.
- **Real-time update:** every poll, frontend refetches `/api/data` + `/api/hourly`, recomputes hourly_avg client-side too (so UI reflects instantly even before next hourly flush); backend hourly file ensures persistence across restarts.
5. **Storage & retention:** `hourly.jsonl` append 24 records/day → ~9k/year, trivial. Keep forever; same manual-truncate philosophy (no purge command). Document `jq` one-liner to trim.
6. **UI integration:** new card “Est. days remaining (live hourly)” with large `N days` + sub `3.2 GB/hour avg (24h) • 1.1 TB remaining • updates each 5m`; secondary line wear model. New mini sparkline/bar inside card showing last 24h `gb_written` per hour (or when `<24h`, show available). Wear chart tooltip cross-links.
**API additions:**
```python
GET /api/config → {interval, device, port, version}
GET /api/status → {alive, samples, last_ts, pid, uptime_hours, hourly_samples}
GET /api/hourly → [hourlyRec, ...] # ETag + no-store
GET /api/data → unchanged + ETag
# no backup/restore
```
## 6. Layout & shadcn Mapping (Dense + Side-by-Side)
### 6.1 Dense Cards + Flexible Graphs
- **Cards:** tighter — Tailwind: `grid gap-3` (was 1rem), `grid-cols-2 md:grid-cols-3 xl:grid-cols-5`, card `p-3` (was 1rem 1.2rem), `rounded-lg` (was 10px), label `text-[0.70rem] tracking-wide`, value `text-xl font-semibold` (was 1.7rem), sub `text-xs`. Row height uniform via `min-h-[96px]`.
- **Graphs:** `section.grid.grid-cols-1.lg:grid-cols-2.gap-4` — two `Card`s side-by-side on ≥1024px, stacked below. Each Card: `p-4`, header `CardTitle` 0.9rem, canvas `h-[200px] lg:h-[220px] w-full` (down from 220 full-width), `flex-1 min-w-0` so canvases shrink. `drawLine` canvas `width=clientWidth, height=200` → responsive. No fixed page width; container `max-w-[1400px] mx-auto px-4`.
- **Flex guarantees:** `canvas { width:100%; height:100%; display:block }`, `chart-wrap { flex:1 min-w-0 }`, `canvas` DPR scaled but CSS size flexes. ResizeObserver re-draw on container resize.
- **Overall page:** less vertical scroll — header sticky + dense cards + two graphs in one row + footer.
| Current | shadcn | Tight classes |
|---|---|---|
| `.grid .card` | `Card` | `rounded-lg border bg-card p-3 shadow-sm gap-3` |
| value | `CardTitle` numeric | `text-xl font-semibold tabular-nums` |
| wear % | `Progress` | `h-1.5 bg-primary` |
| Spare/errors | `Badge` + `Alert` | `text-xs px-1.5 py-0` |
| charts | `Card` flex | `h-[200px] lg:h-[220px] p-4` |
| footer | muted | `text-xs text-muted-foreground` |
## 7. Branding Spec
Header sticky `[symbol 28px | wordmark] Fenris — NVMe Wear Monitor` left, `[Live ● next 03:42] [every 5m]` right, `bg-background/80 backdrop-blur`. Favicon `favicon-32.png`, `icon-dark-512`. Footer “© Bongbetic — Fenris · interval 300s · v0.1” + `b_glyph.svg` 16px + link to diagnostics count. No `data/backups`.
## 8. File / Code Changes (Option A)
**`fenris.py`:**
- Delete data-mgmt: `BACKUP_DIR`, 5 cmds, parsers; keep `start/stop/status/run/sample`.
- Add globals `_interval`, `_device`, `_port`; vendored `assets/` static branch.
- New `data/hourly.jsonl` + helpers `append_hourly()`, `load_hourly()`, `flush_hourly()`, `rebuild_hourly_from_history()`.
- `collector_loop`: on each sample also `update_hour_bucket`; hourly flush.
- Endpoints: `/api/config`, `/api/status`, `/api/hourly`, ETag on `/api/data`+`/api/hourly`, static `/assets/*`.
- Replace `DASHBOARD_HTML`: Tailwind CDN + shadcn vars, sticky header logos, dense grid, side-by-side Cards (200px canvases, flex), hours-in-day write chart (§5.1), live hourly forecast card (§5.2) with `hourly_avg` + `days_remaining` + 24h bar sparkline, midnight caption, footer glyph.
**`fenris.sh`:** remove items 6–10 branches, renumber Help/Exit, update `help_text`.
**`README.md`:** drop Data Management; add “Hourly diagnostics `data/hourly.jsonl` (GB/hour) + live days-remaining forecast after 24h” + `jq` truncate note.
**Assets:**
```bash
mkdir -p assets
cp ~/Documents/bongbetic/Logo/bongbetic-logo-dark.svg assets/
cp ~/Documents/bongbetic/Logo/bongbetic-symbol-dark.svg assets/
cp ~/Documents/bongbetic/Logo/bongbetic-brand/favicon-32.png assets/favicon.ico
cp ~/Documents/bongbetic/Logo/bongbetic-brand/icon-dark-512.png assets/
```
Keep `collector_loop` otherwise unchanged; no Vite.
## 9. Implementation Phases (Option A)
**Phase 0 — Approved** (this plan): Option A + removals + hours-in-day + dense layout + hourly model.
**Phase 1 — Deletion + API + hourly store:** delete data-mgmt, add config/status/hourly endpoints, hourly rollup helpers + rebuild, verify `curl /api/hourly | jq`.
**Phase 2 — Shell/layout:** dense cards, side-by-side flex graphs (200px), Tailwind CDN + shadcn vars, header/footer logos.
**Phase 3 — Live JS + charts:** interval-synced poll, diff patch, writeChart 0–24h (§5.1), wear chart, visibility/backoff, ETag 304.
**Phase 4 — Hourly forecast live:** `hourly_avg` rolling 24h, TBW-derived `days_remaining` + wear cross-check, warming-up badge <24h, per-hour GB bar in forecast card, midnight reset; wire `GET /api/hourly`.
**Phase 5 — Polish:** `Progress/Badge` variants, `Skeleton`, temp alert, responsive, `prefers-color-scheme`, ResizeObserver.
## 10. Risks
| Risk | Mitigation |
|---|---|
| Tailwind CDN offline | Vendor `assets/tailwind.css` offline fallback. |
| Logo currentColor | Inline SVG, test Chrome/Firefox. |
| Interval desync | Python global single source. |
| Flicker | `requestAnimationFrame` + keep rows. |
| History large | `hourly.jsonl` is compact; keep `/api/data` full v1, add `?since=` v2. |
| Data-mgmt removal confusion | `--help` no backup/restore; log orphan `backups/`. |
| Midnight reset | Caption “Resets at midnight — today only”. |
| <24h forecast noisy | Flag “preliminary (n=Xh)”, EWMA; show both GB and wear models. |
| TBW derivation unstable when pct=0 | Fallback to wear model only; badge “TBW unknown”. |
| Side-by-side overflow | `min-w-0 flex-1` + `grid-cols-1 lg:grid-cols-2` + 200px height; test 1024/1440. |
## 11. Verification
- [ ] `python3 -m py_compile fenris.py` / `bash -n fenris.sh` pass
- [ ] `python3 fenris.py backup` → unknown command; menu 1–5 only
- [ ] `python3 fenris.py start --interval 10` → badge `every 10s`
- [ ] `/api/config`, `/api/hourly`, `/api/data` 200; 304 when unchanged; `curl /assets/bongbetic-logo-dark.svg` svg+xml
- [ ] Cards tight: 5/col xl, p-3, no overflow; graphs side-by-side lg, stacked sm, each ~200px, flex resize no clipping
- [ ] writeChart X `00:00 06:00 12:00 18:00 24:00` today-only; midnight resets to 0
- [ ] Kill/restart daemon → `/api/hourly` rebuilt from history, no dupe hours
- [ ] `<24h`: forecast badge “Warming up (n=Xh) ~Y days (preliminary)”
- [ ] `≥24h`: let run 24h (or fake hourly file with 24 records) → card shows `hourly_avg` over 24, `days_remaining` updates each poll (change write load → forecast moves within one interval); bar of 24h gb/hour visible
- [ ] Tab hidden → pause, visible → immediate fetch; stale banner > interval*2
- [ ] `prefers-color-scheme` legible
## 12. Out of Scope
- Vite/React build, SSE, auth, purge — removed intentionally.
---
*Option A — dense side-by-side + hourly live forecast — ready to implement.*