# 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 (`` served from the **local cache** §6.6; slug = `icon` or slugified `name`; first-letter `
` 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 `.png` once from dashboard-icons (`cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/.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.