Files
Dash/ROADMAP.md
T
Tomas DvorakandDevin 54b5497121
ci / backend (push) Canceled after 0s
ci / frontend (push) Canceled after 0s
docs: v2.0.0 changelog, configuration for auth/boards, announcement refresh
- CHANGELOG 2.0.0 entry covering all Phase 5 waves
- configuration.md: transports, per-monitor alert rules, auth, boards,
  appearance/language, PWA
- announcement.md: updated for shipped features and opt-in auth
- AGENTS.md: "no auth" line was stale
- ROADMAP: tick shipped M1–M3 planning checkboxes

Authored By: TDvorak <[email protected]>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-22 13:10:29 +02:00

296 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Dash — Roadmap
A self-hosted homelab dashboard. CasaOS-grade ease of use, Vercel-grade design.
Single container, UI-managed, no YAML, no auth required.
Stack: **Go + Gin + SQLite** (single static binary, embeds frontend) · **React 18 + Vite + TS strict + Tailwind + shadcn/ui + dnd-kit** · OpenAPI-generated client.
---
## Design system
Multiple board styles are a core feature: same system, same functionality,
different UI structure per style. A **renderer** supplies the layout; board
state, drag-drop, popovers, and dialogs are shared primitives underneath.
| Renderer | Source mockup | Ships |
|---|---|---|
| Bento (default) | `d-bento-mono.html` | v0.1.0 — mixed-size grid, widgets inline |
| Cards | `a-vercel-mono.html` | v0.1.0 — uniform compact grid; validates the seam early |
| Index | `e-editorial.html` | v0.3.0 — serif headers, list rows, stat strip |
| Console | `f-console.html` | v0.3.0 — terminal panels, `[ up ]` tags |
Contract: `renderers/<name>/{BoardView, SectionView, ItemView, WidgetView}`.
Tokens (`--bg`, `--surface`, `--border`, …) shared; dark/light is orthogonal.
User picks renderer in settings; persisted. Renderers never reimplement dnd
or popover logic.
---
## Releases
| Version | Contents | Gate |
|---|---|---|
| `v0.1.0` | Phase 0 + 1 — working MVP, single image | manual smoke test ✅ |
| `v0.2.0` | Phase 2 — widget layer + first integrations | Pi-hole widget live ✅ |
| `v0.3.0` | Phase 3 — four renderers, ⌘K palette, icon suggest | palette + switcher live ✅ |
| `v0.4.0` | Phase M1 — uptime monitors + alerts backbone | monitor lifecycle live |
| `v0.5.0` | Phase M2 — domain intelligence | domain lookup + expiry alerts |
| `v0.6.0` | Phase M3 — system monitoring + agent | agent ingest + charts |
| `v0.7.0` | Phase M4 — incidents, status pages, badges, metrics | public status page live |
| `v1.0.0` | Phase 4 — OSS polish, docs, importers, CI releases | public repo launch |
| `v1.1.0` | P5a — uptime graphs, iframe/JSON widgets, theming, PWA, demo GIF | per-item ships |
| `v1.2.0` | P5b — SMTP + notifier presets + alert rules | alert pipeline live |
| `v1.3.0` | P5c — per-container metrics, SMART/ZFS/GPU, subdomain discovery | agent payload v2 |
| `v2.0.0` | P5d — opt-in auth, multiple boards, full i18n | structure change |
---
## Phase 0 — Foundation → part of v0.1.0 ✅ done 2026-09-17
Repo hygiene and pipelines before features.
- [x] Monorepo scaffold (`apps/frontend`, `apps/backend`, `packages/api-client`, `infra/`, `data/`)
- [x] MIT `LICENSE`, `README.md`, `.gitignore`, `AGENTS.md`
- [x] `openapi.yaml` stub → generated TS client (`packages/api-client`)
- [x] Backend skeleton: Gin router, `zap` logging, `/api/healthz`, SQLite open + goose migrations runner
- [x] Frontend skeleton: Vite + TS strict + Tailwind + shadcn/ui, theme tokens matching `d-bento-mono.html` CSS vars
- [x] CI (GitHub Actions): `go build ./...`, `go vet ./...`, `go test ./...`, `tsc --noEmit`, `oxlint` + vendored anti-slop rules, `npm run build`
- [x] Multi-stage `Dockerfile` (frontend build → Go build → distroless, `embed.FS` serves UI)
- [x] SPA fallback routing in embedded server (`/` + client-side paths → `index.html`, `/api` isolated)
- [x] `docker-compose.yml` — one service, `./data:/data` volume
**Exit:** `docker compose up` serves a blank board — verified locally (33.6 MB image). CI green on `main` — pending first push.
## Phase 1 — MVP → v0.1.0 ✅ done 2026-09-17
The product's spine: sections, services, drag-drop, multi-URL.
- [x] Schema: `sections`, `items`, `urls`, `settings` (fractional `position REAL` ordering)
- [x] REST API: CRUD for sections/items/urls, `reorder` endpoints, icon upload, `settings`, `export`/`import` JSON
- [x] Renderer contract: shared board store + `BoardView`/`SectionView`/`ItemView`/`WidgetView` seam, dnd + popover as shared primitives
- [x] **Bento** renderer (default): mixed-size grid, hairline section headers, tiles with status chips
- [x] **Cards** renderer: uniform compact grid — second renderer proves the seam while code is small
- [x] dnd-kit: reorder items in a section, move items between sections, reorder sections; persist on drop
- [x] Collapse/expand sections (persisted)
- [x] `UrlPopover`: 1 URL → direct open, 2+ → chooser (`local`/`external`/custom tags)
- [x] `ServiceDialog` — one dialog for add + edit (name, URLs+labels, icon URL or file upload)
- [x] Icon handling: upload to `data/icons/`, remote URL passthrough, letter-tile fallback
- [x] Dark/light toggle (persisted), renderer picker in settings, responsive to ~360px
- [x] Pre-paint theme bootstrap in `index.html` (no wrong-theme flash on load)
- [x] Status ping: server-side HEAD request per service URL, cached ~60s → `up`/`down` chip
- [x] Parallel probing: per-item `sync.WaitGroup` fan-out, 4s client timeout
- [x] URL scheme allowlist (`http`/`https` only — `file://` etc. rejected on create + probe)
- [x] Post-drag click suppression (`lastDropAt` window — drops no longer fire tile activation)
- [x] Mobile polish: status chip collapses to dot-only below 620px
- [x] `api_test.go`: reorder positions, URL scheme rejection, settings round-trip
**Exit:** create sections, add services with icons and dual URLs, drag everything, reload — state persists. `docker run` single image works. Export/import round-trips. — verified: full API + browser smoke (dnd, popover, upload, collapse, themes, both renderers, 360px), `api_test.go` covers reorder/URL-validation/settings.
## Phase 2 — Widgets → v0.2.0 ✅ done 2026-09-17
- [x] Go widget registry: `Widget` interface (`Type()`, `Fetch(cfg)`), `GET /api/widgets/:id/data` with ~30s in-process TTL cache
- [x] `GET /api/widgets/types` → registry metadata drives the add-widget dialog
- [x] `WidgetHost` component: maps `config.type` → React component, polls with backoff
- [x] Widgets as grid items (same `items` table, `kind = 'widget'`, `config JSON`)
- [x] Clock/timezone widget (frontend-only)
- [x] Pi-hole fetcher (blocked %, queries today) — per-day bar series deferred: v5 `api.php` lacks it, needs Pi-hole v6 `/api/history`
- [x] AdGuard Home fetcher (blocked %, queries, avg processing ms)
- [x] Immich fetcher (photo/video count, library size)
- [x] Per-widget config UI (endpoint URL, API key — stored in `config`, never logged)
- [x] `local` type flag: frontend-only widgets (clock) register in `/api/widgets/types` without a fetcher
- [x] Widget tile error/loading states (graceful upstream 502 rendering)
- [x] Widget endpoint tests: type registry, 404 on missing item, 502 on dead upstream, TTL cache hit
**Exit:** Pi-hole widget shows real data; adding a new integration = one Go file + one React component. — verified: registry, cache, 502 path, generic config dialog, clock live in-browser, Pi-hole fetcher against stubbed v5 API.
## Phase 3 — Renderers & ergonomics → v0.3.0 ✅ done 2026-09-17
- [x] **Index** renderer: serif section headers, full-width rows, masthead stat strip
- [x] **Console** renderer: bordered panels/tables, JetBrains Mono, `[ up ]` tags
- [x] Renderer switcher polished (2×2 radio grid with per-renderer descriptions)
- [x] ⌘K command palette: jump to service, add service, toggle theme, switch renderer (⌘K / Ctrl+K / `/`)
- [x] dashboard-icons auto-suggest in icon picker (name → slug → CDN URL chip + live preview)
- [x] Renderer contract `index` prop → row/section numbering without re-deriving position
- [x] Icon URL live preview in service dialog (dead URLs degrade to dashed tile)
- [x] Palette `/` shortcut + header search button for discoverability
- [x] Fonts: Instrument Serif + JetBrains Mono via fontsource (bundled, no CDN dep)
- [x] Fix: global `:focus-visible` moved into `@layer base` (was silently overriding all utility overrides)
- [ ] User services list → prioritized widget backlog (owner-supplied list pending)
**Exit:** all four renderers work off the same board state; palette navigates everything. — verified: both new renderers render/persist/dnd/collapse, palette filters and executes, icon suggest resolves real CDN assets, 360px + light theme, docker image 43.9 MB.
## Merge program — Beszel fork → Dash
Source: the local Beszel fork (`~/Desktop/PROG+HTML/Beszel` — PocketBase hub
with custom monitors/domains/status-pages) plus lissy93/domain-locker as
design inspiration. Upstream references cloned to `../_ref/beszel` and
`../_ref/domain-locker` for consultation only.
**No code moves verbatim.** The fork is PocketBase collections + its own
auth + a WebSocket agent; Dash is Gin + SQLite + goose with no auth. We port
data models, check logic, and lookup pipelines — DB, API, and UI layers are
re-implemented on Dash conventions.
**Deployment reality.** Monitoring needs a persistent process (schedulers),
non-HTTP egress (WHOIS :43, ICMP, SMTP, raw TCP), and a writable DB. None of
that exists on serverless — Vercel cannot host this product, full stop. The
deploy story stays `docker run` + GHCR image + goreleaser binaries; agents
are a second small binary.
**UI model.** The app becomes routed: `/` board (default), `/monitors`,
`/domains`, `/systems`, `/status/:slug` (public, no board chrome). Router:
`wouter` (~2 kB — every route we need, nothing more). Charts: `recharts`.
Board widgets surface monitoring entities through the existing widget
registry — monitor uptime, domain expiry, system gauges — so the board
becomes the summary layer over the monitoring pages.
### Phase M1 — Monitors → v0.4.0
Uptime checking: the backbone everything else hangs off.
- [x] Schema: `monitors` (name, type, url/hostname/port, method, keyword, json_query, expected_value, interval_s, timeout_s, retries, active, status, tags, notes), `heartbeats` (monitor_id, status, ping_ms, msg, cert_expiry, checked_at — 30-day retention prune)
- [x] In-process scheduler: due-monitor sweep, bounded worker pool, heartbeat write, status transitions
- [x] Checkers: `http`/`https`, `tcp`, `ping` (unprivileged ICMP datagram via `x/net/icmp` — distroless has no ping binary), `dns`, `keyword`, `json-query`, `push` (caller-generated ingest URL)
- [x] API: `/api/monitors` CRUD, `/:id/heartbeats?range=`, pause/resume, check-now
- [x] Monitors page (status, uptime 24h/30d, ping, interval) + detail (response chart, heartbeat log)
- [x] Board widget type `monitor` bound to a monitor id
- [x] ServiceDialog checkbox: create matching http monitor from a service URL
- [x] The fork's ~30 exotic monitor types were TCP stubs — not ported. Real types only.
### Phase M2 — Domains → v0.5.0
Domain-locker-grade domain intelligence.
- [x] Schema: `domains` (core columns + `extra` JSON for the long tail), `domain_history` (domain_id, change_type, field, old, new, at)
- [x] Lookup pipeline ported from fork `hub/domains/whois/lookup.go`, trimmed: RDAP over HTTPS → native WHOIS TCP:43 + parser; SSL chain via `crypto/tls`; DNS (NS/MX/TXT/A/AAAA) via `net.Resolver`; host geo via ip-api; provider detection via fork `detect/providers.go`; favicon
- [x] Daily scheduler + manual refresh; field diffs recorded to `domain_history`
- [x] Alerts: `alert_rules` + `notifications` tables; dispatchers — generic webhook (JSON POST), SMTP via `net/smtp`, Discord/Slack presets. Triggers: domain expiry ≤ N days, SSL expiry ≤ N days, monitor down/recovered (M1), system offline (M3)
- [x] Domains page + detail (expiry countdown, registrar, SSL, DNS, subdomains)
- [x] Board widget `domain` (expiry countdown)
- [x] Subdomain discovery: port `subdomain_discovery.go`, opt-in per domain
- [x] Not ported (v1): whoisxml API, EURid web scraping, SEO/robots parsing, valuation estimates — demand only
### Phase M3 — Systems → v0.6.0
Beszel-style server monitoring, push-based.
- [x] Schema: `systems` (name, token, host, os/arch, last_seen, status, `latest` JSON snapshot), `system_stats` (system_id, ts, payload JSON — one column, no sub-field queries; 7d retention prune)
- [x] Ingest: `POST /api/systems/ingest` with per-system bearer token; offline when silent > max(3×interval, 30s); `system.up`/`system.down` webhook events
- [x] `cmd/dash-agent`: Linux collector — `/proc` (cpu/mem/net/load/uptime), `/sys` hwmon temps, statfs disk, docker.sock container list; POST every `-interval` (default 10s); `-once` debug mode
- [x] Agent packaging: systemd unit (`packaging/dash-agent.service`) + `infra/Dockerfile.agent` + goreleaser binaries (Phase 4)
- [x] Systems page (status cards, cpu/mem/disk bars, uptime) + detail (recharts: cpu%, mem%, net rx/tx, load; temps + containers tables)
- [x] Board widget `system` (cpu/mem/disk mini bars)
- [x] Not ported: the beszel agent protocol (SSH/WS into PocketBase) — our agent is push-JSON. SMART/ZFS/GPU metrics + per-container cpu/mem deferred to demand.
### Phase M4 — Ops layer → v0.7.0
- [x] Incidents: `incidents` + `incident_updates`; manual CRUD + auto-open on monitor down, auto-resolve on recovery; severity + status flow (open → ack → resolved → closed)
- [x] Maintenance windows: `maintenance_windows` (monitor_ids empty = all); suppress alerts + auto-incidents, status pages show `maintenance` state while active
- [x] Status pages: `status_pages` (monitor_ids/system_ids JSON columns — a join table for ≤ dozens of ids is ceremony); admin `/status`, public `/status/:slug` + `GET /api/status-pages/:slug/public`
- [x] Badges: `GET /api/badge/:kind/:id.svg` — stateless SVG from live data (monitor status+uptime, domain days-left, system status)
- [x] `GET /api/metrics` Prometheus exposition
- [x] Bulk: `POST /api/import/csv?kind=monitors|domains` header-driven CSV; monitors/domains/systems rows folded into `/api/export` (v2, additive)
- [x] ⌘K palette + header nav wired for new pages
## Phase 4 — Open-source launch → v1.0.0
- [x] Docs: install, configuration, widget development guide (`docs/`)
- [x] Importers: Homepage `services.yml`, Homarr JSON, Dashy `conf.yml` → sections/items/urls
- [x] `README.md` hero: screenshots, `docker run` one-liner (demo GIF deferred — pngs suffice for v1.0.0)
- [x] goreleaser → binaries; GHCR image on tag; semver + `CHANGELOG.md`
- [x] `CONTRIBUTING.md`, issue/PR templates, code of conduct, security policy
- [x] i18n scaffolding (`src/i18n/` — typed `t()` + `en` dict; full string extraction deferred to first real locale)
- [x] Public repo (already flipped) + announcement draft (`docs/announcement.md`) — v0.7.0 released: binaries + GHCR image live
**Exit:** a stranger can `docker run`, import their Homepage config, and have a working board in under two minutes.
## Phase 5 — Post-1.0 (shipped 2026-09-22)
All items shipped on `feat/phase5`.
### P5a — Quick wins (frontend-heavy, no schema risk)
- [x] **Uptime graphs** — monitor detail renders 24h/30d heartbeat bars +
latency line (recharts, already a dep). Data: existing
`/api/monitors/:id/heartbeats` + uptime stats. Frontend only.
- [x] **iframe widget** — `config.url`, sandboxed `<iframe>` tile in the
widget registry; local type, no backend fetcher.
- [x] **Generic JSON-path widget** — backend fetcher: GET `config.url` with
optional headers, extract `config.path` (dot/bracket path), display
value + optional label/format. Covers most "is API X alive" asks.
- [x] **Custom CSS / accent / wallpaper** — settings keys `accent`,
`custom_css`, `wallpaper_url`; injected as CSS var override + `<style>`
+ board background layer. Respect reduced-motion/contrast defaults.
- [x] **PWA** — `manifest.webmanifest` (icons, name, theme color), minimal
service worker: cache-first for static assets, network-first for /api.
No offline board editing — read-only graceful degradation.
- [x] **Demo GIF** — scripted playwright walkthrough (board → monitors →
status page) rendered via ffmpeg; committed to `docs/screenshots/`.
### P5b — Alert depth
- [x] **SMTP transport** — `net/smtp` stdlib (STARTTLS + plain-auth only,
no external dep). Settings: `smtp_host`, `smtp_port`, `smtp_user`,
`smtp_pass` (secret), `smtp_from`, `smtp_to`. Same event pipeline as
the webhook; test button reuses `/api/notify/test`.
- [x] **Notifier presets** — refactor `notify.go` into a transport registry:
`webhook` (generic, current), `slack`, `discord`, `telegram`, `gotify`,
`ntfy`, `smtp`. Presets = field set + payload template per transport;
settings store per-transport config. UI: transport picker + fields.
- [x] **Alert rules** — per-monitor JSON `alerts` config: consecutive
failures before down (default 1), latency-warn ms, cert-days threshold
for domains, mute flag. Scheduler evaluates rules before firing events;
maintenance windows keep priority. Migration adds nullable
`alerts TEXT` on `monitors`/`domains`.
### P5c — Agent + domain depth
- [x] **Per-container metrics** — agent calls Docker `/containers/{id}/stats?stream=false`
(cpu%, mem used/limit) when docker.sock present; payload adds
`containers[].cpu`, `.memUsed`, `.memLimit` (JSON — no migration).
UI: containers table gets cpu/mem columns + optional sparkline.
- [x] **SMART / ZFS / GPU** — optional collectors, all best-effort and
silently skipped when tooling is absent:
SMART via `smartctl -j` exec if installed; ZFS via `zpool status`/`-j`
parse; GPU via `/sys/class/drm` (amd/intel basic) and `nvidia-smi -j`
when present. No new hard deps — exec-or-skip.
- [x] **Subdomain discovery** — crt.sh CT-log query + A/AAAA resolve for
discovered names; `subdomains` table (domain_id, name, ips, first_seen,
last_seen). Domain detail: subdomains tab with probe status. Daily
sweep alongside the domain refresh job.
### P5d — Structure (big, changes product shape)
- [x] **Auth (opt-in, email/username + password)** — deliberate constraint:
stays disabled by default; enabling is a settings toggle. First enable
creates the admin user. `users` table (id, email, name, passhash
bcrypt/argon2id, created_at), session cookie (httpOnly, signed,
SQLite-backed sessions or HMAC token), `POST /api/auth/login|logout`,
middleware gating all /api + non-public UI routes; public status pages
and badges stay open. No OIDC, no SSO — local accounts only.
Security pass required: rate-limit login, constant-time compare,
session rotation.
- [x] **Multiple boards** — `boards` table (id, name, slug, position),
`sections.board_id` FK (migration backfills all rows to board 1).
Board switcher in the header (dropdown + `/b/:slug` route), settings
for default board. Monitors/domains/systems pages stay global — only
the service board multiplies.
- [x] **Full i18n extraction** — every UI string moves into `en.ts` via the
existing `t()` seam; mechanical pass over pages/dialogs/widgets.
Second locale (cs) lands in the same pass to prove the seam end-to-end.
### Ordering rationale
P5a items are isolated and shippable individually. P5b builds on the existing
event pipeline. P5c grows the agent payload (backward-compatible JSON
additions — old servers ignore new fields). P5d is last because auth +
multi-board touch routing, middleware, and schema globally; doing them first
would force rework of everything above.
## Non-goals (post-1.0)
Postgres/external DB, multi-user/RBAC, mobile app, container lifecycle
management, YAML-as-source-of-truth, OIDC/SSO (local accounts only),
Beszel-protocol agent compatibility. Revisit only with evidence of demand.