- 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>
19 KiB
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.yamlstub → generated TS client (packages/api-client)- Backend skeleton: Gin router,
zaplogging,/api/healthz, SQLite open + goose migrations runner - Frontend skeleton: Vite + TS strict + Tailwind + shadcn/ui, theme tokens matching
d-bento-mono.htmlCSS 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.FSserves UI) - SPA fallback routing in embedded server (
/+ client-side paths →index.html,/apiisolated) docker-compose.yml— one service,./data:/datavolume
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(fractionalposition REALordering) - REST API: CRUD for sections/items/urls,
reorderendpoints, icon upload,settings,export/importJSON - Renderer contract: shared board store +
BoardView/SectionView/ItemView/WidgetViewseam, 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/downchip - Parallel probing: per-item
sync.WaitGroupfan-out, 4s client timeout - URL scheme allowlist (
http/httpsonly —file://etc. rejected on create + probe) - Post-drag click suppression (
lastDropAtwindow — 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:
Widgetinterface (Type(),Fetch(cfg)),GET /api/widgets/:id/datawith ~30s in-process TTL cache GET /api/widgets/types→ registry metadata drives the add-widget dialogWidgetHostcomponent: mapsconfig.type→ React component, polls with backoff- Widgets as grid items (same
itemstable,kind = 'widget',config JSON) - Clock/timezone widget (frontend-only)
- Pi-hole fetcher (blocked %, queries today) — per-day bar series deferred: v5
api.phplacks 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) localtype flag: frontend-only widgets (clock) register in/api/widgets/typeswithout 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
indexprop → 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-visiblemoved 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 viax/net/icmp— distroless has no ping binary),dns,keyword,json-query,push(caller-generated ingest URL) - API:
/api/monitorsCRUD,/:id/heartbeats?range=, pause/resume, check-now - Monitors page (status, uptime 24h/30d, ping, interval) + detail (response chart, heartbeat log)
- Board widget type
monitorbound 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 +extraJSON 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 viacrypto/tls; DNS (NS/MX/TXT/A/AAAA) vianet.Resolver; host geo via ip-api; provider detection via forkdetect/providers.go; favicon - Daily scheduler + manual refresh; field diffs recorded to
domain_history - Alerts:
alert_rules+notificationstables; dispatchers — generic webhook (JSON POST), SMTP vianet/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,latestJSON snapshot),system_stats(system_id, ts, payload JSON — one column, no sub-field queries; 7d retention prune) - Ingest:
POST /api/systems/ingestwith per-system bearer token; offline when silent > max(3×interval, 30s);system.up/system.downwebhook events cmd/dash-agent: Linux collector —/proc(cpu/mem/net/load/uptime),/syshwmon temps, statfs disk, docker.sock container list; POST every-interval(default 10s);-oncedebug 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 showmaintenancestate 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/metricsPrometheus exposition- Bulk:
POST /api/import/csv?kind=monitors|domainsheader-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, Dashyconf.yml→ sections/items/urls README.mdhero: screenshots,docker runone-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/— typedt()+endict; 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.urlwith optional headers, extractconfig.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/smtpstdlib (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.gointo 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
alertsconfig: 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 nullablealerts TEXTonmonitors/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 addscontainers[].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 -jexec if installed; ZFS viazpool status/-jparse; GPU via/sys/class/drm(amd/intel basic) andnvidia-smi -jwhen present. No new hard deps — exec-or-skip. - Subdomain discovery — crt.sh CT-log query + A/AAAA resolve for
discovered names;
subdomainstable (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.
userstable (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 —
boardstable (id, name, slug, position),sections.board_idFK (migration backfills all rows to board 1). Board switcher in the header (dropdown +/b/:slugroute), settings for default board. Monitors/domains/systems pages stay global — only the service board multiplies. - Full i18n extraction — every UI string moves into
en.tsvia the existingt()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.