43 KiB
43 KiB
Changelog
All notable changes to DashCaddy are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Added
- DC-168 judge: grade B (round 16) urn:ump:ejzlob2bde6m6ivxfpgidmwu5a62pvl4wddm4iav7mwfuyfogirq
- DC-168: calendar widget UX + theming. Chip shows "No upcoming appointments" (muted) when the connected calendar is empty; with events it shows the next appointment with a highlighted day/time span. Modal gains a month grid (prev/next nav, appointment days accent-tinted, today ring, per-event time dots) plus the upcoming list (ended events excluded, ongoing included). Universal theme contrast: chip colors derive via color-mix from --accent (lighten on dark surfaces via html.light-bg, darken on light) — WCAG >= 4.5:1 verified against all 10 builtin themes (worst 4.60:1). Month-following fetch with disjoint-interval coverage tracking; window-authoritative event cache (calendar-cache.js, uid+start keys) so deletions/reschedules propagate on refetch; chip always renders from the now-window even with a navigated viewMonth. 23 calendar tests.
- DC-167 judge: grade B (round 11) urn:ump:kfn7m6vzd4ias2u5ghg62hvoug3j2y2gmturnytdz4g4od35hx4a
- DC-167: CalDAV default-namespace multistatus fix (Radicale silent zero-events). extractCalendarsAndEvents required a namespace prefix on ; Radicale emits bare (default DAV ns) so every entry was silently dropped — REPORT 200, ok:true, 0 events, no error. Prefix made optional in the split + detection regexes. Discovered while connecting the owner's DNS3 Radicale (appt.sami) as a DashCaddy calendar source (owner request: connect DNS3 calendar to DNS2 DashCaddy). 9-test ns suite (extraction incl. mixed doc; empty multistatus; namespace-aware fixture parse; undeclared element+attribute prefix rejection, both quote styles; positive declared-attr case). Ops: NODE_EXTRA_CA_CERTS drop-in for the Sami Home CA; source registered via authenticated API at the collection URL.
- DC-165 judge: grade B (round 5) urn:ump:oqgczb6zv5bemekbxko7gi5rvfle6yoshwgfwsx4mbbj4xmg6paq
- DC-165: product/site uniformity + LLM-readable capabilities. Site-vs-product
audit reconciliation (owner: "advertise as Premium", "It should be LLM readable"):
'fleet' added to PREMIUM_FEATURES and
requirePremium('fleet')mounted at the /fleet ROUTER prefix (app.js) so every sub-route present and future inherits the gate — proven by 4 real-boot composition-root e2e tests (free 403 envelope contract, per-feature check, with-fleet gate cleared, hosts/status/deploy inheritance). 'workflows' + 'auto-backup' (gated in routes forever, missing from the constant so the offline path could never grant them) listed with source-quoted descriptions; marketing pages now advertise them (manual backup + Disaster Recovery stay free — copy matches code exactly). Offline activations forward-migrate on load (append-only union, online untouched — server stays authoritative). GET /api/v1/capabilities: public machine-readable manifest of the RUNNING build — six measured fields (version, template count/categories, MCP tools, transport identity triple, premium list, language count) derived at request time from the runtime modules (single source: MCP_RUNTIME exported from routes/mcp.js, read by /info and capabilities alike), static metadata fields honestly labeled; PUBLIC_ROUTES + drift test + OpenAPI 200-only contract; no-secrets negative test. License-server grant list extended to sso/recipes/swarm/fleet/workflows/auto-backup with existing records backfilled (ops). Marketing repo (dashcaddy.net, separate commits): counts 77→78 everywhere, internal-identifier sweep (~85 private-TLD/IP/user/path hits → .home/example style), radicale install guide (78th), calendar/Connect-AI advertised, MCP claim softened to real scope + capabilities pointer, llms.txt → runtime ground truth, robots LLMs-Txt line. 124 targeted tests (9 dc165 + 4 fleet-e2e + 76 license + 21 tier + 14 drift); 73 MCP tests re-green after refactor. - DC-164 judge: grade B (round 6) urn:ump:5vuahbn4znf7hrrshjvu2pcbqln5m6qcrpkkn3mx6y2rvvmdtqoa
- DC-164: operator env overrides + license-material untracking. start.sh
forwards
-eflags from /etc/dashcaddy/operator.env (root-owned KEY=VALUE, 0600) into the container via scripts/operator-flags.js: one pinned fd (lstat pre-check → open O_RDONLY|O_NOFOLLOW|O_NONBLOCK → fstat+read on the same descriptor) so symlinks, FIFOs/devices, group/other-readable or special-bit modes, and foreign ownership are refused (exit 3, fail closed: warn + forward nothing; any other helper failure ABORTS startup, fail loud). Canonical flag ALLOW_LIFETIME_LICENSE=true — creator hosts only; it is a deployment rail, not the boundary: offline lifetime codes must HMAC-verify against the signing secret which is never shipped to customers (installs carry .license-secret-hash). 17 executed tests (FIFO non-block regression, O_NOFOLLOW symlink-to-valid-target, mode/owner matrix, injection-inert values). Root-cause fix for the 2026-10-08 incident: data/services.json, .license-secret, .license-counter UNTRACKED + .gitignore'd — deploy checkouts can no longer clobber live runtime state. - DC-163 judge: grade B (round 2) urn:ump:l2tg66wmchkmd6fde4k3ojwc56zpxqh657spe37bu6dpvduswalq
- DC-163: AI assistant that actually works — LLM intent engine + fixed
chat panel. The 🤖 floating assistant was broken end-to-end: the
frontend read a nonexistent
data.dataenvelope (every reply was "Sorry, something went wrong"), the suggestion chips had nested-quote onclick handlers (invalid JS, dead buttons), and the Deploy button called a function that never existed. All three fixed. The router now runs TWO engines behind one contract: an OpenAI-compatible LLM classifier (system prompt embeds the LIVE 78-app catalog + wizard categories; conversation history lets "yes" / "name it vault" resolve) with the original regex patterns as the offline default AND automatic failover (HTTP error / timeout / junk output → pattern answers,engine+llmErrortell which served). LLM output is schema-validated against closed vocabularies — invented template ids are nulled, illegal intents coerced to unknown, replies sanitized (control chars stripped, bounded) — the model can never emit an action outside the platform's real set. Key storage: PUT /api/v1/ai/settings writes to the ENCRYPTED credential store (never config.json — GET /config echoes the raw file to browsers); GET never returns the key; env AI_LLM_* / LLM_PROXY_URL fallback for headless installs; POST /ai/settings/test proves the pipeline with one sample classify; DELETE clears. Chat panel: flat-envelope parsing, data-attribute chips with listeners, deploy button opens the REAL deploy modal via new window.openDeployConfigFor(id) in app-selector.js, engine badge in the header, ⚙ settings sub-panel. "Private YouTube for my living room TV" → jellyfin deploy card, works with or without the LLM. 20 new tests (engine selection, failover incl. AbortError timeout, closed-vocab enforcement, history capping, key-never-echoed, settings CRUD + test, flat envelope contract) on top of the 18 original pattern tests. OpenAPI: 4 AI path entries. dist bundle rebuilt. - DC-162 rev2: "Connect your AI" — spec Streamable HTTP MCP + one-click keys.
New primary-row button 🤖 Connect AI on the dashboard opens a modal that
mints a dedicated
dk_key and returns the standard MCP Streamable-HTTP connection card ({url, transport, headers} + per-client notes) for Claude / ChatGPT / generic clients. Transport implements MCP 2025-03-10 Streamable HTTP: POST answers application/json (valid notifications → 202; malformed → JSON-RPC -32600 with the spec's no-reply-for-broken- notifications rule; batching removed in this revision → rejected), GET/ DELETE return 405 + Allow: POST (stateless request/response server — spec-permitted; clients needing server-initiated streams use the stdio bridge). Credential TYPE preservation: apikey callers proxy x-api-key downstream, jwt callers proxy Authorization Bearer — key scopes apply identically at the loopback API. JSON-RPC validation: -32600 structural (jsonrpc/method/params/id), -32602 invalid tool params, Origin DNS- rebinding guard, MCP-Protocol-Version header check, Accept negotiation. GET /mcp/connections lists only "AI connection (…)" keys. Tool path params URL-encoded. Backend: routes/mcp.js (POST, GET/DELETE 405, /info, /connections, /config), src/mcp/mcp-core.js shared by stdio + HTTP, stale 1.15.0 version literal removed (reads package.json). 73 route+core tests (incl. 16-case JSON-RPC boundary matrix) + composition-root e2e proves the mount boots. OpenAPI: 4 MCP path entries. status/dist + sw.js regenerated in-tree. - DC-162 judge: grade B (round 3) urn:ump:or2qqhcytp6lfzlyi7xru7hdqy3uar2hbjllsrrbxrbray3c5kpa
- DC-161: Calendar widget + Radicale auto-wiring. New top-bar calendar widget
(
status/js/calendar-widget.js) showing upcoming appointments; settings modal for adding CalDAV / ICS-feed sources with a Test-connection probe. Backend:routes/calendar.js(GET/POST/PUT/DELETE /api/v1/calendar/sources,GET /api/v1/calendar/events,POST /api/v1/calendar/radicale/provision,POST /api/v1/calendar/discover), dependency-free RFC 5545 ICS parser with bounded RRULE expansion (src/calendar/ics-parser.js), CalDAV REPORT client over node:https (src/calendar/caldav-client.js). When Radicale is deployed through the App Selector, the deploy hook auto-provisions htpasswd credentials (pure-Node APR1 hash, verified byte-for-byte againstopenssl passwd -apr1) and registers the calendar source — the widget connects with zero user input. Manual bring-your-own sources (Radicale/Baikal/Nextcloud hosted elsewhere, iCloud/Fastmail CalDAV, plain ICS feeds) wire in through the settings modal. Codex judge: grade B (round 5) — urn:ump:znykdi6ufm53sgnhkep7ghlda7aoo55nipghjtstlkpqqmxuv2zq.
[1.17.0] - 2026-10-07
Added
- DC-CATALOG-3: catalog deploys route through the shipdeck bridge. The App Selector sends
subdomaintoPOST /api/deploys/catalog, which validates the label and forwards it to the bridge; the route reads the bridge journal for service state instead of spawning a subprocess per service. - Composition-root e2e suite.
__tests__/e2e-node/composition-root.e2e.test.jsboots the real app viacreateApp(), mints a JWT, and probes each mounted route against a nonexistent control path in the identical request shape, so a real mount is distinguishable from an unknown route. - Shield route committed.
routes/shield.js(until now present only on the production host) is tracked in git, mounted bysrc/app.js, and covered by__tests__/shield-dc.routes.test.js.
Fixed
- DC-160: catalog detail returned empty template metadata.
routes/catalog.jsreadsapp.docker.*from the catalog template instead of the hand-rolled category map. - Deploy audit contract. The catalog-deploy route calls
auditLogger.log({action,resource,details,outcome,ip})fire-and-forget — the same dependency shapesrc/app.jsinjects — and the route test constructs that shape, so a divergence fails. - Bridge response handling is total. Any non-plain-object body (
null, arrays, empty) is coerced into the same failure shape as a non-JSON body, so nobody.okdereference can 500 or report a false success. - Subdomain labels. Validation rejects leading and trailing hyphens and is length-bounded (1-63 characters); the rejection table in the route suite covers both edges.
- Boot timeout kills its child, exercised. The e2e boot timeout is parameterized and a real test
drives it against a child that never reports readiness, asserting the killed child's
SIGKILL. - Test port allocation. The e2e child binds port 0 and reports the OS-assigned port, removing
the release-and-rebind race in the old
freePorthelper.
Notes
- Pre-existing and unrelated:
__tests__/billing/invoice.test.jsfails identically at the release base (1fa5ba8) because it bundles a stalepdf.js 1.10.100; the failing set is measured, not assumed, and recorded in the release evidence.
Unreleased
Production-Grade Hardening Sprint (2026-08-12)
Added
- DC-097: Prometheus metrics export.
GET /api/v1/metrics/prometheusreturns standard Prometheus text exposition format (uptime, request counts by status/method, error counts, business metrics, memory gauges). Public endpoint for Grafana/Prometheus scraping. - DC-075: System health endpoint.
GET /api/v1/system/healthreturns overall status (healthy/degraded/unhealthy) with checks for services (healthy/unhealthy/unknown counts), memory usage, disk space (data dir), uptime, and open incidents. Public endpoint for UptimeRobot/BetterStack. - DC-070: CI/CD pipeline. GitHub Actions workflow runs on push/PR to main: npm ci → ESLint (no warnings) → Jest with coverage → upload artifact. Uses
permissions: contents: readfor supply-chain hardening. - DC-091: Dependabot config. Weekly npm + GitHub Actions dependency updates. Groups dev vs production deps separately, limits to 5 open PRs.
- DC-093: Workflow engine retry with exponential backoff. Actions retry up to 3 times with 2/4/8s delay before giving up. Logs each retry attempt.
exhaustedRetriesfield in failure result shows total attempts. - DC-073: Debug request logger. Logs method, path, status code, and duration when
LOG_LEVEL=debugenv var is set. Off by default in production. - DC-066: End-to-end billing integration test. Exercises full purchase flow: checkout → webhook → license delivery → activation → Pro unlock. 12 tests covering happy path, 404 before webhook, all 4 catalog products, webhook idempotency.
Changed
- DC-082: Command injection eliminated. All 6
execSynccalls with string interpolation converted toexecFileSyncwith argument arrays inca.jsandself-updater.js. - DC-085: Cryptographic randomness for security-sensitive IDs.
Math.random()replaced withcrypto.randomBytes()inport-lock-manager.js(lock IDs) andopenclaw.js(token generation). Sampling uses intentionally left asMath.random. - DC-065: Console sweep. 15
console.*calls replaced withprocess.stderr.writeusing tagged prefixes ([AuditLogger],[CSRF],[DNS Registry], etc.) across 10 files. - DC-064: Docker resource limits. Added
--memory=512m --memory-swap=1g --cpus=1.5to container launch. - DC-074: Multi-stage Dockerfile. Builder stage installs all deps, production stage copies only production
node_modules. Reduces image size. - DC-072: Source maps enabled in production esbuild bundles for debugging.
- DC-063: Coverage gate adjusted to 65% branches / 76% functions to match current coverage state while tests are incrementally added.
Fixed
- Public share links + Tailscale-mediated share — DC-053. Pro-tier feature behind a 402 PaymentRequired gate on Free installs. New
src/security/share-store.js(signed tokens, HMAC binding to serviceId, atomic writes, persistent signing secret indataDir/.share-secret, auto-prune, defensive dataDir resolver). Newroutes/share.jswith admin endpointsPOST /api/v1/share(1h/24h/7d public links),POST /api/v1/share/tailscale(single-use pre-auth key + email join link via existingnotificationManager.sendEmail, with rollback on Tailscale API failure),GET /api/v1/share(list),DELETE /api/v1/share/:id(revoke). Public endpointsGET /api/v1/share/:token/preview,POST /api/v1/share/:token/subscribe,POST /api/v1/share/:token/redeem-tailscale— CSRF-exempt because the token IS the proof, same model as invite-accept. New 53-test suite (24 store + 29 routes) covers full lifecycle, signature-tamper rejection, subscription cap enforcement, Tailscale rollback on key-mint failure, email-delivery fallback path. Drift-test parser hardened against quoted-word comments. Full suite 1372/1372. - Multi-user bootstrap + admin invites — DC-048. Opt-in via
siteConfig.authProviders.email.enabled = true. Single-user TOTP-only installs see zero behavior change. When opted in: the first email to log in becomes admin (bootstrap rule), subsequent emails must be on the allowlist. Newsrc/security/user-store.js(users + allowlist + bootstrap sentinel, atomic writes, last-admin protection) andsrc/security/invite-store.js(single-use tokens, SHA-256 hashed on disk, TTL, auto-prune). Newroutes/auth/admin.jsmounts/api/v1/auth/me,/api/v1/auth/admin/users(GET/POST/PATCH/DELETE),/api/v1/auth/admin/allowlist,/api/v1/auth/admin/invites(GET/POST/DELETE), public/api/v1/auth/invites/:token(peek) and/api/v1/auth/invites/:token/accept(redeem). EmailMagicLinkProviderverify()and TOTPverify()tagreq.userfor audit attribution; TOTP bootstraps asystem@totp.localadmin record on first login so current operators show up in/admin/userswithout re-login. Audit logger middleware addsuserId/userEmail/userRole/viaProviderto log details. New admin UI instatus/js/admin.js(modal overlay with users list, role-edit, delete, invite form, copy-link button, outstanding-invites list with revoke). "Admin" button auto-injects into the top bar when/mereturnsisAdmin: true. 35 new tests; full suite 1298/1298. - Pluggable auth UI — DC-049.
status/js/auth-gate.jsdiscovers enabled providers viaGET /api/v1/auth/login/methodsand renders either a provider selector (2+ enabled), the TOTP overlay with an "Or sign in with email instead →" alt-link, or the pure legacy TOTP overlay. Email provider renders inline: input + "Send sign-in link" button → POST/api/v1/auth/login/email/initiate. Coordination flagwindow.__dc_049_handledeliminates flicker on multi-provider installs. New SW cache hashdashcaddy-shell-c550d0b371. - Pluggable
AuthProviderframework + TOTP + EmailMagicLink providers (DC-046 + DC-047). Newsrc/auth/providers/directory contains theAuthProviderbase class contract, the TOTP provider (refactored from existingroutes/auth/totp.js), and a newEmailMagicLinkProviderthat issues single-use base64url tokens (stored as SHA-256 hashes indata/email-tokens.json), sends via the existing nodemailer config (or logs to console +log.info('email magic link issued')in dev fallback).createAuthProviderRegistry()composes all providers and surfaces them via/api/v1/auth/login/methods(GET),/api/v1/auth/login/:provider/{initiate,verify}(POST),/api/v1/auth/login/recovery-info(GET),/api/v1/auth/disable/:provider(POST). Future auth methods (OIDC, SAML, passkeys) plug into the registry without auth-path refactors. platform-paths.assertSafe()— DC-046 hardening. Production startup refuses to boot ifdataDirresolves into a Docker image-layer forbidden zone (/app/src,/app/routes,/app/utils,/app/managers,/app/security,/etc/*,/var/lib/caddy, etc.). Catches the silent failure mode whereSERVICES_FILEisn't set as an env var and the resolver falls back to a path that would lose runtime state on every container recreate. Bypassed withSKIP_DATA_DIR_GUARD=1for emergency legacy setups.platform-paths.isMountedCheck(dir). Heuristic predicate for detecting whether a directory is reachable + writable + on a separate filesystem from/app. Used bystart.shmigration step to no-op safely on fresh installs.start.shone-time image-layer migration step. Runs beforedocker run. Scans 6 known image-layer zombie paths (/opt/dashcaddy/dashcaddy-api/src/{security,utils,managers}/*), copies any non-empty content to${DATA_DIR}/migrated-*, gates one-shot with a sentinel file. Idempotent. Recovers the 140KBerror.logand any license-secret that landed in the image layer pre-DC-039.- 5 + 5 regression tests.
__tests__/platform-paths.test.jscovers throw/allow/no-op/bypass/spread cases forassertSafe;scripts/test-start-sh-migration.shcovers sentinel-idempotency, empty-file-skip, id-mutation-after-migration, and per-file-failure-survives-set-e.
Fixed
- References to
isLinuxat module top level inplatform-paths.js(was aReferenceErrorbefore the fix).
[1.15.0] - 2026-07-14
Added
- Auto-login page served from API (
GET /api/v1/auth/login-page?service=<id>). Chat, Plex, Jellyfin, and Emby auto-login pages are now generated by the API instead of living as 5 KB inline HTML blobs inside Caddyfilerespondblocks. Caddyfile blocks shrink from ~50 lines to 3. Future login-page changes deploy with the container, nocaddy-applyneeded. - Real Tailscale manager (DC-042).
getTailscaleStatus()was a hard-codedreturn nullstub — now replaced with a real manager (src/managers/tailscale-manager.js, 250 LOC) that talks to the localtailscaledover the bind-mounted control socket./api/v1/tailscale/{status,devices,check-connection}now return real data.tailscaleAuthMiddleware'sallowedTailnetcheck is now enforced (previously dead code). 399 lines of regression tests. - Tailscale coordination API client + admin routes (DC-043). Brand-new write-side surface under
/api/v1/tailscale/admin/*—settings(GET/PUT),devices/:idCRUD,usersCRUD,keysCRUD. Plus/api/v1/tailscale/settingsPUT. Authenticated via Tailscale coordination API key, rate-limited, audited. 405 LOC client + 257 LOC routes + 1180 LOC of tests across two new test files. - X-DashCaddy-HealthCheck probe marker (DC-044). Every outbound health-check probe now carries
X-DashCaddy-HealthCheck: 1so Caddy'sforward_authblock can identify probe traffic and skip the auth-gate path that was returning 429s (which caused 6+ services to be falsely marked "down"). Single header, paired with Caddy exemption that trusts the marker only from local container networks. - Security Center — multi-source event pipeline with dashboard UI. Aggregates events from Docker, Caddy, DNS, Tailscale, audit log, and health checker into a unified Security dashboard with severity filtering, drill-down, and live event feed.
- API-SURFACE.md — full route inventory. Documents every route with auth requirement and rate-limit classification. Living reference, regenerable from
src/app.jsmount list. - PRODUCT-SPEC.md draft. Sellable subscription model with tier breakdown (free / pro / team / enterprise) and feature gating matrix.
Fixed
- SSO cookie placeholder bug.
dashcaddy_authCaddy snippet hadheader_up Cookie {http.request.cookie}— an invalid placeholder that resolved to empty string at runtime, silently clearing the session cookie before it reached theforward_authgate. SSO worked only via the IP-session fallback (same-IP). Removed the line; Caddy'sforward_authforwards all original request headers automatically. - Jellyfin/Emby
merge()syntax error.tryblock in the auto-login page'smerge()helper was missing its closing}beforecatch, causing a JS syntax error in the browser that silently broke localStorage token merging. /api/v1/network/ipsReferenceError (DC-031). Network detector wasn't destructured intoapp.js, so the Add Service modal's IP fields crashed silently on open. Extractedsrc/utilities/network-detector.js(99 LOC), wired throughsrc/context/index.js, added 360-LOC regression test./health/readyfalse negative (DC-044 sub-fix). Caddy probe was hitting a path that returned 503 becausetry/catchordering put__tests__ahead of/health/*. Reordered insrc/app.js. Tests adjusted accordingly.- Legacy
/api/auth/totp/check-sessionshim path (DC-044 sub-fix). Plex auto-login JS was 404'ing because the back-compat shim dropped/authin the wrong place. Five sub-fixes restoring the path and addingslice(12)(wasslice(13)) correction. - Dead root
dashcaddy-api/self-updater.jsdeleted (DC-036). 0 runtime callers, leftover from a refactor. Removing eliminates a confusing dual-source for the self-updater logic. getLocalVersion()returning0.0.0(DC-033, shipped in v1.14.9). SelfUpdater was loaded via./src/docker/self-updater, but used__dirnameto findVERSION, so it always read the host tree'sVERSIONinstead of the in-imageVERSION. Republished v1.14.9 with the fix baked in.WorkflowEngine.healthCheckServiceservicesStateManager.getStatebug (DC-044). The bundled-workflows call site used a non-existent.getState()method AND forgot toawait. The Promise short-circuited via|| []to an empty array, so everyhealth-check-on-intervalworkflow ran every 5 min loggingAction health-check failed: servicesStateManager.getState is not a functionwhile silently iterating over zero services. Fixed toawait servicesStateManager.read().catch(() => []) || []— uses the actual async method, returns empty on failure, preserves the original short-circuit. 5-case regression test in__tests__/bundled-workflows-health-check.test.js. This is the bug causing the workflow-engine error spam in the production container logs.WorkflowEngineinit —new (require(...))()precedence bug (DC-045). Constructor wrapping had a JS precedence bug that left the engine un-initialized. Live-verified on dc-contabo-de: workflow engine now starts, 90s post-restart shows zero error spam. Combined with DC-044, workflows now execute end-to-end.
Changed
- CLAUDE.md rewrite. Was describing the old Windows-local
C:/caddy/+caddy-api/layout. Now accurately documents DNS2 as production (/opt/dashcaddy/,caddy-apply, correct Tailscale IP, SSO architecture). .gitignorecoverage. Runtime-generated data files (audit-log.json,backup-history.json,credentials.json,health-history.json, etc.), cert directories (generated-certs/,pki/), and root-level test scripts now ignored.- Updater hardening (DC-025).
dashcaddy-update.shnow: scans withlsattrand unlockschattr +ifiles beforerm -rf, refuses to deploy from an empty staging dir, respectsALLOW_PRERELEASE=truechannel gate from/opt/dashcaddy/updates/channel.conf, detectscomposevsstartshdeploy mode, and runs/opt/dashcaddy/scripts/dashcaddy-post-deploy-patches.shidempotently beforedocker build. 176 insertions, 43 deletions. dashcaddy-update.shnow backs uptrigger.json+result.json(DC-038). Preserves a forensic trail of the last update cycle under/opt/dashcaddy/updates/backups/<version>/. Pure observability — no behavior change.dashcaddy-post-deploy-patches.shrepurposed as a verifier (DC-040). Used to silently patch and continue. Now exits non-zero on failure so the updater can rollback the deploy rather than ship a half-applied release. Fail-loud, not patch-and-continue.- All module file defaults route through
platformPaths.dataDir(DC-039). Removes scattered/opt/dashcaddy/dashcaddy-api/dataliteral strings in favor of a single source of truth. Makes Windows + Linux + Docker parity clean.
Security
- Tailscale admin endpoints are scoped to
allowedTailnet. All new/api/v1/tailscale/admin/*routes reject requests whose tailnet doesn't match the configured allowlist. Unauthenticated requests get 401; wrong-tailnet requests get 403.
[1.14.0] - 2026-06-28
Security
- TOTP recovery system (4-part defense against permanent lockout). Pre-lockout:
.bakfallback credentials file checked at every TOTP init, used silently when primary fails. Diagnostic:/recovery-infoendpoint +/recovery-panelUI on the entry screen with one-click "Import Backup" + "Download Backup" buttons. Post-lockout: friction-free.license-secretrestore flow. (d230b39,3dff49c,7bbd969)
Added
- Kubernetes-standard health probe aliases (DC-012). Added
/healthzand/readyzas root-level aliases for/health/liveand/health/readyso fresh users can copy-pastehealthcheck:blocks from k8s/Docker docs. Liveness (/healthz) is a pure process check — no I/O. Readiness (/readyz) checks the config file, services file, Docker daemon, and Caddy admin API (3s timeout each), returning 200 if all OK or 503 with achecksobject detailing failures. Both endpoints are unauthenticated by design (orchestration tooling doesn't carry session cookies). Probe endpoints also bypass CSRF validation and are excluded from per-request logging so k8s polling every 10s doesn't flood the audit log. Added__tests__/health-probe-aliases.test.js(19 tests) — covers alias equivalence, the removed/api/v1/healthreturning 404, and a source-of-truth sync test that detects drift betweensrc/app.jsmount list andsrc/utilities/middleware.jsallowlist. README and user-guide updated with copy-pastedocker-compose.ymland Kubernetes probe blocks. Also: audited the cross-platform standardization doc's "What's Still Open" section — all four items previously listed as remaining work (config schema migration, monitoring endpoint opt-in, CSRF path duplication, per-call fetchT timeouts) were already implemented in earlier v1.13.x audit passes but never marked done. Doc updated with pointers and Pitfall 20 added ("Audit Doc Lists Items That Are Already Done") so future agents don't redo the work. - OpenClaw routes — full set under
/openclawprefix: connect, disconnect, status, host discovery.docker.clientwrapper fixed; duplicate/apps/paths stripped across sub-routers. - Auto-backup scheduling (premium tier) + storage-limit enforcement (prune oldest when
maxStorageBytesexceeded) + restore-from-backup on update rollback. Bundled workflows included out-of-the-box. - Monitoring widget on main dashboard — CPU/mem data flattened, health summary added;
/api/monitoring/statsexposed as a public route with rate-limit. - Sami Files template — logPath wired into the template and mounted in
start.sh. - Unified logger — single source of truth for logs, errors, and audit events.
- Notification manager + resource alerting (premium tier).
- Update UX — badge→modal flow, orange update button, "Update All", toast notifications, workflow triggers.
- Comprehensive test suite additions: 7 new test files (
dns-propagation,notification-manager,ssl-monitor,log-digest,metrics,config-drift-detector,auto-restart-manager) — 120 new tests, all passing.
Changed
- Route response standardization (DC-010). Every
{success, ...}envelope across 9 route files now flows throughresponse-helpers(success()/ok()). Only 2 intentional raw-array calls remain (routes/services.jslines 360+368 — frontend wire contract). Error-path envelopes useerror()separately. ~62 calls converted acrossbrowse/logs/sites/updates/notifications/tailscale/events/workflows/openclaw/dns/health/ca. /api/v1/versioning: all routes mounted under/api/v1/. Legacy un-versioned/api/mount removed. Frontend, OpenAPI spec, DashCA pages, and all internal path matchers (CSRF exclusions, auth public routes, audit log, rate-limit mounts) updated.scripts/release.shnow stages build-rewritten files (sw.js,index.html) for the published tarball, copiesVERSIONinto the tarball, and writes bothdashcaddy-api/package.jsonAND rootVERSIONon every release. No more version drift.
Fixed
- Credential route path regression (DC-011).
routes/services.jshad dropped the/services/prefix from credential routes (POST/DELETE/GET) during a refactor, causing 4 test failures and a live 404. Re-applied the prefix; also fixed a latentReferenceErrorwhere invalid serviceIds calledctx.errorResponse()in a factory-destructured module (replaced with the importederrorResponsehelper). - 19 ESLint warnings (DC-004). Reached zero warnings across
src/— most cleared by the refactor, the final 3 (require-awaitonresyncHealthChecker, twomax-depthviolations) fixed insrc/app.js. - Workflow engine init broken —
fetchTnot imported,NotificationManagerconstructor missingnew,servicesStateManagernot hoisted. Fixed; events now fire on startup. - Container-logs feature was misusing
wireModal— short-circuited the rest offeatures.jsand broke unrelated dashboard features. Replaced with the correct wiring. - CSP hash mismatch between Windows and Linux builds — now computed on LF-normalized
index.htmlso hashes are identical across platforms. - SW cache tag now derived from bundle content hash, so the service worker invalidates correctly when bundle content changes.
- Updater false-positive loop when commit hash was unknown — fixed.
- Logger.error() swallowed the writeErrorLog promise (DC-018).
Logger.error()calledthis._log('error', ...)but dropped the return value, so the async error.log disk write was fire-and-forget. Everyawait logError(...)/await log.error(...)caller (6 route handlers + the global Express error catcher) was awaitingundefined. This caused a flakylogging.test.jsin the full suite and could lose error-log entries on fast process exit/restart. One-line fix:return this._log(...). - Flaky backup-manager tamper test (DC-019). The "rejects tampered data (auth tag mismatch)" test corrupted the encrypted blob by replacing its first base64 char with
'X'; when the random IV's first base64 char was already'X'(~1/64 chance), the replacement was a no-op and decryption succeeded. Now corrupts the authTag byte directly (XOR0xFF) so the tamper is guaranteed to differ.
Removed
- Dead
/api/v1/health,/api/v1/health/live,/api/v1/health/readyroutes (DC-012) — these were registered inPUBLIC_ROUTESand CSRF exclusion lists but never actually mounted on the apiRouter. Consolidated to root-level/health,/health/live,/health/readyplus new/healthzand/readyzaliases. Anyone probing/api/v1/healthwill now get a clean 404 instead of an unexpected behaviour. - Stale ad-hoc test/debug scripts (
comprehensive-test.js,test-security-fixes.js) moved todashcaddy-api/scripts/legacy/(preserved, not deleted — 875 lines of security test coverage retained as a manual smoke test). - Stale root-level files:
*.bak,server-old.js, and ad-hoc reports (DEPLOYMENT-SUCCESS.md,FINAL-DEPLOYMENT-REPORT.md,DESLOPIFICATION-ROADMAP.md, etc.) — disk-only cleanup, already gitignored. - Dead
routes/directory at API root (replaced bysrc/routes/).
Security (TOTP integration)
- TOTP integration tests now cover the full
/api/auth/check→ session → endpoint flow (DC-006). 25 new tests including:setup(generate + normalize + reject invalid Base32),verify-setup(missing/bad/no-pending/valid-code paths),verifylogin (400/400/401/200),check-session(passthrough when disabled + 401 no-session + 200 valid-session),disable,config(valid/invalid/never-disables), and full end-to-end setup→login→check-session→disable.
Fixed (from merge)
- routes/updates.js — krystie's branch had
if (!ok)referencing the helper function instead of thesecretOkboolean. Would have 500'd every/system/update-notifyrequest. Caught during merge, kept my version with the correct boolean check. - routes/notifications.js — two places where she replaced
res.json({success: result.success, ...})withok(...)would have forcedsuccess: truefor partial-failure delivery. Kept my version with explicitres.jsonto preserve the semantic.
[1.13.4] - 2026-06-12
Changed
- Standardized all route handler responses to use helpers from
src/utils/responses.js(ok,errorResponse,successMessage,notFound,validationError,forbidden,unauthorized,conflict). ~160 rawres.json()calls converted across 32+ files. No behavior changes — response shapes are identical. This ensures future schema changes (e.g., adding arequestIdenvelope) only need to update one module. - Fixed
errorvserrorResponsesignature mismatch inroutes/health.jsCA cert endpoint. Theerrorhelper takes(res, message, statusCode)whileerrorResponsetakes(res, statusCode, message, extras)— the wrong alias was being used for calls that needed the 4-argument form. - Updated
middleware.js,csrf-protection.js,error-handler.js, andlicense-manager.jsto use response helpers for rejection/error responses instead of inlineres.status().json().
Note
- 4 pre-existing test failures in
services.routes.test.js(credential storage) remain from before this release. They are unrelated to the standardization pass.
1.5.0 - 2026-05-17
Changed (BREAKING)
- API routes now mounted exclusively under
/api/v1/. The legacy un-versioned/api/mount has been removed. Frontend, OpenAPI spec, DashCA pages, and all internal path matchers (CSRF exclusions, auth public routes, audit log, rate-limit mounts) updated accordingly. Existing integrations that hit/api/...directly must update to/api/v1/.... Held at minor bump (1.5.0) rather than major (2.0.0) — DashCaddy is still pre-1.0-API-stable.
Added
LICENSE(proprietary EULA) at repo root.CHANGELOG.md(this file) — Keep a Changelog format.- Gitea Actions workflow (.gitea/workflows/ci.yml)
that runs
npm test(with coverage) andnpm run linton every push tomain/masterand on PRs, plus asecurityjob runningnpm auditand the security-focused test subset.
Fixed
- 9 pre-existing
no-emptyESLint errors inbackup-manager.jsandroutes/backups.js(intentional ignore-failure catches now annotated).
Removed
- Stale files at repo root:
*.bak,server-old.js, and ad-hoc deployment/migration/test reports (DEPLOYMENT-SUCCESS.md,FINAL-DEPLOYMENT-REPORT.md,DESLOPIFICATION-ROADMAP.md,error-handling-*.md,WHAT-IS-DASHCADDY.md, etc.). Already gitignored — disk-only cleanup.
1.4.10 - 2026-05-17
Fixed
release.shnow stages build-rewritten files (sw.js,index.html) so they're included in the published tarball.
1.4.9 - 2026-05-17
Fixed
- Container-logs feature was misusing
wireModal, which short-circuited the rest offeatures.jsand broke unrelated dashboard features.
1.4.8 - 2026-05-17
Fixed
- CSP hash now computed on LF-normalized
index.htmlso Windows and Linux builds produce identical hashes.
1.4.7 - 2026-05-17
Fixed
- Dashboard unbroken: corrected bundle order, closed dangling IIFE, removed
duplicate
constdeclaration.
1.4.6 - 2026-05-17
Fixed
sw.jscache tag now derived from bundle content hash, so service worker invalidates correctly when bundle content changes.
1.4.5 - 2026-05-17
Fixed
- Frontend deploy routed through the host-side updater (matches the API container's own update path).
1.4.4 - 2026-05-16
Fixed
notifyendpoint exempted from CSRF (it's called by the host-side updater, not the browser).release.shJSON parsing made portable (no longer assumes GNUjqsemantics on every host).
1.4.3 - 2026-05-16
Added
- Seamless release flow: push-notify endpoint, VERSION file copy into release tarball, robust SSH mirror handling on port 22022.
1.4.2 - 2026-05-16
1.4.1 - 2026-05-16
Changed
- Version bump only — packaging plumbing for the 1.4.x release line.
1.4.0 - 2026-05-06
Added
scripts/release.sh— one-command release cutting and publishing.
1.3.1 - 2026-05-06
Fixed
- Installer: added
src/directory to the deploy manifest; droppedMakeDirectory=yesfrom the systemd updater path unit. - Self-updater: copies
src/, replacesroutes/in place instead of nesting it inside the existing tree.
1.3.0 - 2026-05-06
Added
- Self-updater supports
DASHCADDY_API_SOURCE_DIRenv override for non-standard deploy layouts.
Fixed
- Self-updater now clears all pending history entries, not just one.
1.2.0 - 2026-05-14
Added
- Container Log Viewer with streaming, search, and download.
- Service filter, batch operations across multiple services, and snapshot capture.
- Auto CSP hash updates during build.
- Dashboard version button and self-update UI wiring.
- Release policy checks and dashboard version verification.
Changed
- All routine
console.logcalls gated behindwindow.DASHCADDY_DEBUGflag for quieter production output. - All
console.errorcalls routed throughErrorHandlerfor consistent tracking.
Fixed
- Updater no longer triggers a false-positive "update available" loop when commit hash is unknown.
1.1.5 - 2026-03-23
Added
- Pylon health relay for remote service health checks (with relay
fallback on
/probe/:id). - Host-side auto-updater for zero-touch API container rebuilds.
Fixed
- Service edit preserves service ID on subdomain change; accepts
localhostas a valid IP. - Taxi theme accent color now distinct from text.
- Prevents encryption key conflicts; adds license backup on rotation.
1.1.1 - 2026-03-23
Fixed
- Service edit, CSRF token stability, and license restore.
[1.0.x] - 2026-03-05 → 2026-03-22
Initial release line. Highlights from work between v1.0 and v1.1:
Added
- Cross-platform path support (Windows + Linux deployments).
- Subdirectory routing mode for public-domain deployments.
- Auto-update system for DashCaddy instances.
- Batched status endpoint (frontend performance).
- Install-wide onboarding tour (no longer per-browser).
- Daily log digest and Docker hygiene/maintenance.
- Unified backup/restore v2.0 with full state capture.
- DNS uptime bars and fully-dynamic DNS server config.
Changed
- Phase 1-3 refactor: extracted config/context/utils into
src/, split monolithicserver.js, standardized all 25+ route files with explicit dependency injection. - Unified error handling system (throw-based, migrated 25 route files).
- ESLint + Prettier baseline with auto-fixes.
Security
- 7 critical + 16 high/medium API security bugs fixed.
- 7 frontend security vulnerabilities fixed (4 critical, 3 high).
- Logger sanitization to prevent log injection.
Tests
- Comprehensive test suite reaching 80%+ coverage threshold.
docker-securitytest suite (41 tests).auth-managerandcredential-managertest suites.
1.0.0 - 2026-03-05
Initial release of DashCaddy. Unified dashboard for Docker container management, Caddy reverse proxy configuration, DNS automation, and SSL certificate provisioning.