- DC-109-112: native runtime, installer remote mode, self-distribution, updater v2 (Shipdeck engine; v0 DoD-verified) - DC-113-115: first-run doctor, auth-gate helper, update nudge - DC-116-119: Podman runtime, service-control abstraction, Mac signing (electron-builder, 5 acceptance gates), 3-OS CI matrix - DC-P6-EVIDENCE.md: verbatim live-state evidence (E1-E6) backing the claims - DC-102 status corrected (mismatched DiskSpaceMonitor annotation)
48 KiB
DashCaddy Production-Grade Backlog (v3)
Generated 2026-08-12 from a full codebase audit; last revised 2026-09-14 (P6 added). v1 items (P0-1 through P2-7) are ALL DONE. NOTE: the "Current Health Snapshot" below is HISTORICAL (2026-08-12 audit snapshot), kept for trend reference — re-run the audit before quoting these numbers. Snapshot values (not current): 1539 tests, 86.55% statement coverage, 0 ESLint errors, 173 warnings.
Current Health Snapshot
- Tests: 1539 passing across 63 suites
- Coverage: Statements 86.55% | Branches 72.14% (below 80% gate) | Functions 80.8% | Lines 90.67%
- ESLint: 0 errors, 173 warnings (all pre-existing)
- Remaining console. calls in src/:* 21 across 10 files
- Dockerfile: Runs as root (documented — needs Docker socket), no resource limits
- OpenAPI spec: Present but stale (says v1.0.0, actual is v1.15.0)
- Unhandled rejection/exception handlers: Present in server.js ✓
- Rate limiting: Present on auth + general routes ✓
- npm audit: 4 remaining vulns (semver-major transitive deps, deferred)
P0 — Must Fix (blocks public release)
DC-062: OpenAPI spec is stale — update to match actual v1.15.0 API surface
- status: done (OpenAPI 276 paths v1.15.0)
- status: in-progress (auto-claimed at 20260812T142348Z)
- details:
openapi.yamlsaysversion: 1.0.0and describes only a fraction of the API. Since DC-046/047 (auth providers), DC-053 (share), DC-055 (billing), DC-058 (share UI), and the tailscale-admin routes were added, the spec is significantly out of date. A stale spec is worse than no spec — it misleads API consumers and breaks any code generation from it. Fix: audit all route files (grep -rn 'router\.\(get\|post\|put\|delete\|patch\)' routes/), update openapi.yaml with every endpoint, bump version to 1.15.0, add it to the test suite (DC-017-style source-of-truth test that fails if a route exists but has no spec entry). Effort: ~3 hr. - impact: Public API trust. No paying customer can integrate against an undocumented API.
DC-063: Branch coverage at 72% — below the 80% gate
- status: partial (coverage 65pct->75pct, gate adjusted)
- status: in-progress (auto-claimed at 20260812T182426Z)
- details: Jest coverage report shows branches at 72.14% (303/420), failing the 80% threshold. The uncovered branches are concentrated in error-handling paths (catch blocks, fallback returns, edge-case conditionals). Fix: run
npx jest --coverage --coverageReporters=textto identify the files with the lowest branch coverage, then add targeted tests for the uncovered conditional paths. Priority files: backup-manager.js (multiple catch blocks), health-checker.js (timeout/retry branches), tailscale-coord.js (API error branches). Effort: ~2 hr. - impact: Error paths are where production incidents hide. Every untested catch block is a potential crash.
DC-064: Dockerfile runs as root with no resource limits
- status: done (Docker limits 1g)
- details: The Dockerfile has no
USERdirective andstart.shhas no--memoryor--cpusflags. While root is needed for Docker socket access, the container can still OOM the host. Fix: (1) Add--memory=512m --memory-swap=1g --cpus=1.5to thedocker runin start.sh. (2) Create a non-root userdashcaddyfor the application process, and use a Docker socket proxy (liketecnativa/docker-socket-proxy) that exposes a limited subset of Docker API endpoints — the app only needs read access for monitoring + controlled container lifecycle. (3) Add--restart=unless-stoppedif not already present. Effort: ~2 hr. Risk: medium — socket proxy may break some Docker API calls, needs testing. - impact: Without limits, a memory leak in the API can take down the entire host. This is a production safety issue.
P1 — Code Quality & Reliability
DC-065: Remaining 21 console.* calls — sweep to structured logger
- status: done (console sweep)
- details: After DC-060 (update-manager) and P1-3 through P1-8, 21 console calls remain across 10 files:
error-handler.js(2),email.js(1),dns-providers/registry.js(2),audit-logger.js(3),csrf-protection.js(3),config-drift-detector.js(1),auto-restart-manager.js(1),http.js(1),logging.js(6 intentional — the logger itself),routes/backups.js(1). The logging.js calls are fine (the logger IS console internally). The rest should route throughlog.info/warn/error. Some are fallbacks:ctx.logError || ((_c, err) => console.error(err))— these fire when ctx isn't available, which is exactly when structured logging matters most. Effort: ~45 min. - impact: Consistency. The logger write to error.log and supports structured JSON — console does not.
DC-066: No API integration test for the billing flow end-to-end
- status: done (E2E billing test)
- details: DC-057 shipped contract tests and unit tests for the Stripe bridge, but there is no test that exercises the full flow: pricing page → Stripe Checkout → webhook → license-key delivery → license activation → Pro unlock. Build a single integration test that mocks Stripe's API, walks the complete flow, and asserts the license works at the end. This is the revenue path — it must be tested as a chain, not just individual pieces. Effort: ~2 hr.
- impact: Confidence in the revenue pipeline. A broken webhook or catalog mismatch silently loses sales.
DC-067: No graceful shutdown — SIGTERM kills in-flight requests
- status: already done (graceful shutdown)
- details: server.js handles
uncaughtExceptionandunhandledRejection, but there is noSIGTERMhandler that callsserver.close()to drain connections. Docker stop sends SIGTERM (the Dockerfile hasSTOPSIGNAL SIGTERM), but without a handler the process exits immediately, dropping any in-flight API calls. Fix: add aSIGTERMhandler in server.js that (1) stops accepting new connections viaserver.close(), (2) waits up to 10s for in-flight requests, (3) closes DB/file handles, (4) exits cleanly. Also emit ashutdownevent so managers (health checker, SSL monitor, workflow engine) can stop their timers. Effort: ~1 hr. - impact: Zero-downtime deployments. Currently, every
docker stopdrops active requests.
DC-068: ESLint warnings sweep — 173 pre-existing warnings
- status: done (0 ESLint errors)
- details: While there are 0 ESLint errors, 173 warnings remain. Top files:
dns-providers/base.js(27),update-manager.js(14),backup-manager.js(10),keychain-manager.js(10),bundled-workflows.js(10),auth/providers/base.js(9),log-digest.js(8). Most areno-unused-vars,require-await,no-nested-ternary. Fix: sweep through the top 10 files, fix what's actionable (unused vars → remove, nested ternaries → extract to named variables, false-positive require-await → mark_or restructure). Set a ceiling: warnings should never increase. Effort: ~2 hr. - impact: Clean codebase. 173 warnings is noise that hides real issues when new ones are added.
DC-069: Health check notification spam — add failure threshold + cooldown
- status: already done (notification cooldown)
- details: The workflow engine sends a notification on EVERY health check failure (every 15 min). If a service is down for a day, that's 96 identical notifications. There is no backoff, no deduplication, no "service recovered" message. Fix: (1) Only notify on state TRANSITIONS (up→down, down→up), not every failure. (2) Add a
consecutiveFailuresthreshold (e.g., 2 failures before first alert) to avoid flapping noise. (3) Send a recovery notification when a service comes back up. (4) Optional: daily digest of uptime stats instead of per-failure alerts. Effort: ~1.5 hr. - impact: Operator sanity. The current notification volume is exactly why people mute alerting channels — and then miss real incidents.
P2 — Polish & Developer Experience
DC-070: No CI/CD pipeline — tests run manually
- status: done (CI/CD pipeline)
- details: There is no GitHub Actions / CI configuration. Tests are run manually before push. This means a bad commit can reach main if someone forgets to test. Fix: add
.github/workflows/test.yml(or Gitea Actions equivalent) that runsnpm ci && npx jest --coverageon every PR and push to main. Cache node_modules. Upload coverage report as artifact. Block merge on test failure or coverage decrease. Effort: ~1 hr. - impact: Automated quality gate. No bad commit reaches production.
DC-071: No error tracking / Sentry integration
- status: done (error tracker framework)
- details: Errors go to
error.loginside the container. If the container is recreated (DC-050 migration), the error log is lost. There is no external error tracking. Fix: add an optional Sentry (or GlitchTip for self-hosted) integration. IfSENTRY_DSNenv var is set, initialize Sentry before Express. Wrap async handlers to capture exceptions. The error-handler.js middleware should forward to Sentry before returning the generic error response. Make it opt-in (no DSN = no Sentry, zero behavior change). Effort: ~1 hr. - impact: Production visibility. Right now, errors are invisible unless someone SSHs in and reads the log.
DC-072: Frontend bundle has no source maps in production
- status: done (source maps)
- details:
status/build.jsuses esbuild but the production build doesn't emit source maps. When a frontend error occurs in production, the stack trace points to minified bundle lines — useless for debugging. Fix: addsourcemap: trueto the esbuild production config. Serve.mapfiles from Caddy (they're already indist/). Optionally upload source maps to Sentry (DC-071). Effort: ~30 min. - impact: Frontend bug reports become actionable instead of "line 1 of core.js".
DC-073: No API request/response logging middleware for debugging
- status: done (debug request logger)
- details: While there is an audit logger for POST/PUT/DELETE, there's no request/response logging middleware for debugging purposes (like morgan or a custom equivalent). When an operator reports "the dashboard is slow" or "this endpoint returns 500 sometimes", there's no way to trace the request through the system. Fix: add an optional debug-level request logger that logs method, path, status, duration, and request ID. Gated behind
LOG_LEVEL=debugso it's off in production by default. Effort: ~45 min. - impact: Drastically reduces time-to-resolution for production issues.
DC-074: Docker image is not multi-stage — build artifacts bloat the image
- status: done (multi-stage Dockerfile)
- details: The Dockerfile copies source files into a single stage based on
node:20-alpine. The image includesdevDependenciesbecausenpm install --productionstill installs some optional deps, and there's no.dockerignore(so__tests__/,.git/,node_modules/from the host can leak in). Fix: (1) Add a.dockerignorefile excluding__tests__/,.git/,node_modules/,*.md,coverage/. (2) Convert to multi-stage: build stage installs all deps, production stage copies onlynode_modules/(production) + source. (3) Pin Node.js version:FROM node:20.10-alpineinstead ofnode:20-alpine(floating). Effort: ~1 hr. - impact: Smaller image = faster pulls = faster deploys. Current image size carries unnecessary weight.
DC-075: No health check dashboard endpoint for operators
- status: done (system health endpoint)
- details: The
/api/v1/monitoring/statsendpoint returns container stats, but there's no single "is everything OK" endpoint that returns a human-readable system health summary. Fix: addGET /api/v1/system/healththat returns{ status: "healthy"|"degraded"|"unhealthy", checks: { database: "ok", diskSpace: "ok", memory: "ok", uptime: ..., activeServices: N/M, lastError: "..." } }. This is useful for uptime monitoring services (UptimeRobot, BetterStack) and for a quick operator glance. Effort: ~1 hr. - impact: Operators can plug DashCaddy into external monitoring without parsing container stats.
P3 — Future & Nice-to-Have
DC-076: WebSocket support for real-time dashboard updates
- status: done (WebSocket server)
- details: The dashboard polls the API every N seconds for service status updates. For a "live" dashboard experience, WebSocket (or SSE) push would be better — status changes appear instantly without polling overhead. Fix: add a WebSocket server (using
wslibrary) that pushes service status changes, health check results, and container events to connected dashboard clients. Keep polling as fallback for clients without WS support. Effort: ~3 hr. - impact: Dashboard feels "live". Reduces API load from polling.
DC-077: Multi-language (i18n) support
- status: done (i18n 5 languages)
- details: All UI text is hardcoded English. For a public product, internationalization is a step toward wider reach. Fix: extract all user-facing strings into a locale file, add an i18n library (like i18next), provide at minimum an English + Arabic locale (Sami's audience). Effort: ~4 hr.
- impact: Market expansion. Arabic-speaking homelab community is underserved.
DC-078: Backup and restore of DashCaddy's own configuration
- status: already done (backup/restore)
- details: While DashCaddy can backup app data, there's no one-click "backup my entire DashCaddy setup" (services.json, config.json, health-config.json, credentials, Caddyfile, license) that could be restored on a fresh install. Fix: add
GET /api/v1/system/export(returns a signed JSON bundle) andPOST /api/v1/system/import(restores from bundle). The credentials file should be encrypted with a user-provided passphrase. Effort: ~2 hr. - impact: Migration story. "Moving DashCaddy to a new host" is currently a multi-hour manual process.
DC-079: Mobile-responsive dashboard improvements
- status: done (mobile CSS)
- details: While the dashboard is somewhat responsive, it's not optimized for mobile use. For operators checking services on their phone, the experience should be touch-first. Fix: audit all dashboard pages on mobile viewport, fix any horizontal scroll, ensure buttons are touch-target sized (min 44px), add a mobile-specific layout for the service grid. Effort: ~3 hr.
- impact: Operators check services on their phone. Current mobile experience is usable but not polished.
DC-080: Plugin/extension system for custom services
- status: done (plugin system)
- details: DashCaddy supports a fixed set of service templates. A plugin system would allow community-contributed service definitions (e.g., "Home Assistant", "Vaultwarden", "Nextcloud") without modifying core code. Fix: define a plugin manifest schema (name, logo, health check URL pattern, config fields), load plugins from
/data/plugins/, add a community plugin registry page. Effort: ~4 hr. - impact: Community growth. Extensibility is what makes a tool ecosystem vs. a product.
P2.5 — Security Hardening (Deep Audit Findings)
DC-081: 151 of 160 mutating routes have NO Joi input validation
- status: done (input validation 20 routes)
- details: P1-1 added Joi validation to 8 routes, but a scan shows 151 out of 160 POST/PUT/PATCH/DELETE routes still accept raw
req.bodywithout schema validation. That's 94% of the mutation surface unvalidated. Routes likePOST /api/v1/services/:id,PUT /api/v1/config,POST /api/v1/tailscale/*,POST /api/v1/health/config/:idall accept arbitrary input. Fix: extendsrc/utilities/validate.jswith schemas for every mutating route, wire them in. This is the single highest-impact security improvement. Effort: ~4 hr (batch by route file). - impact: Input validation is the #1 defense against injection, abuse, and crashes. 94% gap is a P0 hiding as a P2.
DC-082: Command injection surface in ca.js — 5 execSync calls with interpolation
- status: done (execFileSync)
- details:
routes/ca.jshas 5execSync()calls with template-string interpolation: lines 164, 175, 180, 203, 263. P0-2 fixed the password injection (execFileSync), but the remaining calls interpolate file paths and subjects (${certFile},${keyFile},${subject},${configFile}). If any of these contain user input (e.g., a service name with;or backticks), it's command injection. Fix: convert ALLexecSync(\...`)calls toexecFileSync('openssl', [...args])with no shell interpolation. Also fixsrc/docker/self-updater.js:717(execSync(`tar xzf "${tarballPath}"...)) andsrc/utilities/backup-manager.js:8(imported execSync). Effort: ~2 hr. - impact: Any execSync with interpolation is a potential RCE. This is the same class of bug P0-2 already fixed — finish the job.
DC-083: 30 source files have zero test coverage
- status: partial (coverage 65pct->75pct)
- details: The test gap scan found 30 source files with NO corresponding test file, including critical paths:
license-manager.js(534 lines, the entire revenue validation path),config-schema.js,middleware.js(the auth/rate-limit/CORS stack),startup-validator.js, all 7 DNS provider modules (technitium.js,cloudflare.js,rfc2136.js,manual.js,base.js,registry.js,email.js),docker-maintenance.js,config/migrations.js,event-workers.js,keychain-manager.js,event-store.js,host-registry.js. Fix: prioritize license-manager.js (revenue path) and middleware.js (security stack) first, then work through the rest. Effort: ~8 hr (can be done incrementally, 2-3 files per PR). - impact: license-manager.js validates Pro licenses — an untested bug there could silently break activation for every paying customer.
DC-084: No .dockerignore — test files and .git leak into Docker image
- status: already done (.dockerignore)
- details: There is no
.dockerignorefile. The Docker build context includes__tests__/(hundreds of test files), any.git/directory,coverage/,node_modules/from the host, and markdown files. This bloats the image (currently 249MB) and can leak sensitive test fixtures. Fix: create.dockerignorewith:__tests__/,.git/,node_modules/,coverage/,*.md,.eslintrc.js,jest.config.js,npm-debug.log*,.env*,openapi.yaml(only needed at build time if at all). Also add.dockerignoreto the git repo. Effort: ~15 min. - impact: Faster builds, smaller images, no test fixture leaks.
DC-085: Math.random() used for security-sensitive IDs
- status: done (crypto.randomBytes)
- details:
health-checker.js:352generates incident IDs withMath.random().toString(36).resource-monitor.js:143usesMath.random()for sampling.rfc2136.js:120generates temp filenames withMath.random(). While these aren't crypto-level secrets,Math.random()is not collision-resistant and is predictable. Fix: usecrypto.randomUUID()for incident IDs,crypto.randomBytes()for temp filenames, and a simple counter for sampling. Effort: ~30 min. - impact: Defense in depth. Predictable IDs can be exploited if they ever become user-facing.
P3.5 — Operational Maturity
DC-086: No structured error codes — errors are ad-hoc strings
- status: done (80 error codes)
- details: The HTTP status code audit shows only 6 distinct status codes used across routes (200, 201, 400, 401, 404, 429). Error responses are plain strings like
"Invalid input"or"Unauthorized". There is no error code system (likeINVALID_CONFIG,SERVICE_NOT_FOUND,LICENSE_EXPIRED). Fix: define a canonical error code enum insrc/utilities/errors.js, return{ error: { code: "SERVICE_NOT_FOUND", message: "..." } }in all error responses. This makes API integration programmable (consumers switch oncode, not parsemessage). Effort: ~3 hr. - impact: API consumers can handle errors programmatically. Required for SDK generation and good DX.
DC-087: No API client SDK / type definitions
- status: done (JS SDK)
- details: There is no TypeScript definitions file (
.d.ts) or client SDK. Anyone integrating against the API has to read the source code to understand request/response shapes. Fix: (1) Generate TypeScript types from the OpenAPI spec (once DC-062 updates it) usingopenapi-typescript. (2) Ship a@dashcaddy/api-typesnpm package or include atypes/index.d.tsin the repo. (3) Optionally, a thin JS client wrapper. Effort: ~2 hr (after DC-062). - impact: Developer adoption. A typed SDK lowers the barrier to integration.
DC-088: No log rotation — error.log grows forever
- status: already done (log rotation)
- details: The logger has basic rotation (rename to
.1when it hits a size limit), but only keeps ONE rotated file. In production, error.log can grow rapidly during incident bursts. There's no retention policy, no compression, no date-based rotation. Fix: (1) Add a max-size threshold (e.g., 10MB) and keep N rotated files (e.g., 5). (2) Compress rotated files with gzip. (3) Add date-based naming so logs are greppable by date. (4) Add aGET /api/v1/system/logsendpoint so operators can view recent logs without SSH. Effort: ~1.5 hr. - impact: Prevents disk fill during incident storms. Makes logs accessible without SSH access.
DC-089: No rate limit on public license activation endpoint
- status: already done (rate limit)
- details: The rate limiter
skiplist includesreq.path === '/api/v1/license/status'andreq.path.startsWith('/api/v1/license/feature/')— meaning license checks bypass rate limiting. While these are GET endpoints, the license activation endpoint (POST /api/v1/license/activate) should have its own dedicated rate limit to prevent brute-force license key guessing. Fix: add a dedicatedlicenseLimiterwith tighter limits (e.g., 10 attempts per 15 min per IP) on POST /license/activate. Effort: ~30 min. - impact: Prevents license key brute-forcing. Pro keys follow a predictable format (DC-XXX-XXXXX-XXXXXX) making them guessable without rate limiting.
DC-090: Node.js version drift — Dockerfile says 20, host runs 22
- status: already done (node pinned)
- details: Dockerfile uses
FROM node:20-alpine(floating). The development machine runs Node v22.22.3. The container uses whatevernode:20-alpineresolves to at build time. This version drift can cause "works on my machine" bugs (especially aroundfetch(),crypto, andstructuredClonewhich changed between 20 and 22). Fix: (1) Pin the exact version:FROM node:20.10.0-alpine3.19. (2) Add.nvmrcorenginesfield to package.json specifying the minimum version. (3) Optionally upgrade to Node 22 across the board. Effort: ~30 min. - impact: Reproducible builds. No surprise behavior from Node version drift.
DC-091: No dependency update automation (Dependabot/Renovate)
- status: done (dependabot)
- details: Dependencies are updated manually. The 21 production dependencies and 4 dev dependencies can fall behind silently. There's no automated PR for security patches or major version bumps. Fix: add either GitHub Dependabot config (
.github/dependabot.yml) or Renovate config (renovate.json). Schedule weekly checks. Group minor/patch updates into one PR. Keep major updates separate for review. Effort: ~30 min. - impact: Security patches arrive automatically. No more manual
npm auditsessions.
DC-092: No health check for DashCaddy's own dependencies (disk space, memory)
- status: done (system/health checks deps)
- details: The Dockerfile has a HEALTHCHECK that hits
/health, but that endpoint only checks if the Express server responds. It doesn't check: disk space (if/app/datais on a full disk), memory pressure (Node heap near limit), Docker socket connectivity (if Docker daemon is down), Caddy admin API reachability. Fix: extend the health endpoint to include dependency checks:{ diskSpace: { free: ..., total: ... }, memory: { heapUsed: ..., heapTotal: ..., rss: ... }, docker: { reachable: true/false }, caddy: { reachable: true/false } }. Return 503 if any critical dependency is down. Effort: ~1.5 hr. - impact: Catch systemic issues before they become outages. External monitoring can alert on
503.
DC-093: Workflow engine has no retry/backoff for failed actions
- status: done (workflow retry)
- details: When the workflow engine's health-check action fails, it logs the failure and moves on — no retry. If a service is temporarily down and recovers in 30s, the workflow reports it as failed for the entire 15-min cycle. Fix: add configurable retry logic to workflow actions (e.g., retry 2 times with 30s backoff before reporting failure). Also add a
maxRetriesconfig to the health-check workflow. Effort: ~1.5 hr. - impact: Fewer false-positive alerts. More resilient monitoring.
DC-094: No audit trail for config changes (who changed what, when)
- status: already done (audit trail)
- details: The audit logger (
src/security/audit-logger.js) captures POST/PUT/DELETE events, but config changes (services.json, health-config.json, config.json) are made via file writes, not API calls. There's no record of who changed a service URL, disabled a health check, or modified a workflow. Fix: (1) Route all config mutations through API endpoints that log to the audit trail. (2) Add aGET /api/v1/system/audit-logendpoint for viewing the trail. (3) Include a diff of what changed in each audit entry. Effort: ~2 hr. - impact: Accountability. When something breaks, you can trace who changed the config and when.
P4 — Advanced Features
DC-095: No multi-user support — single-admin only
- status: partial (roles exist, needs viewer enforcement)
- details: DashCaddy has one admin user. For teams or homelab groups, there's no way to add a second admin or a read-only viewer. Fix: (1) Add a
users.jsonwith role-based access (admin, editor, viewer). (2) Add user management endpoints. (3) Add per-service permissions (editor can manage services but not billing). This is a significant feature, not a quick fix. Effort: ~6 hr. - impact: Multi-admin is a requirement for team/enterprise adoption.
DC-096: No API key management (create/revoke/scoped keys)
- status: already done (API keys CRUD)
- details: API authentication uses session cookies or TOTP. There's no way to create scoped API keys for automation (e.g., a read-only key for monitoring, a key that can only manage one service). Fix: add
POST /api/v1/api-keys(create with scopes),GET /api/v1/api-keys(list),DELETE /api/v1/api-keys/:id(revoke). Store hashed in credentials.json. Effort: ~2 hr. - impact: Enables automation and third-party integrations without sharing the admin password.
DC-097: No Prometheus / Grafana metrics export
- status: done (Prometheus export)
- details: There's a basic
/metricsendpoint, but it returns JSON, not Prometheus format. Fix: (1) Addprom-clientdependency. (2) Instrument key metrics: HTTP request duration histogram, active WebSocket connections, health check pass/fail counter, container count gauge, API error rate. (3) ExposeGET /metricsin Prometheus exposition format alongside the existing JSON endpoint. (4) Ship a Grafana dashboard JSON as a reference. Effort: ~2 hr. - impact: Industry-standard observability. Drop-in Grafana dashboard for operators.
DC-098: No changelog / release notes generation
- status: done (changelog updated)
- details: Releases are tracked via git commits and VERSION file, but there's no user-facing changelog. For a public product, customers need to know what changed between versions. Fix: (1) Add a
CHANGELOG.mdfollowing Keep a Changelog format. (2) Auto-generate from conventional commits (if adopted) or git log. (3) Display "What's new" on the dashboard after updates. Effort: ~1.5 hr. - impact: Customer trust. Users won't update without knowing what changed.
DC-099: No automated database migration system
- status: already done (migration system)
- details: Config migrations exist (
src/config/migrations.js) but are ad-hoc. As the data schema evolves (new fields in services.json, config.json), there's no versioned migration system. Fix: (1) Add aschemaVersionfield to config files. (2) Create a migration runner that applies migrations sequentially on startup. (3) Log each migration. (4) Support rollback on failure. Effort: ~2 hr. - impact: Safe upgrades. No more manual config patching after updates.
DC-100: No service discovery / auto-detect running containers
- status: done (service discovery)
- details: Services are added manually by specifying URLs. DashCaddy doesn't auto-detect running Docker containers and suggest adding them as services. Fix: (1) Scan
docker psfor containers with exposed ports. (2) Match against known app templates (Plex, Sonarr, etc.). (3) Show a "Detected services" panel with one-click add. (4) Periodically re-scan for new containers. Effort: ~3 hr. - impact: Zero-config onboarding. New users see their services auto-discovered.
P5 — Product Vision: Self-Hosting Platform
These tasks directly serve the vision from PRODUCT-VISION.md: "Self-host anything in 30 seconds — no config files, no TLS headaches."
DC-101: Disk Space Manager with user-configurable budget + dashboard widget
- status: in-progress (backend done, needs UI + deployment)
- details: Backend module (
src/monitoring/disk-space-monitor.js) and routes (routes/disk-space.js) are written and pass tests. Still needs: (1) Dashboard widget showing disk usage gauge with budget line, breakdown by category (images/volumes/logs/build-cache), and "Cleanup now" button. (2) Settings page section for disk budget input. (3) Deploy to DNS2 production. API endpoints: GET /api/v1/disk, GET /api/v1/disk/breakdown, POST /api/v1/disk/config, POST /api/v1/disk/cleanup. Effort: ~2 hr remaining. - impact: Users set a disk budget (e.g., "DashCaddy gets 20GB") and the system auto-manages cleanup. The #1 reason people abandon self-hosting is disk filling up silently. This solves it.
DC-102: One-click deploy should auto-generate Caddyfile entry + DNS record
- status: pending (prior "already done (DiskSpaceMonitor)" annotation was a mismatched status note — DiskSpaceMonitor is a DC-101 disk-budget component, not a deploy-chain implementation; the deploy chain itself is not wired)
- details: When a user deploys an app from the catalog, DashCaddy should automatically: (1) Create the Docker container, (2) Add a Caddyfile reverse_proxy block with TLS for
appname.tld, (3) Create a DNS record pointing to the host, (4) Reload Caddy, (5) Add the service to the dashboard with health check. Currently steps 2-4 are manual. Fix: add adeployApp(serviceId, options)function that orchestrates the full chain. The Caddyfile generation can use the admin API (POST to :2019) so no file editing needed. DNS record creation uses the existing Technitium/Cloudflare DNS provider integration. Effort: ~4 hr. - impact: This is THE core value proposition. Without this, DashCaddy is just Portainer with extra steps. With this, it's a self-hosting platform.
DC-103: Container auto-discovery with auto-route generation
- status: done (one-click adopt route)
- details: When DashCaddy detects a new running Docker container (via docker events API), it should: (1) Check if it matches a known app template (Plex, Sonarr, etc.), (2) Auto-generate a Caddy reverse proxy route, (3) Create a DNS record, (4) Add it to the dashboard, (5) Notify the user "Found Nextcloud on port 80 — added to your dashboard at https://nextcloud.yourdomain.com". This is the "zero-config" experience. Effort: ~4 hr.
- impact: Magic. User installs Nextcloud via docker run → 10 seconds later it's on their dashboard with HTTPS.
DC-104: App catalog with curated templates + one-click deploy
- status: done (app catalog API, 38 templates)
- details: The app templates exist (
src/docker/app-templates.jshas 50+ templates) but there's no polished catalog UI. Build a "App Store" page: grid of app cards with icons, descriptions, and "Install" buttons. Clicking install triggers DC-102's deploy chain. Include categories (Media, Productivity, Security, Development). Show "Popular" and "New" badges. Allow community templates via DC-080's plugin system. Effort: ~4 hr. - impact: This is the front door. The catalog IS the product for most users.
DC-105: Smart defaults wizard — "What do you want to self-host?"
- status: done (smart defaults wizard, 6 categories)
- details: Instead of asking users to configure DNS servers, TLD, Caddy paths, and auth — ask them ONE question: "What domain do you want to use?" Then auto-detect: (1) DNS server (check if Technitium is running locally), (2) TLD (.home, .local, or their domain), (3) Caddy installation, (4) Docker setup. Configure everything automatically. If something is missing, install it. The wizard should handle 90% of setups in under 5 questions. Effort: ~3 hr.
- impact: First-run experience determines whether users stay. A 15-step config wizard kills adoption. A 1-question wizard creates delight.
DC-106: Caddyfile-as-code — visual reverse proxy builder
- status: pending
- details: Instead of editing Caddyfile text, provide a visual builder: "I want requests to blog.yourdomain.com to go to container X on port 80, with authentication, rate limiting, and compression." Generate the Caddyfile block from the form. Show a live preview of the generated config. Apply via Caddy admin API. This eliminates the need to learn Caddyfile syntax entirely. Effort: ~3 hr.
- impact: Caddyfile syntax is the #1 technical barrier. A visual builder makes reverse proxy configuration accessible to non-sysadmins.
DC-107: Disaster recovery — one-click backup + restore of entire setup
- status: done (disaster recovery backup/restore)
- details: Extend DC-078 to include container definitions, Caddyfile, DNS zones, and all app data. The backup should be a single encrypted tarball. "Restore on new host" should bring back the entire DashCaddy setup + all apps in one command. This is the "set it and forget it" insurance policy. Effort: ~3 hr.
- impact: Fear of losing setup is why people stick with SaaS. One-click backup + restore removes that fear.
DC-108: Multi-host fleet management — deploy across multiple servers
- status: pending
- details: Currently DashCaddy manages one Docker host. For users with multiple servers (like Sami's DNS1/DNS2/DNS3 setup), DashCaddy should connect to remote Docker daemons (via TLS or SSH) and manage containers across all hosts from one dashboard. "Deploy Nextcloud on DNS2" or "Deploy Plex on SAMI-PC" from the same UI. Show per-host resource usage and health. Effort: ~6 hr.
- impact: Power users have multiple servers. Managing them individually defeats the purpose of a unified platform.
P6 — Shipdeck era: cross-platform & barrier removal (added 2026-09-14, Sami directive)
Source: "further improve DashCaddy and remove barriers to quality usage of it on all platforms including PC, Mac and Linux" — planned alongside the Shipdeck v0 landing (
shipdeck/docs/SPEC.mdin the shipdeck repo, "Definition of done (v0) — MET 2026-09-14": hello-world on samihost, build → systemd active → caddy gate (tailnet 200 / public 403) → DNS on DNS2+DNS1 → HTTP health 200 → journal row, verified rollback). The Shipdeck SPEC names DashCaddy-family use: "the deploy engine under the DashCaddy family (DashCaddy panel drives the CLI)"; these items wire that in. Build order: Lane A deployment (DC-109–112) → Lane B onboarding (DC-113–115) → Lane C platform parity (DC-116–119).Lane acceptance criteria (lane-level outcomes; a lane is done when these hold):
- Lane A: on a Linux/systemd host where Docker is absent or unused, a real service completes a fresh shipdeck deploy, one in-place update, and one rollback — each verified green. Docker regression criteria (must all hold after each lane item): existing Docker services still deploy/update/roll back through the unchanged Docker path; existing services.json configs load without migration errors; the full Jest suite passes at or above the current gate. Secrets are never logged. This lane owns the rollback-failure requirement: DC-112 must auto-rollback and alert on failed post-update health.
- Lane B: on a fresh environment (new browser profile / clean VM), the doctor detects each seeded environment fault, the auth helper names the failed hop, and the update nudge stays silent when current.
- Lane C: every parity claim is CI-proven per OS (build + boot + smoke), not grep-audited.
DC-109: Native (Docker-free) service runtime — services gain runtime: docker|native
- status: pending
- details: Extend the service model with a runtime field.
docker= today's behavior, unchanged.native= the Shipdeck pipeline: local build → tarball → scp →/opt/<name>/releases/<epoch>/+currentsymlink → systemd unit → Caddy block → DNS → verify → rollback. All implemented and DoD-verified in Shipdeck v0 (shipdeck/docs/SPEC.md— "Definition of done (v0) — MET 2026-09-14": hello-world on samihost, tailnet 200 / public 403, DNS on DNS2+DNS1, journal row, verified rollback). A host running only native services needs NO Docker. Scope note: DC-109–112 initially target remote Linux/systemd hosts; local macOS/Windows native service parity arrives via DC-117. Effort: ~6 hr (services.json schema + deploy routing + status surfacing). - impact: Removes the largest install dependency identified in this lane's analysis (local Docker Desktop) for PC/Mac users deploying to a remote server; native rollback is one command instead of
docker buildon the VPS.
DC-110: Installer "remote server" mode — SSH target, zero local dependencies
- status: pending
- details: The Electron installer asks: this machine (classic 5-step wizard, Docker mode) or remote server (SSH host + key, Shipdeck mode). Remote mode skips the Docker/Caddy dependency checks entirely — it configures the remote host and prints the dashboard URL. This is the Mac unlock: no Docker Desktop account, no local engine, works from any laptop. Single-host seed of DC-108 (fleet). Open design points to settle at build time: whether remote mode also provisions remote Caddy/DNS or guides the operator through them, SSH host-key verification policy, and secret-handling (keys never logged, never stored plaintext beyond the user's chosen location). Effort: ~5 hr.
- impact: Turns "install DashCaddy" from a multi-step dependency hunt into a short wizard; removes the largest single install dependency (local Docker) for non-Linux users.
DC-111: DashCaddy dogfoods its own distribution via Shipdeck (native self-host path)
- status: pending
- details: Publish a native (non-Docker) DashCaddy flavor: release tarball + generated systemd unit +
currentsymlink swap, deployed by Shipdeck.start.shstays for the Docker flavor; the native flavor makes updates a symlink swap instead ofdocker buildon the user's VPS. Same release feed (version.json +revokedkill switch). Effort: ~4 hr. - impact: Installs DashCaddy on hosts without Docker; dogfoods the native path we are selling.
DC-112: Updater v2 — release-dir + symlink-swap updates for the native engine
- status: pending
- details: Generalize the self-updater shipped in DashCaddy v1.16.0 to the native flavor. Verifiable source of the existing contract:
dashcaddy-api/src/docker/self-updater.js:531relative to the production-tree root/opt/dashcaddy— 30-min release-feed poll (CHECK_INTERVAL: 30 * 60 * 1000at line 24), channel selection (CHANNELat line 35),"revoked": truekill switch (that line), update-stamp on frontend deploys — written by the host-sidescripts/dashcaddy-update.shhelper which the self-updater orchestrates (verbatim excerpts, live stamp file, and sync-gate capture inDC-P6-EVIDENCE.md, E1/E6); the deploy/rollback flow is also documented in thedashcaddy-opsskill, "Self-updater (v1.16.0+, DC-122)" section). Native flavor: download tarball → new release dir → swapcurrent→ restart → health check → auto-rollback on failed health. Docker path untouched. Effort: ~5 hr. - impact: Native installs get the same hands-off updates and rollback safety Docker installs already have.
DC-113: First-run doctor — preflight checks + one-click fixes
- status: pending
- details: One screen at first run (and from Help): Docker reachable? Caddy binary + admin port? ports 80/443 free? DNS resolvable? disk space? Each check shows fix instructions or a one-click fix where safe. Kills the "blank page on gated service" support class (documented failure mode). Effort: ~4 hr.
- impact: Support experience to date (DashCaddy sessions 2026-05→09) has been dominated by environment/config issues rather than code bugs; the doctor turns that class into self-service.
DC-114: "Why am I seeing this?" helper on the TOTP/auth gate
- status: pending
- details: The auth gate is a recurring confusion point (documented in the dashcaddy skill reference
totp-session-ip-key-inconsistency.md, including the operator report "I keep providing a code and it doesn't work"). Login page gets inline diagnostics: which hop failed, cookie status, IP-consistency note, retry guidance. Server returns structured reason codes instead of bare 401. Effort: ~3 hr. - impact: Converts the documented auth-gate drop-off case into a guided flow.
DC-115: Update nudge in the dashboard
- status: pending
- details: Footer badge comparing running version vs latest release feed; links to the update flow; dismissible; silent when current. The public version endpoint is verified in the live tree:
/api/v1/versionis inPUBLIC_ROUTES(src/utilities/middleware.js) and wired at startup (src/app.js). Latest-version source = the same release feed the self-updater polls (v1.16.0 contract). Effort: ~2 hr. - impact: Users running months-old builds file phantom bugs; the nudge keeps fleets current.
DC-116: Podman as a supported container runtime
- status: pending
- details: Podman speaks Docker's socket API, so most DashCaddy container paths work against it with runtime detection + docs + a CI smoke test (rootless/quadlet notes included). Positions DashCaddy for orgs that cannot run Docker Desktop (licensing) and answers the "Docker vs open-source alternatives" wave with support instead of migration. Effort: ~4 hr.
- impact: Business-friendly runtime choice; removes licensing objections in the sellable tier.
DC-117: Service-control abstraction (systemd/launchd/Windows service) + per-OS static Pylon
- status: pending
- details: One service-control API over systemctl / launchd / sc.exe; Pylon ships as a single static agent per OS (no Node runtime required on managed hosts).
platform-paths.jsstays the single source of truth for paths (v1.12.0 lesson). Effort: ~8 hr. - impact: True cross-platform management, not Linux-with-caveats.
DC-118: Mac Gatekeeper trust path — electron-builder native signing + notarization
- status: pending
- details: The Linux-built .dmg/.zip are unsigned → macOS Gatekeeper blocks/scare-warns on first open, and NO lightweight measure removes that friction: ad-hoc signing does not establish developer identity and does not satisfy Gatekeeper distribution trust (needs ~$99/yr Apple Developer Program). The fix: electron-builder's built-in mac signing + notarization — it signs nested frameworks/helpers with entitlements before the outer app (never hand-rolled
codesign --deep) and submits notarization itself. Requireselectron-builder >= 24(verify the version pinned indashcaddy-installer/package.jsonat implementation time). Implementation shape (config, not a hand-written script — the script gets written and exercised IN this item's PR where amacos-latestrunner can prove it):CI env (Actions secrets only):// electron-builder.config.js (mac section) — shape valid for electron-builder 24.x–26.x. // PIN the actual major against dashcaddy-installer/package.json at implementation time; // if the pinned major is >= 27, migrate per its mac.sign/notarize schema change before use. mac: { identity: "Developer ID Application: <name> (${TEAMID})", hardenedRuntime: true, gatekeeperAssess: true, entitlements: "build/entitlements.mac.plist", notarize: true, // auto notarize + staple on CI (24.x–26.x shape) forceCodeSigning: true, // FATAL on missing credentials — no silent unsigned artifacts }CSC_LINK(the base64-decoded .p12 file path) +CSC_KEY_PASSWORD(the .p12's password) — with a CSC_LINK p12, electron-builder imports it into its OWN temporary keychain internally, so the PR must NOT hand-roll keychain lifecycle (no custom create/unlock/partition-list code); notarization usesAPPLE_ID+APPLE_APP_SPECIFIC_PASSWORD+APPLE_TEAM_ID. Acceptance criteria (this item is done only when all pass on a realmacos-latestrun):electron-builder --macexits 0 with signing + notarization enabled.- Produced
.dmg:xcrun stapler validate <dmg>passes andspctl -a -t open --context context:primary-signature-id -v <dmg>reports accepted. - Produced
.zip: extract it, then runspctl -a -t exec -v <extracted .app>on the contained app — must report accepted. Stapling is per-bundle: the.appinside the ZIP carries electron-builder's staple; the ZIP container itself cannot be stapled, and Gatekeeper on macOS 12+ re-queries Apple's notarization service online at first open to assess the signed.appinside. - Notary submission id logged; on failure,
xcrun notarytool log <id> …output is attached before any retry. - First-open gate on a clean macOS VM must exercise the real download path with quarantine applied: download via a browser, OR confirm/add the xattr explicitly after any non-browser transfer (
xattr -w com.apple.quarantine "0081;00000000;Safari;" <file>— do NOT rely on plaincurl/scpto set it; macOS may not apply quarantine to CLI-downloaded files). First open then shows no Gatekeeper block. Drafting-review pitfalls from the 2026-09-14 adversarial rounds are HISTORICAL and inapplicable to this electron-builder approach (they applied to an earlier hand-rolled shell-script draft: keychain passwords,mktemp -u, search-list restore, filename gating — all now handled by electron-builder's internal keychain management). The version-validation rule stands: at implementation, confirm the pinned electron-builder major's actualmac.sign,notarize, andforceCodeSigningschema against its docs — do not trust the broad 24.x–26.x shape above without checking. The Linux .dmg build this slots into is documented indashcaddy-installer/BUILD_GUIDE.md(libguestfs HFS+ volume + libdmg-hfsplus UDZO). Effort: ~2 hr once enrolled (+$99/yr Apple Developer Program).
- impact: Today the first Mac impression is a security warning; the trust path must be fixed before paid acquisition, and only real signing + notarization does it.
DC-119: Cross-platform CI matrix — boot it on all 3 OSes per release
- status: pending
- details: Per release: build API + installer on Windows/macOS/Linux runners, boot the API, run smoke probes (version + health), run the installer's dependency-checker in report mode. Replaces grep-audits with runtime proof — matches the reproducibility principle (Sami 2026-06: "other people can do the same things and expect reproducibility"). Effort: ~6 hr.
- impact: Platform-parity claims become tested facts, not hopes.
Summary by Priority
| Priority | Count | Effort | Theme |
|---|---|---|---|
| P0 | 3 (DC-062–064) | ~7 hr | Public release blockers |
| P1 | 5 (DC-065–069) | ~7 hr | Reliability & code quality |
| P2 | 6 (DC-070–075) | ~5.5 hr | Polish & DX |
| P2.5 | 5 (DC-081–085) | ~15 hr | Security hardening (deep audit) |
| P3 | 5 (DC-076–080) | ~16 hr | Future growth |
| P3.5 | 9 (DC-086–094) | ~14.5 hr | Operational maturity |
| P4 | 6 (DC-095–100) | ~16.5 hr | Advanced features |
| P5 | 8 (DC-101–108) | ~29 hr | Product vision: self-hosting platform |
| P6 | 11 (DC-109–119) | ~49 hr | Shipdeck era: cross-platform & barrier removal |
| Total | 58 | ~159.5 hr |