Server-side icon cache (lib/health/icons.js + GET /api/icons/:slug.png) fetches each icon once from dashboard-icons into a persistent dir and serves it from the LAN. Browser never contacts the CDN; slug sanitized; first-letter fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
219 lines
11 KiB
Markdown
219 lines
11 KiB
Markdown
# Void 2.0 — Plan 6: Sacred Valley Widgets — Design Spec
|
||
|
||
> Status: APPROVED 2026-06-02. Design-led phase; brainstormed with the
|
||
> `frontend-design` skill and the visual companion. Next step: `writing-plans`.
|
||
> Supersedes the prep brief `docs/plan-6-brainstorm-brief.md`.
|
||
|
||
## 1. Purpose
|
||
|
||
Bring the Sacred Valley homelab dashboard to Void 2.0, on v2's real backend.
|
||
The `#/sacred-valley` route currently holds a stub (`public/views/sacred_valley.js`);
|
||
this plan replaces it with a working dashboard composed of **two distinct bands**:
|
||
|
||
1. **Data cards** — a draggable, reorderable set of widget cards.
|
||
2. **Little Blue's Health band** — a separate, fixed zone owned by the Little Blue
|
||
agent persona, showing homelab service health & uptime.
|
||
|
||
The two bands are kept visually and structurally separate, by explicit request.
|
||
|
||
## 2. Scope
|
||
|
||
### In scope (ships as `2.0.0-alpha.8`)
|
||
- The two-band Sacred Valley view + grid framework + drag-to-reorder.
|
||
- Seven data cards: clock, weather, host-perf, speedtest, jobs, inbox, search.
|
||
- Little Blue **read-only** Health band: config-file service registry, a pg-boss
|
||
health-check engine, grouped status tiles with auto-pulled icons, links, and a
|
||
placeholder avatar/identity.
|
||
- Server-side, owner-scoped layout persistence.
|
||
|
||
### Explicit non-goals (deferred)
|
||
- Little Blue as a **fix-it agent** — chat + repair/Proxmox tools. Belongs with the
|
||
later Yerin/Orthos agent-wiring phase.
|
||
- DB-backed and Proxmox-auto-discovery service registries (future upgrade path).
|
||
- Multi-host host-perf (Plan 6 monitors CT 311 only; "other hosts up?" is the
|
||
health band's job).
|
||
- Per-agent hue theming, `--rail-accent`, final Little Blue avatar art.
|
||
- Agent-output cards (Dross briefing / Yerin pulse / Orthos council).
|
||
|
||
## 3. Decisions (from the brainstorm)
|
||
|
||
| Decision | Choice |
|
||
|---|---|
|
||
| Grid engine | Custom CSS-grid auto-flow + hand-rolled drag-to-reorder. **No resize, zero deps.** Sizes are S/M/L presets (column spans 1/2/3). |
|
||
| Scope of dashboard | One **global** homelab dashboard (not per-Space). |
|
||
| Layout persistence | **Server-side**, owner-scoped, so desktop ↔ phone sync. localStorage mirrors for instant paint. |
|
||
| Card chrome | **"Refined B"** — dark panel, faint engraved texture, accent-underlined Cinzel title, low blackflame glow that **intensifies on hover**. |
|
||
| Real-time | **Per-card polling**, no new SSE. |
|
||
| Service registry (LB) | **Config file** now; DB-backed + Proxmox auto-discovery are noted future upgrades. |
|
||
| Little Blue fix-it tools | **Deferred** to a later agent phase. |
|
||
| Service icons | **Cached locally, server-side.** The server fetches each needed icon once from dashboard-icons into a persistent cache and serves it from the LAN. Browser never hits the CDN; no per-request slug leak. First-letter fallback on miss. |
|
||
|
||
## 4. Architecture
|
||
|
||
### 4.1 View structure
|
||
`public/views/sacred_valley.js` (replaces stub) renders into `#main`:
|
||
- `#sv-cards` — the data-card band (draggable).
|
||
- `#sv-health` — Little Blue's health band (fixed).
|
||
|
||
A small scheduler starts each card's refresh timer on view-mount and clears all
|
||
timers on unmount (route change), so polling never leaks across views.
|
||
|
||
### 4.2 Card component contract
|
||
`public/components/sv_card.js` is a factory producing the refined-B chrome
|
||
(`.sv-card` + theme tokens). Each data card is a self-contained module in
|
||
`public/views/cards/` exporting a uniform interface:
|
||
|
||
```js
|
||
export default {
|
||
id: 'host-perf',
|
||
title: 'Host Perf',
|
||
size: 'm', // 's' | 'm' | 'l' → grid column span 1 | 2 | 3
|
||
mount(bodyEl) { /* build DOM via safe el()/mount() — never innerHTML from data */ },
|
||
start() { /* begin refresh interval */ },
|
||
stop() { /* clear interval */ }
|
||
};
|
||
```
|
||
|
||
One module per card — each independently understandable and testable. All DOM is
|
||
built with the existing safe `el()/mount()`/`safeHref()` helpers (Plan 2 safe-DOM
|
||
invariant: no `innerHTML` from API data).
|
||
|
||
### 4.3 Grid + reorder
|
||
CSS-grid auto-flow; S/M/L = column spans. A hand-rolled pointer-drag reorder on
|
||
`#sv-cards` (no resize). Reordering updates the in-memory order and persists it
|
||
(§4.4). The health band is not draggable.
|
||
|
||
### 4.4 Layout persistence
|
||
- Migration: `dashboard_layout` table, owner-scoped, single logical row:
|
||
`{ order: text[]/jsonb, hidden: jsonb, sizes: jsonb }` + `updated_at`.
|
||
- API: `GET /api/dashboard/layout`, `PUT /api/dashboard/layout` (owner bearer).
|
||
- Client: localStorage mirror for first paint; server is source of truth.
|
||
|
||
## 5. Data cards
|
||
|
||
| Card | Size | Source | Backend |
|
||
|---|---|---|---|
|
||
| clock | S | Client only. Melbourne primary; optional secondary TZ. | none |
|
||
| weather | S | Open-Meteo, Melbourne, no key. | **new** `GET /api/weather` — server proxy, 15-min cache |
|
||
| host-perf | M | CT 311 `/proc` CPU/RAM/disk/net, 30s. | **new** `GET /api/host` — port v1 `lib/resources.js` → `lib/host/resources.js` |
|
||
| speedtest | M | Latest + ~30-bar history. | **new** `speedtest_results` migration + pg-boss recurring `speedtest` job (hourly `speedtest-cli`) + `GET /api/speedtest/history` + `POST /api/speedtest/run` (enqueue) |
|
||
| jobs | M | pg-boss states — counts by state + recent. | **reuse** Jobs API |
|
||
| inbox | S | Pending changes awaiting approval — count + recent; links `#/inbox`. | **reuse** pending_changes API |
|
||
| search | L | Spotlight: type → results → navigate. | **reuse** `GET /api/search` (FTS+vector RRF) |
|
||
|
||
Notes:
|
||
- `speedtest-cli` (or `speedtest` Ookla CLI) must be present on the worker box;
|
||
`push-workers.sh` / deploy notes updated accordingly.
|
||
- `/api/weather` caches server-side (15 min) to avoid hammering Open-Meteo.
|
||
|
||
## 6. Little Blue Health band
|
||
|
||
### 6.1 Identity
|
||
- `public/components/littleblue_avatar.js` — **placeholder** blue humanoid
|
||
water-creature (inline SVG, cyan `--lb` glow). Marked placeholder; final art later.
|
||
- Header subtitle: "Health & Uptime of the lab".
|
||
|
||
### 6.2 Service registry (config file)
|
||
`config/services.yaml`, authored fresh — **v1 tile titles are NOT inherited**
|
||
(they were mislabeled); every entry gets a correct title here.
|
||
|
||
```yaml
|
||
- id: gitea
|
||
name: Gitea
|
||
category: infrastructure # agents | infrastructure | media
|
||
host: ct105 # display label on the tile
|
||
url: http://192.168.1.223:3000 # link target — "how to get there"
|
||
icon: gitea # optional; defaults to slugified name
|
||
check:
|
||
type: http # http | tcp
|
||
# url defaults to `url`; expect 2xx–3xx; short timeout
|
||
```
|
||
|
||
Group render order: **Agents → Infrastructure → Media → other**. Each group header
|
||
shows a "X/Y healthy" count. The build seeds the file with the real homelab
|
||
services for the user to correct.
|
||
|
||
### 6.3 Health-check engine
|
||
- A **Node pg-boss recurring job** `health.check` (every ~60s) pings each service
|
||
(HTTP/TCP, short timeout) from void-server and writes a cache row to a
|
||
`service_status` table: `{ service_id, status, latency_ms, detail, checked_at }`.
|
||
- Status semantics: `ok` = check passed · `warn` = reachable but slow/degraded ·
|
||
`down` = unreachable/error.
|
||
- Decoupled from page load → the band renders instantly from cache.
|
||
- The checker only fetches URLs from the operator-authored registry (no user
|
||
input) — pinned to the configured list (SSRF surface is operator-controlled).
|
||
|
||
### 6.4 API
|
||
- `GET /api/health/services` → services grouped by category, each with cached
|
||
status + per-group healthy counts.
|
||
- `POST /api/health/check` → enqueue an immediate pass (owner-only). Backs the
|
||
"Run checks" button.
|
||
|
||
### 6.5 Rendering
|
||
- `public/views/health_band.js` + `public/components/service_tile.js`.
|
||
- Tile: auto-icon (`<img loading="lazy" src="/api/icons/<slug>.png">` served from
|
||
the **local cache** §6.6; slug = `icon` or slugified `name`; first-letter `<div>`
|
||
fallback on `onerror`), status dot (ok/warn/down), name, host, hover "open ↗"
|
||
link → `url`.
|
||
- Grouped sections with header + "X/Y healthy" + divider, refreshed every 60s.
|
||
|
||
### 6.6 Local icon cache (no CDN leak)
|
||
- `lib/health/icons.js` + a persistent cache dir `ICON_CACHE`
|
||
(default `/var/lib/void/icons`, like the blob store — survives deploys).
|
||
- `GET /api/icons/:slug.png` serves the cached file. On a **cache miss**, the
|
||
server fetches `<slug>.png` once from dashboard-icons
|
||
(`cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/<slug>.png`), validates it's
|
||
a PNG, writes it to the cache, then serves it. Subsequent requests are local.
|
||
- Slug is constrained to `[a-z0-9-]` (no path traversal, no arbitrary upstream).
|
||
- On upstream 404/error, respond 404 → the tile falls back to the first-letter
|
||
badge. Only the **server** ever contacts the CDN, once per distinct icon.
|
||
- "As needed": icons are fetched lazily on first reference, not bulk-synced.
|
||
|
||
## 7. Theme tokens
|
||
Add to `public/style.css :root`:
|
||
- Reuse `--accent` / `--accent-dim` / `--panel` / `--border` for refined-B chrome.
|
||
- Add `--lb: #7dd3d8` (Little Blue cyan) for the health band.
|
||
- A commented per-agent-hue block (Dross/Yerin/Orthos) reserved, unused this plan.
|
||
|
||
## 8. Testing (TDD, real DB per v2 norms)
|
||
Backend (vitest):
|
||
- weather proxy cache behavior (mock fetch).
|
||
- `/api/host` response shape.
|
||
- speedtest repo + `/api/speedtest/history`.
|
||
- health engine status computation (ok/warn/down from mocked fetch/connect).
|
||
- registry loader: parse + group + ordering + icon-slug derivation.
|
||
- `dashboard_layout` repo: get/put + owner scoping.
|
||
- `/api/health/services` grouping + healthy counts.
|
||
- icon cache: miss → fetch-once → cached file served; slug sanitization rejects
|
||
traversal/invalid chars; upstream 404 → 404 (browser falls back).
|
||
|
||
Frontend (pure-logic units):
|
||
- card module contract (each exports id/title/size/mount/start/stop).
|
||
- reorder ordering logic.
|
||
- category ordering (Agents→Infra→Media→other).
|
||
- safe-DOM: no `innerHTML` from data.
|
||
|
||
Security: confirm the health checker rejects targets outside the registry.
|
||
|
||
## 9. Build order (design risk → integration risk)
|
||
1. Grid framework + card contract + refined-B chrome + reorder + layout
|
||
persistence, proven on **clock / weather / host-perf**.
|
||
2. Reuse cards — **jobs / inbox / search**.
|
||
3. **speedtest** (worker + table + cron).
|
||
4. **Little Blue health band** (registry + engine + tiles + icons + avatar
|
||
placeholder).
|
||
|
||
## 10. Release
|
||
- Version → `2.0.0-alpha.8` (bump `server.js` VERSION const + `package.json` +
|
||
CHANGELOG).
|
||
- New migrations: `dashboard_layout`, `service_status`, `speedtest_results`.
|
||
- Standard deploy: snapshot CT 310 + 311, `deploy/push.sh`, run `npm run migrate`,
|
||
refresh workers if `push-workers.sh` changed, verify `/health` = alpha-8.
|
||
|
||
## 11. Future (out of this plan)
|
||
- Little Blue fix-it agent (chat + repair/Proxmox tools).
|
||
- DB-backed + Proxmox-auto service registry.
|
||
- Multi-host metrics; per-agent theming; final avatar art; agent-output cards.
|
||
- Optional: pre-seed/vendor the icon cache at deploy (fully offline, no first-use
|
||
CDN fetch) and a periodic refresh.
|