Three coordinated fixes for the System Overview widget:
1. routes/monitoring.js — flatten getAllStats() shape from
{current:{cpu:{percent},memory:{percent}}} to {cpu,memory,memoryUsage}
so the widget's Number() coercion actually produces numbers, not NaN.
Skill reference: references/totp-and-system-overview-pitfalls.md §3.
2. routes/health.js — add summary block to /health-checks/status response.
Widget looks for {healthy, unhealthy, total} but only per-service objects
existed. Permissive on healthy side (up|healthy|online), strict on
unhealthy (down|unhealthy|offline|error); anything else counted as
unknown. Same skill §3 reference.
3. middleware.js — add /api/v1/monitoring/stats to PUBLIC_ROUTES and the
rate-limit skip list. The widget polls it every 5s from the dashboard;
cookie-auth works but listing it explicitly makes it future-proof
against auth-cookie expiry and prevents per-second 429s.
End-to-end test (unauthenticated):
GET /api/v1/monitoring/stats -> {cpu: 8.71, memory: 0.37, ...}
GET /api/v1/health-checks/status -> {summary: {healthy:11, unhealthy:4, total:15}}
Three cascading bugs in server.js's workflow engine init block:
1. fetchT was referenced but never imported from ./src/utils/http
2. notification-manager was called as factory function but the module
now exports a class (NotificationManager) - need 'new'
3. servicesStateManager was referenced in workflowCtx but only created
later inside an async IIFE (out of scope at workflow init time)
Result: every container start logged
Workflow engine failed to initialize - fetchT is not defined
and the workflow engine never actually wired to resourceMonitor/
updateManager event sources. The 'app' context workflow engine
still ran but didn't get those connections.
Fix:
- Import fetchT at top of file
- Use 'new' for NotificationManager instantiation
- Hoist servicesStateManager creation before workflow init and
remove the duplicate inside the health-checker async IIFE
Verified: container restart shows
[server] Workflow engine initialized
[ResourceMonitor] Workflow engine configured
[UpdateManager] Workflow engine configured
in the log, no more errors at startup.
Also bumps VERSION to current SHA (bump from c64bbe2).
- credential-manager.js: add diagnose(key) method that distinguishes
ok | missing | unreadable | corrupt instead of silently returning null
- crypto-utils.js: silent fallback to .encryption-key.bak when primary
can't decrypt existing credentials; first-run bootstrap writes .bak;
rotateKey() backs up old key before swap
- routes/auth/totp.js: new public /api/v1/totp/recovery-info endpoint
returns {status, isSetUp, hint} so UI can show meaningful errors
- middleware.js: add /totp/recovery-info to PUBLIC_ROUTES so the
locked-out user can read the diagnostic without being logged in
Three logical changes grouped:
1. Widget bundle rebuild + sami-files logo (from previous session)
- status/dist/{init,core,features,onboarding}.js rebuilt from latest source
- status/sw.js cache bumped to dashcaddy-shell-594ec75648 to force SW refresh
- status/assets/sami-files.png added (Sami Files service card logo)
2. status/build.js: include monitoring-widgets.js in bundle
- The original build.js was missing monitoring-widgets.js from its JS()
bundle list — that's why the System Overview widget never showed up
in the live init.js until we ran the live /var/www/dashcaddy-status/
build.js. Now consistent.
3. dashcaddy-api/scripts/dashcaddy-update.sh restart_container(): preserve
TOTP secret across container recreates
- Was only setting SERVICES_FILE; container fell back to image-local
/app/credentials.json + /app/.encryption-key (auto-generated fresh
every recreate), which broke TOTP for the bind-mounted secret at
/app/data/credentials.json
- Added CREDENTIALS_FILE + ENCRYPTION_KEY_FILE env vars pointing at
/app/data/ so the container reads from the bind-mounted host data dir
- See skill: software-development/dashcaddy/references/totp-and-system-overview-pitfalls.md §9
4. Auto-updater integration (pulled from upstream release):
- dashcaddy-api/VERSION: dev → c64bbe2
- dashcaddy-api/health-checker.js, middleware.js, package.json,
routes/backups.js, src/app.js: new release code (bundled workflows,
/api/auth/ → /api/v1/ back-compat rewrite, backup storage limits)
- Recreate status/js/monitoring-widgets.js with robust services count
(reads from window.APPS, #cards DOM, then live fetch as fallback)
- Add sami-files service to data/services.json (Sami Files card)
- Add sami-files template to app-templates.js under 'Files' category
with full systemd deployment docs and Caddy snippet
- Bundle monitoring-widgets.js into init.js
After the DC-005 module reorganization (41 files moved into src/ subdirs),
138 test suites failed because the refactor script's path-rewrite logic
missed three categories:
1. Files inside src/ doing 'require("./src/...")' — should be 'require("../...")'
2. Files in src/X/Y/ doing 'require("../../../src/...")' — should be 'require("../../...")'
3. Test files in __tests__/ with leftover 'require("../../../src/...")' paths
Root cause: the original refactor script ran before all files were moved,
so it computed relative paths against stale filesystem state.
Result:
- 30/30 test suites pass
- 879/879 tests pass (was: 18/30 suites, 614/687 tests)
Also fixed:
- routes/apps/restore.js: wrong responses import path
- routes/*/*.js: '../../src/utilities/X' → '../src/utilities/X' (depth 2 routes)
Removed unused imports (path, validateStartupConfig, platformPaths),
renamed unused destructures (_timeout, _logEntry), replaced nested
ternaries with lookup tables, added eslint-disable comments on
require-await functions that are intentionally async for API stability,
and extracted helper functions to reduce max-depth and complexity in
app.js, dns.js, provider-dns.js, and site.js. All 879 tests pass.
comprehensive-test.js and test-security-fixes.js are 875 lines of
ad-hoc security test scripts (not Jest tests). They have zero references
in code or docs. Moved to scripts/legacy/ to declutter repo root
without losing the content. All 759 Jest tests still pass.
The 3 credential endpoints (POST/DELETE/GET /:serviceId/credentials) were missing
the /services/ path segment, causing 404s when tests called /api/services/<id>/credentials.
Fixed routes now match the URL pattern used by the live frontend
(/api/v1/services/<id>/credentials) and the test suite.
All 759 tests pass.
Convert ~160 raw res.json()/res.status().json() calls across 32+ files
to use centralized helpers from src/utils/responses.js (ok, errorResponse,
successMessage, notFound, validationError, forbidden, unauthorized, conflict).
No behavior changes — response shapes are identical. Future schema changes
(e.g., requestId envelope) only need to update one module.
Fix error vs errorResponse signature mismatch in routes/health.js CA cert
endpoint where error(res, message, statusCode) was being called with
errorResponse(res, statusCode, message, extras) argument order.
Files changed: middleware.js, csrf-protection.js, error-handler.js,
license-manager.js, src/app.js, and 27 route files.
Test suite: 755 pass / 4 pre-existing failures (services credential tests).
Three small cleanups for v1.14.0:
1. /caddy/cas now uses standard success envelope
Was: { status: 'success', data: { cas: caList } }
Now: { success: true, cas: caList }
Updated frontend service-infrastructure.js to match.
2. /api/health/ca now uses standard envelope + meaningful HTTP codes
Was: { status, message, daysUntilExpiration } with 200 on every error
Now: { success, caStatus, message|error, daysUntilExpiration }
with 200 / 404 / 500 as appropriate
caStatus field preserves the original 'healthy'/'warning'/'critical'/'error'
semantic so any future consumer of the CA-health state still has it.
Tests updated to match.
3. Dead timeout: keys in fetchT opts are now a warning, not a silent strip
src/utils/http.js:41 used to do without telling
anyone. Callers that wrote fetchT(url, { timeout: 5000 }) got the default
5s timeout with no indication that their explicit value was ignored.
Now it logs a warning naming the call site, then strips the key.
Fixed 4 call sites that had stale timeout: keys:
- src/context/caddy.js
- src/context/dns.js
- src/context/provider-dns.js
- routes/dns.js (2 places)
Two cleanups in one pass for the v1.14.0 'works on any platform' theme:
1. Response helpers — merged src/utils/responses.js and the root-level
response-helpers.js into a single module at src/utils/responses.js.
The old module had a richer set (created, noContent, validationError,
unauthorized, forbidden, notFound, conflict) and is now re-exported
from the new location. Updated 15 routes to import from
src/utils/responses and deleted the root response-helpers.js.
2. Error logger — error-handler.js now uses the unified
src/utils/logging.js#logError (same one src/app.js uses), so all errors
go to one log file with one rotation policy. Removed the dead
asyncHandler export (the real one is in src/utils/async-handler.js
and is used everywhere). Deleted the legacy error-logger.js.
Both are invisible to users — same HTTP response shapes, same log file
path, same error format. Internal-only refactor.
The readiness probe was using asyncHandler directly, but this codebase's
asyncHandler has signature (logError, fn, context) — first arg is the logger.
Switched to boundAsyncHandler which is what every other route in src/app.js
uses. Verified working on both DNS2 (Docker) and Contabo (systemd).
8 new tests in __tests__/health-endpoints.test.js verify both endpoints.
The readiness probe was crashing with 'res.status is not a function' because
asyncHandler(async (req, res) => {...}, 'health-ready') was called directly,
but asyncHandler's signature is (logError, fn, context) — first arg is the
logger, not the handler. The fix uses boundAsyncHandler like all other routes
in the file do.
Added 8 unit tests for both /health/live and /health/ready:
- live always 200 (liveness ≠ readiness)
- ready returns 503 when config/services/docker fail
- no 'res.status is not a function' crash when dependencies fail
- all 4 check keys present in response
Also added MONITORING_PUBLIC env var (defaults true) and the new health
endpoints to PUBLIC_ROUTES so k8s probes can hit them without auth.
By default /api/v1/monitoring/stats and /api/v1/health-checks/status are
public (current behavior, dashboard needs them pre-login). Users deploying
DashCaddy on the open internet can now set:
MONITORING_PUBLIC=false
...or add 'monitoring: { public: false }' to config.json to require auth.
This prevents anonymous disclosure of CPU/memory/disk data.
The check uses env var first, then config.json, then defaults to true
(preserves current behavior for existing users).
When config.json schema changes between versions, register a migration
function in src/config/migrations.js. On startup, loadSiteConfig() detects
the stored version, runs all migrations forward, and writes the result back.
Users never see the migration — it runs silently and the rest of the app
only ever sees the current schema.
Includes:
- v0 → v1: normalize dns from string to object
- v1 → v2: add dns.provider field (default 'technitium')
- Forward compat: configs from future versions left untouched
- Idempotent: re-running on already-migrated config is a no-op
- Safe: no user data is removed during migration
21 unit tests covering edge cases: null input, forward compat, corrupt
JSON, missing parent dirs, idempotency, full migration chain.
Cross-platform hardening — removes all hardcoded /app/ paths from route files
and routes them through platform-paths.js so the app works the same way
regardless of Docker layout (single-file mount vs consolidated data dir).
Changes:
- platform-paths.js: add generatedCertsDir, pkiDir, containerUpdatesDir,
containerFrontendDir, containerAssetsDir, resolveAssetsPath()
- self-updater.js: UPDATE_URL/MIRROR_URL/CHANNEL env var overrides
- routes/ca.js: use platformPaths for cert paths and generated certs dir
- routes/services.js: use platformPaths.pkiRootCert
- routes/themes.js: derive THEMES_DIR from platformPaths.servicesFile
- routes/config/assets.js + backup.js: use resolveAssetsPath() fallback
- routes/services.js + src/app.js: use platformPaths.pkiRootCert
- server.js: HOST env var support, parse PORT as int
- src/app.js: GET /api/v1/version (public, no auth), global request timeout,
disable x-powered-by, trust proxy
- pylon/dashcaddy-pylon.js: PYLON_HOST env var, graceful shutdown on SIGTERM/SIGINT
A fresh user can now deploy with a custom Docker layout (e.g. /opt/dc/data/
as a single volume mount) and the app finds its files automatically, no env
var configuration required.
The CREDENTIALS_FILE and ENCRYPTION_KEY_FILE env vars defaulted to
__dirname/credentials.json and __dirname/.encryption-key, which works
for the standard install (where individual files are mounted to /app/)
but breaks for deployments using a consolidated data directory at
/app/data/.
Add resolveCredentialsFile() and resolveKeyFile() helpers that:
1. Honor explicit env var if set
2. Check /app/credentials.json and /app/data/credentials.json
3. Check /app/.encryption-key and /app/data/.encryption-key
4. Default to standard path for new installs
This makes DashCaddy deployable with either pattern without requiring
custom env var configuration, which is essential for general-public
reproducibility.
- Add /api/v1/monitoring/stats and /api/v1/health-checks/status to PUBLIC_ROUTES
so the frontend widget can fetch without auth
- Transform monitoring stats response from nested {cpu:{percent}} to flat
{cpu: number, memory: number, memoryUsage: number} for the widget
- Add summary {healthy, unhealthy, total} to health-checks/status response
- deploy.js: wrap logError/notification in try/catch so they never mask the original deploy error
- deploy.js: use optional chaining for error.message access
- logging.js: safeErrorMessage handles null/undefined error gracefully
- dns-providers/: adapter base class + registry with auto-discovery
- technitium.js: wraps existing Technitium API calls into adapter interface
- cloudflare.js: Cloudflare API v4 adapter (zones, records, credentials)
- rfc2136.js: RFC 2136 dynamic DNS via nsupdate (BIND, PowerDNS, etc.)
- manual.js: no-op adapter for external DNS management with instructions
- provider-dns.js: provider-aware DNS context, resolves active adapter from config
- Universal helper methods: universalCreateRecord/Delete/ResolveRecord
- All 7 route files updated to use universal methods instead of raw dns.call()
- Setup wizard: provider dropdown (Technitium, Cloudflare, RFC 2136, Manual)
- DNS template selector: added Cloudflare and External/Manual options
- Config schema: validates dns.provider field
- Capability gating on Technitium-specific endpoints (logs, restart, update)
- Backward compatible: no provider set = auto-detect (technitium if dns.ip exists)
Service categories (described in README roadmap, never wired):
- Backend: POST /services now persists category/containerId/port/ip/tailscaleOnly
- Backend: POST /services/update accepts category for in-place changes
- Frontend: category <select> in add-service modal (local + external)
- Frontend: category <select> in edit-service modal with current value
- Frontend: All Categories dropdown in service filter bar (auto-populated
from both API categories and any categories present on rendered cards)
- Frontend: colored category badge (icon + name) on service cards
- Frontend: filter auto-refreshes after buildGrid
Monitoring on main dashboard (replaces orphaned monitoring-dashboard.html):
- New monitoring-widgets.js embeds a 5-card System Overview panel above
the filter bar: Services, Containers Up, Avg CPU, Avg Memory, Health
- Pulls /api/v1/monitoring/stats + /api/v1/health-checks/status
- Auto-refreshes on DC.POLL.STATS (5s), color-coded bars (warn >=65%, bad >=85%)
Build:
- Added monitoring-widgets.js to init.js bundle in build.js
- Rebuilt dist/ bundles (core.js, features.js, init.js)
- sw.js cache version bumped automatically
- CSP hash regenerated
- Add backup_data_dir() and restore_data_dir() using rsync
- Data backed up to backups/{version}/data-backup/ alongside code
- restore_data_dir() called in all three rollback paths (build fail, restart fail, health check fail)
- Add restart_container() that does rm + run to apply new env vars
- Handle action=rollback explicitly (no new version deployment)
- Uses standalone docker build instead of compose for reliability
- Add start.sh at /opt/dashcaddy/start.sh for reboot survival
The data/ directory (services.json, config.json, credentials,
TOTP config, notifications) was never included in the update
backup. Every update wiped user data — services, licenses,
credentials — requiring manual restore.
Now the host-side updater:
- Backs up data/ alongside code files before any update
- Restores data/ on rollback (build failure, restart failure,
or health-check failure)
- openClawRoutes was mounted at root causing /status vs /openclaw/status mismatch
- ctx.docker is a typed wrapper {client,pull,...} — all calls now use docker.client.*
- templates/deploy/removal/restore sub-routers had /apps/ hardcoded in inner routes
causing double-stacking when mounted under /apps (→ /apps/apps/templates etc)
- openclaw.js: GET /status, POST /deploy, GET/POST /proxy/*, DELETE /
1. WebSocket exec auth bypass (exec.js): Require valid JWT or API key
before accepting WebSocket upgrade. Reject unauthenticated requests
with 401 before the upgrade completes.
2. Shell injection in router auto-login (session-handlers.js): Validate
baseUrl against safe hostname pattern before embedding in wget shell
command. Reject with null session if invalid.
3. Path traversal in credentials routes (services.js): Add explicit
serviceId validation (alphanumeric + dash/underscore/dot, max 100
chars) to all three credential endpoints. Removed redundant
try/catch wrapper.
4. execSync injection in CA CSR generation (ca.js): Add sanitize step
replacing any non-alphanumeric domain chars with underscore before
interpolation into shell subj argument. Redundant with existing
validation but provides defense-in-depth.
5. Auth bypass when TOTP disabled (middleware.js): Split the logic
cleanly — disabled TOTP means no auth (initial setup state), enabled
TOTP means all auth methods checked (session/JWT/API key). Removed
the sessionDuration:never conflating shortcut.
- Set Domain=.sami on session + CSRF cookies so browsers send them to all subdomains
- This fixes Caddy forward_auth returning 401 for radarr/sonarr/prowlarr
- Fix login URL concatenation bug (radarr.samilogin -> radarr.sami/login)
- Fix getSetCookie() missing from _httpsFetch/_httpFetch response objects
- Fix array/string handling for set-cookie header in session-handlers fallback
- Refactor csrf-protection to createCSRFMiddleware() factory with cookieDomain support
- Pass renewCSRFToken through middleware deps chain to TOTP route