Files
Dash/ROADMAP.md
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

19 KiB
Raw Permalink Blame History

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.

  • Monorepo scaffold (apps/frontend, apps/backend, packages/api-client, infra/, data/)
  • MIT LICENSE, README.md, .gitignore, AGENTS.md
  • openapi.yaml stub → generated TS client (packages/api-client)
  • Backend skeleton: Gin router, zap logging, /api/healthz, SQLite open + goose migrations runner
  • Frontend skeleton: Vite + TS strict + Tailwind + shadcn/ui, theme tokens matching d-bento-mono.html CSS vars
  • CI (GitHub Actions): go build ./..., go vet ./..., go test ./..., tsc --noEmit, oxlint + vendored anti-slop rules, npm run build
  • Multi-stage Dockerfile (frontend build → Go build → distroless, embed.FS serves UI)
  • SPA fallback routing in embedded server (/ + client-side paths → index.html, /api isolated)
  • 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.

  • Schema: sections, items, urls, settings (fractional position REAL ordering)
  • REST API: CRUD for sections/items/urls, reorder endpoints, icon upload, settings, export/import JSON
  • Renderer contract: shared board store + BoardView/SectionView/ItemView/WidgetView seam, dnd + popover as shared primitives
  • Bento renderer (default): mixed-size grid, hairline section headers, tiles with status chips
  • Cards renderer: uniform compact grid — second renderer proves the seam while code is small
  • dnd-kit: reorder items in a section, move items between sections, reorder sections; persist on drop
  • Collapse/expand sections (persisted)
  • UrlPopover: 1 URL → direct open, 2+ → chooser (local/external/custom tags)
  • ServiceDialog — one dialog for add + edit (name, URLs+labels, icon URL or file upload)
  • Icon handling: upload to data/icons/, remote URL passthrough, letter-tile fallback
  • Dark/light toggle (persisted), renderer picker in settings, responsive to ~360px
  • Pre-paint theme bootstrap in index.html (no wrong-theme flash on load)
  • Status ping: server-side HEAD request per service URL, cached ~60s → up/down chip
  • Parallel probing: per-item sync.WaitGroup fan-out, 4s client timeout
  • URL scheme allowlist (http/https only — file:// etc. rejected on create + probe)
  • Post-drag click suppression (lastDropAt window — drops no longer fire tile activation)
  • Mobile polish: status chip collapses to dot-only below 620px
  • 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

  • Go widget registry: Widget interface (Type(), Fetch(cfg)), GET /api/widgets/:id/data with ~30s in-process TTL cache
  • GET /api/widgets/types → registry metadata drives the add-widget dialog
  • WidgetHost component: maps config.type → React component, polls with backoff
  • Widgets as grid items (same items table, kind = 'widget', config JSON)
  • Clock/timezone widget (frontend-only)
  • Pi-hole fetcher (blocked %, queries today) — per-day bar series deferred: v5 api.php lacks it, needs Pi-hole v6 /api/history
  • AdGuard Home fetcher (blocked %, queries, avg processing ms)
  • Immich fetcher (photo/video count, library size)
  • Per-widget config UI (endpoint URL, API key — stored in config, never logged)
  • local type flag: frontend-only widgets (clock) register in /api/widgets/types without a fetcher
  • Widget tile error/loading states (graceful upstream 502 rendering)
  • 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

  • Index renderer: serif section headers, full-width rows, masthead stat strip
  • Console renderer: bordered panels/tables, JetBrains Mono, [ up ] tags
  • Renderer switcher polished (2×2 radio grid with per-renderer descriptions)
  • ⌘K command palette: jump to service, add service, toggle theme, switch renderer (⌘K / Ctrl+K / /)
  • dashboard-icons auto-suggest in icon picker (name → slug → CDN URL chip + live preview)
  • Renderer contract index prop → row/section numbering without re-deriving position
  • Icon URL live preview in service dialog (dead URLs degrade to dashed tile)
  • Palette / shortcut + header search button for discoverability
  • Fonts: Instrument Serif + JetBrains Mono via fontsource (bundled, no CDN dep)
  • 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.

  • 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)
  • In-process scheduler: due-monitor sweep, bounded worker pool, heartbeat write, status transitions
  • 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)
  • API: /api/monitors CRUD, /:id/heartbeats?range=, pause/resume, check-now
  • Monitors page (status, uptime 24h/30d, ping, interval) + detail (response chart, heartbeat log)
  • Board widget type monitor bound to a monitor id
  • ServiceDialog checkbox: create matching http monitor from a service URL
  • 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.

  • Schema: domains (core columns + extra JSON for the long tail), domain_history (domain_id, change_type, field, old, new, at)
  • 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
  • Daily scheduler + manual refresh; field diffs recorded to domain_history
  • 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)
  • Domains page + detail (expiry countdown, registrar, SSL, DNS, subdomains)
  • Board widget domain (expiry countdown)
  • Subdomain discovery: port subdomain_discovery.go, opt-in per domain
  • 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.

  • 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)
  • Ingest: POST /api/systems/ingest with per-system bearer token; offline when silent > max(3×interval, 30s); system.up/system.down webhook events
  • 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
  • Agent packaging: systemd unit (packaging/dash-agent.service) + infra/Dockerfile.agent + goreleaser binaries (Phase 4)
  • Systems page (status cards, cpu/mem/disk bars, uptime) + detail (recharts: cpu%, mem%, net rx/tx, load; temps + containers tables)
  • Board widget system (cpu/mem/disk mini bars)
  • 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

  • Incidents: incidents + incident_updates; manual CRUD + auto-open on monitor down, auto-resolve on recovery; severity + status flow (open → ack → resolved → closed)
  • Maintenance windows: maintenance_windows (monitor_ids empty = all); suppress alerts + auto-incidents, status pages show maintenance state while active
  • 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
  • Badges: GET /api/badge/:kind/:id.svg — stateless SVG from live data (monitor status+uptime, domain days-left, system status)
  • GET /api/metrics Prometheus exposition
  • Bulk: POST /api/import/csv?kind=monitors|domains header-driven CSV; monitors/domains/systems rows folded into /api/export (v2, additive)
  • ⌘K palette + header nav wired for new pages

Phase 4 — Open-source launch → v1.0.0

  • Docs: install, configuration, widget development guide (docs/)
  • Importers: Homepage services.yml, Homarr JSON, Dashy conf.yml → sections/items/urls
  • README.md hero: screenshots, docker run one-liner (demo GIF deferred — pngs suffice for v1.0.0)
  • goreleaser → binaries; GHCR image on tag; semver + CHANGELOG.md
  • CONTRIBUTING.md, issue/PR templates, code of conduct, security policy
  • i18n scaffolding (src/i18n/ — typed t() + en dict; full string extraction deferred to first real locale)
  • 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)

  • 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.
  • iframe widget — config.url, sandboxed <iframe> tile in the widget registry; local type, no backend fetcher.
  • 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.
  • 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.
  • 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.
  • Demo GIF — scripted playwright walkthrough (board → monitors → status page) rendered via ffmpeg; committed to docs/screenshots/.

P5b — Alert depth

  • 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.
  • 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.
  • 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

  • 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.
  • 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.
  • 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)

  • 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.
  • 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.
  • 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.