diff --git a/.gitignore b/.gitignore index 69ac2c8..888b313 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ data/fenris.pid data/fenris.log data/history.jsonl data/hourly.jsonl +plan-dash-changes.md diff --git a/README.md b/README.md index b60ee71..e3af9e4 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,79 @@ -# Fenris β€” NVMe Wear Monitor & Live Dashboard +

+ + + Bongbetic + +
+ crafted with stubborn curiosity by Bongbetic +

-Created by Bongbetic. +

+ Fenris glyph +

-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. +

Fenris 🐺 β€” Your SSD's Tell-All Diary

-## Requirements +

+ Your NVMe drive has been keeping secrets. Fenris makes it confess β€” in real time. +
+ How much did you write today? How long until it taps out? No fairy dust β€” just your actual bytes. +

+ +--- + +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+** -- **smartmontools** (`smartctl`) installed -- Root access to read NVMe SMART logs +- **smartmontools** (`smartctl`) +- 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 sudo visudo -# Add this line (replace youruser with your username): +# add this line (swap in your username): 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 ./fenris.sh +# pick 1) Start monitoring β†’ choose device / interval / port β†’ done ``` -### CLI +### The no-nonsense way ```bash -# Start daemon + dashboard in background -python3 fenris.py start - -# Check status and latest wear stats -python3 fenris.py status - -# Take one sample now -python3 fenris.py sample - -# Stop the daemon -python3 fenris.py stop +python3 fenris.py start # defaults: /dev/nvme0, every 300s, port 8420 +python3 fenris.py start --interval 60 --port 9000 # if you're impatient +python3 fenris.py status # "are we live? how's the drive?" +python3 fenris.py sample # one sneaky sample right now +python3 fenris.py stop # tuck it back in ``` -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 - -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: +Run `./fenris.sh` and you'll get: ``` 1) Start monitoring (background daemon + dashboard) @@ -93,53 +83,80 @@ Run `./fenris.sh` for a guided interface: 5) Open dashboard URL --- h) Help / how this works - q) Exit + q) Exit (go touch grass) ``` -## Data Collected +## What Fenris jots down -| Field | Description | -|-------|-------------| -| `percentage_used` | SSD's own wear indicator (0-100%) | -| `bytes_written` / `bytes_read` | Total data written/read | -| `available_spare` | Remaining spare capacity (%) | -| `media_errors` | Number of uncorrectable errors | -| `power_on_hours` | Total power-on hours | -| `temperature_c` | Current temperature | -| `critical_warning` | NVMe critical warning flags | +| Field | What's the gossip? | +|-------|---------------------| +| `percentage_used` | The drive's own wear-o-meter (0–100%) | +| `bytes_written` / `bytes_read` | Lifetime totals β€” the receipts | +| `available_spare` | Spare blocks left (%) | +| `media_errors` | Uncorrectable boo-boos | +| `power_on_hours` | How long it's been awake | +| `temperature_c` | Is it sweating? | +| `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 -- 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` +## The dashboard β€” what's on screen -## 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.py # Main Python script -β”œβ”€β”€ fenris.sh # Interactive menu wrapper -β”œβ”€β”€ README.md +β”œβ”€β”€ fenris.py # the whole show β€” daemon + server + math +β”œβ”€β”€ fenris.sh # the cozy menu +β”œβ”€β”€ README.md # hi β€” you're here +β”œβ”€β”€ assets/bongbetic-brand/ # Bongbetic wordmarks & glyphs (for Gitea + dashboard) └── data/ - β”œβ”€β”€ history.jsonl # Raw sample log (JSONL) - β”œβ”€β”€ hourly.jsonl # Per-hour aggregates (rebuilt from history on restart) - β”œβ”€β”€ fenris.pid # Daemon PID file - └── fenris.log # Daemon log output + β”œβ”€β”€ history.jsonl # raw samples (JSONL, append-only) + β”œβ”€β”€ hourly.jsonl # per-hour rollups (auto-rebuilt on restart) + β”œβ”€β”€ fenris.pid # daemon PID + └── 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"** ```bash sudo apt install smartmontools # Debian/Ubuntu -sudo pacman -S smartmontools # Arch +sudo pacman -S smartmontools # Arch β€” you already knew ``` **"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"** -Daemon not running. Check with `python3 fenris.py status` and restart if needed. +**Dashboard says "stale"** +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. + +--- + +

+ Fenris 🐺 β€” by Bongbetic Β· Be kind to your SSD and it'll be kind to you. +
+ Bongbetic icon +

diff --git a/assets/bongbetic-brand/b_glyph.svg b/assets/bongbetic-brand/b_glyph.svg new file mode 100755 index 0000000..9618252 --- /dev/null +++ b/assets/bongbetic-brand/b_glyph.svg @@ -0,0 +1,40 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + diff --git a/assets/bongbetic-brand/favicon-32.png b/assets/bongbetic-brand/favicon-32.png new file mode 100755 index 0000000..6ab61ee Binary files /dev/null and b/assets/bongbetic-brand/favicon-32.png differ diff --git a/assets/bongbetic-brand/icon-dark-512.png b/assets/bongbetic-brand/icon-dark-512.png new file mode 100755 index 0000000..a68e9de Binary files /dev/null and b/assets/bongbetic-brand/icon-dark-512.png differ diff --git a/assets/bongbetic-brand/icon-light-512.png b/assets/bongbetic-brand/icon-light-512.png new file mode 100755 index 0000000..336c688 Binary files /dev/null and b/assets/bongbetic-brand/icon-light-512.png differ diff --git a/assets/bongbetic-brand/wordmark-dark.png b/assets/bongbetic-brand/wordmark-dark.png new file mode 100755 index 0000000..607068e Binary files /dev/null and b/assets/bongbetic-brand/wordmark-dark.png differ diff --git a/assets/bongbetic-brand/wordmark-dark@2x.png b/assets/bongbetic-brand/wordmark-dark@2x.png new file mode 100755 index 0000000..7654828 Binary files /dev/null and b/assets/bongbetic-brand/wordmark-dark@2x.png differ diff --git a/assets/bongbetic-brand/wordmark-light.png b/assets/bongbetic-brand/wordmark-light.png new file mode 100755 index 0000000..b4bc818 Binary files /dev/null and b/assets/bongbetic-brand/wordmark-light.png differ diff --git a/plan-dash-changes.md b/plan-dash-changes.md deleted file mode 100644 index 7a15b31..0000000 --- a/plan-dash-changes.md +++ /dev/null @@ -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Γ— `` 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.*