mirror of
https://github.com/Dvorinka/Dash.git
synced 2026-10-28 15:51:27 +00:00
- 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>
296 lines
19 KiB
Markdown
296 lines
19 KiB
Markdown
# 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.
|