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.
## Requirements
- **Python 3.7+**
- **smartmontools** (`smartctl`) installed
- Root access to read NVMe SMART logs
### Setting up passwordless smartctl
Fenris runs `sudo -n smartctl ...` (no-prompt sudo). Either run with sudo or allow passwordless access:
```bash
sudo visudo
# Add this line (replace youruser with your username):
youruser ALL=(root) NOPASSWD: /usr/sbin/smartctl
```
## Quick Start
### Interactive Menu
```bash
./fenris.sh
```
### CLI
```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
```
Dashboard available at: `http://localhost:8420`
## CLI Reference
### 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.
- **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
- 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):
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).
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
**`README.md`:** drop Data Management; add “Hourly diagnostics `data/hourly.jsonl` (GB/hour) + live days-remaining forecast after 24h” + `jq` truncate note.
- [ ] 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
*Option A — dense side-by-side + hourly live forecast — ready to implement.*
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.