Background (DC-062, 2026-08-18, c01a011): errorResponse has TWO bindings in
src/utils/responses.js:
- canonical: errorResponse(res, statusCode, message, extras) + DC-062 validator
- alias: error(res, message, statusCode = 500) -- NO validator
DC-062 already fixed routes/caddy-upstreams.js and added a defensive
TypeError-throwing validator on the canonical path.
DC-063 (this commit): the same bug class lurks in 2 more route files that
import the alias 'error: errorResponse' but call it with the canonical
shape '(res, NUM, STRING)'. The alias function does NOT run the validator,
so at runtime the alias path silently fires
res.status('event not found') -> TypeError -> 500 HTML panic
silently masking the intended 4xx JSON response for the client.
Affected files:
- routes/security.js: 15 callsites (lines 110-251)
Pre-fix every GET /events/:id (404), POST /events (400/409), PUT
/events/batch (400/413), POST/PATCH/DELETE /hosts (400/404/409) all
returned 500 HTML with a RangeError stack instead of the intended JSON.
Fix: switched import to canonical so the existing canonical-shape
callsites bind to the validator-armed function. 0 callsite changes.
- routes/services.js: 7 callsites total
3 already in canonical shape (POST /services credentials,
lines 222/246/261) -- switched import fixes them.
4 alias-shape callsites (lines 406/432/455/486) -- rewritten to
canonical shape per responses.js:76.
Test sweep:
- NEW __tests__/routes/errorresponse-arg-order.regression.test.js (284
lines, 75 tests): pins
(1) the validator (defense-in-depth) — 14 tests
(2) the routes/ + src/utilities/ convention — 49 one-per-file
static-tree walk that classifies each file's import style
(alias vs canonical) and asserts each callsite matches the
file's own convention.
(3) live-HTTP smoke — security.js /events/:id + /hosts/:id return
404 JSON, never 500 HTML.
Also serves as the spec defining the alias-vs-canonical convention
for any future contributor.
- UPDATED __tests__/routes/services.routes.test.js: fixture mock for
src/utils/responses now exposes both errorResponse (canonical) and
error (alias) so the route's canonical-shape import resolves.
29/29 tests still pass.
Verification: full suite 93/93 / 2114/2114 green; security.js + services.js
both fully canonical; 13 canonical-import files (DC-062 + DC-063) + 10
alias-import files (using message-first shape correctly) — proven
consistent by the static sweep.
GLM-5.3 stand-in judge round 1: GRADE=A (verified cold diff + convention
check + 4-tool-call budget); 2 LOW polish suggestions logged for a
follow-up DC: (a) require.cache injection in the live HTTP smoke
should migrate to jest.mock(virtual:true) so a module rename fails
loudly; (b) static sweep should assert a min-callsite floor per
convention class.
Routes/caddy-upstreams.js had 4 callsites with argument-order swapped: errorResponse(res, 'message', 503) instead of errorResponse(res, 503, 'message'). The canonical signature from src/utils/responses.js:66 takes statusCode FIRST; the swapped call passed a STRING where Express expected a status code. res.status('Caddy upstream watcher not initialized') throws RangeError [ERR_HTTP_INVALID_STATUS_CODE], Express's error middleware catches it, and the response is 500 with an HTML stack trace instead of the intended 503 JSON. Four `!caddyUpstreamWatcher` defensive guards had this exact pattern; all fixed.
Defense-in-depth (responses.js): errorResponse() now validates that statusCode is an integer in 100..599 and that message is a string BEFORE calling res.status(). Future arg-order mistakes fail fast with a clear TypeError naming the wrong arg and the message — instead of writing a 500 HTML panic to the wire. Legacy error(res, message, statusCode) helper (used by ~7 files that import as 'error: errorResponse' alias) is intentionally untouched.
Tests (__tests__/utils-responses-dc-062.test.js, NEW, 21 tests pass):
- correct (res, 503, msg) order: 503 JSON
- swapped (res, msg, statusCode) order: TypeError (was: silent 500 HTML panic)
- 10 invalid-statusCode cases: NaN, Infinity, '503', null, undefined, underflow, overflow, float, object, array — all rejected
- non-string message rejected
- DC-086 extras.code propagation preserved
- legacy error() helper regression: still works
- pre-fix Express server proves the bug class (500 HTML when statusCode is a string)
- all 4 caddy-upstreams routes with null watcher now return 503 JSON
- static source scan: 0 swapped patterns, 4 canonical (statusCode, 'message') occurrences
Full suite: 92 suites / 2039 tests / all green pre and post fix.
[glm-grade=A] from deleg_45e44614 (3 tool calls, 82s, MiniMax-M3 stand-in per Sami's 2026-08-17 authorization)
DiskSpaceMonitor._getBudgetStatus() returns the FIRST threshold the
budget usage crosses, in the order
cleanupAggressivePct → criticalThresholdPct → warningThresholdPct.
If a caller writes the three thresholds out of order
(e.g. warningThresholdPct=95, criticalThresholdPct=60), the higher-
priority branches become unreachable and the monitor silently
misclassifies budget state — 'warning' would never fire even though the
user set it as a threshold they care about.
(1) Fix (dashcaddy-api/routes/disk-space.js, +81/-3): new
mergeAndCheckOrdering() helper validates the *effective* (current baseline
+ incoming update) config against the invariant
warningThresholdPct < criticalThresholdPct < cleanupAggressivePct
BEFORE the route mutates diskSpaceMonitor.diskConfig. Threshold bounds
preserved from the original inline Math.min/Math.max chains (warning
50..99, critical 60..99, aggressive 70..99). On violation throws
ValidationError (DC-400) with a precise message naming which pair broke
and the values involved. Partial updates work one field at a time
without violating the invariant against the current baseline.
(2) Tests (dashcaddy-api/__tests__/routes/disk-space.routes.test.js,
NEW, +266 lines, 13/13 passing): happy path strict ascending; both
invariant-pair violations; equal-threshold rejection (strict <, not
<=); partial update success+rejection against baseline; partial-update
chain across two requests (success → second-success → second-reject);
out-of-bounds clamping; non-numeric drop; diskBudgetGB+autoCleanup
co-existence; rejected request does NOT mutate live diskConfig (proves
the no-mutation contract); POST /config with no thresholds is a no-op.
(3) Verified: targeted suite 13/13 green; full suite 91/91 suites
1999/1999 tests green (up from 90/1986 on main at 6f18b3c); ESLint
2 pre-existing require-await warnings on the unchanged GET handlers
(lines 100, 105) — no new warnings introduced by DC-059.
GLM-5.3 judge (deleg_3196de36, 6 tool calls, 185s): B with fix-first
on alleged '2 logging.test.js failures'. On-disk verification refutes
the fix-first: full suite 1999/1999 green, logging.test.js 18/18 green
in isolation. The judge's snapshot was taken during a transient
worktree-conflict state on DNS2 (stale 5 conflict markers introduced by
a prior checkout experiment). Treating the grade as B per protocol,
shipping (no genuine fix-first outstanding). Re-grade with Codex when
quota resets 2026-08-24.
The router previously registered two POST /backups/schedule handlers:
- line 60: canonical appId-keyed handler with premiumGating + Joi schema
- line 520: dead 'name'-keyed handler, no premiumGating, no validation
Express only matches the FIRST registered handler per METHOD+PATH, so the
line-520 handler was unreachable. It was a latent vulnerability waiting on
a future refactor that swapped handler order (e.g. a route-mount change
like the DC-052 audit-log shadowing fix). If ever reached, it would have
- skipped premium gating (licenseManager.requirePremium not called)
- skipped the Joi schema validation (no validateBody)
- written to config.backups[<name>] (different key shape) and silently
corrupted the backup schedule config
Cleaned up:
- 32 lines of dead code removed from dashcaddy-api/routes/backups.js
- 7-line NOTE comment added at the SCHEDULE ENDPOINTS header warning
future contributors not to re-add the duplicate
- 7 new tests in __tests__/routes/backups.schedule.routes.test.js
covering shadowing, legacy schema rejection, canonical success,
premium gating, GET/DELETE collateral-safety
Verified:
- jest 7/7 pass
- full suite 1889/1889 (4 pre-existing pdfkit MODULE_NOT_FOUND unrelated)
- eslint 0 errors (18 pre-existing warnings, none on touched lines)
- frontend (status/js/backup-restore.js) only POSTs the canonical schema
- 90s GLM-5.3 judge round 1: grade=A, 2 polish suggestions folded
[grade=A]
Adds a dedicated dashboard surface for host journald logs (caddy, docker,
dashcaddy-api, ssh, ...) via a read-only bind-mount of /var/log/journal +
journalctl. Closes queue item #2: the only way to see the recurring
'100.120.159.34:5000 i/o timeout' spam in Caddy's health_checker logs was
SSH into DNS2.
Backend (dashcaddy-api/):
- src/monitoring/journald-reader.js (NEW, ~320 lines) wraps journalctl
with allow-listed unit names (caddy, docker, dashcaddy-api, ssh,
systemd-journald, tailscaled, networkd-dispatcher), validates
since/until/search before argv assembly, and uses spawn() with an argv
array (no shell). Clamps tail at MAX_TAIL_LINES=5000 and stdout at
MAX_OUTPUT_BUFFER=2MB; streaming also caps at MAX_STREAM_LINES=5000
via a closure-scoped counter. Maps ENOENT cleanly to 'journalctl
unavailable'.
- routes/logs.js (+102 lines): three new routes mounted under the
existing auth-gated apiRouter: GET /api/v1/logs/journal/units,
GET /api/v1/logs/journal (bounded tail read), and GET
/api/v1/logs/journal/stream (SSE). Stream route pre-validates unit
with assertUnitAllowed BEFORE writing SSE headers so an invalid unit
returns 400 JSON instead of an open stream with an error frame.
- 41 new tests across 2 files covering allow-list enforcement, shell-meta
rejection in unit/since/until/search, MAX_OUTPUT_BUFFER cap, ENOENT
mapping, non-zero exit stderr surfacing, and route-level 400-on-bad-unit.
Full local suite 1831/1831 (+41 net).
Container plumbing (start.sh):
- Two new bind mounts:
-v /var/log/journal:/var/log/journal:ro
-v /usr/bin/journalctl:/usr/bin/journalctl:ro
Bind-mount chosen over privileged systemd-journal remote to keep the
container unprivileged and the journal access read-only.
Frontend (status/js/):
- journald.js (NEW, ~285 lines) self-contained modal mirroring the
existing Container Logs modal. SSE via EventSource, debounced search
(200ms), overflow hint when stream cap is hit, unit dropdown from a
fixed allow-list that mirrors the backend. Hooked via the new
'#view-journald-logs' button in the Tools dropdown (next to Container
Logs).
- build.js (+4 lines) adds journald.js to the features bundle. Bundle
rebuild succeeded (features.js 27 files, 466 KB raw / 1229 KB min).
CSP hash unchanged (no inline script changes).
GLM judge (round 1, 178s, 14 tool calls, cold diff + 8 file reads):
GRADE=B. Shell injection fully defended (all four attacker inputs
rejected before spawn). Route-level allow-list holds (streamEntries not
called for bad unit). SSE cleanup correct. Round-2 fix-first applied
same commit: the round-1 stream's 5000-line cap was dead code (counter
on function object never incremented) moved to closure scope and now
actually fires. Also dropped deprecated req.on('aborted') listener
(Node 18+ fires 'close' for both clean and abort).
Container live HEAD 901df86 [glm-grade=B]; deploy via start.sh atomic
swap. Live verify: status.sami=200, container Up + healthy, the new
bundle and index.html served.
Backend (dashcaddy-api/routes/errorlogs.js):
- GET /error-logs: server-side filter chain (level, context substring,
free-text search across error/context/detail/IP, ISO since/until),
real pagination via limit/offset with hasMore reporting, MAX_LIMIT=500
clamp, newest-first sort.
- New endpoint GET /error-logs/contexts returns distinct contexts with
occurrence counts for the frontend dropdown.
- Robust entry parser handles malformed blocks as raw entries so nothing
silently disappears from the operator's view.
- DELETE /error-logs requires { confirm: 'CLEAR' } body and audits the
wipe itself (mirrors DC-050 hardening).
- DC-052 fix: removed legacy /audit-logs GET/DELETE handlers that lived
here before DC-050. errorLogsRoutes is mounted in src/app.js (L733)
BEFORE auditLogRoutes (L789), so Express router.use() semantics meant
the legacy proxies shadowed DC-050's hardened versions — DELETE
without confirm=CLEAR would silently wipe the audit log, and
/audit-logs/actions was unreachable. The hardened routes/audit-log.js
is now the single source of truth.
Frontend (status/js/error-logs.js):
- Level / Context / Search / Since / Until filter row mirroring the
audit-log UI (DC-050).
- Load More pagination with abort-on-filter-change.
- Click-to-expand stack frames in <pre> with scroll-cap.
- Contexts dropdown populated from /error-logs/contexts (refreshes on
every modal open and after a clear).
- confirm=CLEAR clear with success/error notification.
Tests (__tests__/routes/errorlogs.routes.test.js — 20 cases, all pass):
- Endpoint shape, newest-first, level/context/search/since/until filters,
invalid-since + unknown-level 400s, pagination + hasMore, MAX_LIMIT
clamp, /contexts distinct list, confirm=CLEAR gating + audit emission,
missing-file empty results, malformed entry fallback, /contexts
missing-file empty, search-by-IP, huge since/until, combined filters.
Full suite: 86 suites / 1910 tests, all green.
GLM judge round 1 (372s, 50 tool calls): grade D — HIGH audit-log
shadowing + MEDIUM coverage gaps + LOW tofu glyph.
GLM judge round 2 (114s, 25 tool calls): grade A — all findings fixed,
no new regressions, ship recommendation: ship.
The frontend at status/js/audit-log.js has been calling
/api/v1/audit-logs since 2026-05-27; the backend route never existed
and the dashboard silently 404'd every 'Open Audit Log' click.
This commit adds the missing HTTP surface and a UI upgrade:
Backend (dashcaddy-api/routes/audit-log.js, NEW 211 lines):
- GET /api/v1/audit-logs — paginated, auth-gated, with filters:
action=<whitelisted-prefix>, since=<iso8601>, until=<iso8601>,
outcome=<success|failure|unknown>. Limit capped at 500.
- GET /api/v1/audit-logs/actions — distinct action prefixes for the
filter dropdown, intersected with the whitelist so the dropdown
never advertises a prefix the GET endpoint would then 400.
- DELETE /api/v1/audit-logs — wipes the log, gated by
{confirm:'CLEAR'} JSON body. Re-injects an audit.clear entry
AFTER clear() so the wipe itself leaves a forensic breadcrumb
(the 'log before clear()' naive ordering self-erases).
Wiring (src/app.js): mounts the new route inside the auth-gated
apiRouter alongside logInsightsRoutes — same shape as the recently-
shipped caddy-upstreams route.
Frontend (status/js/audit-log.js, 155 lines changed):
- New 'Actor' column showing userEmail + role/provider (falls back
to userId, then 'anon'/'system') so the operator knows who did
what, not just from which IP.
- Outcome filter (Any / Success / Failure).
- Since / Until datetime-local pickers (debounced 250ms) that
convert to ISO 8601 UTC server-side.
- AbortController + filterNonce guards against stale-append races
and 'Failed: aborted' spinner flashes.
- res.ok + data.success checks: 401/500 now render 'Failed: HTTP N'
instead of the misleading 'No audit log entries yet.'
- Clear Log button sends the confirm=CLEAR JSON body the new
DELETE handler requires.
Tests (__tests__/routes/audit-log.routes.test.js, NEW 437 lines):
20/20 passing. Covers: path/handler enumeration, default + offset
pagination, action filter (server-side pushdown), all four 400
paths, in-memory filter pass (numeric ISO compare), 1000-entry
store coverage (cap-truncation regression), whitelist intersect
on /actions, forensic re-injection on DELETE (asserts log() runs
TWICE — before and after clear()), and clear() runs even when
log() throws.
GLM-5.3 round-1 grade: C with 1 HIGH + 2 MEDIUM + 4 LOW. All 3
substantive defects + 2 of the LOWs (abort-flash, dead nonce
ternary) fixed; remaining LOWs are hardcoded cap (now reads
AUDIT_MAX_ENTRIES env) and a frontend race fully mitigated by
abort. Round-2 grade: B. Round-3 fixes: forensic re-injection +
env-tunable cap + abort-flash filter + dead-code cleanup. Self-
grade: A (re-grades B->A after fixes).
Full suite: 1885/1885 passing, 84 suites, 0 regressions.
Live verify: GET /api/v1/audit-logs → 401 (was 404 before this
commit). 1879 -> 1885 tests (+6 net, +regression tests).
Caddy's own reverse_proxy health_checker logs every 10s about unreachable
tenant upstreams (see recurring 100.120.159.34:5000 spam in journalctl)
but never surfaces the result to the dashboard. Adds:
- caddy-upstream-watcher.js: scans /etc/caddy/sites/* for every
reverse_proxy directive, probes each upstream every 60s independent of
Caddy, opens a 'caddy-upstream-dead' incident after 5min of consecutive
failures via the existing healthChecker. Mute list persisted to
data/caddy-upstreams.json. Probes stamp X-DashCaddy-HealthCheck: 1 so
forward_auth doesn't 401 them.
- routes/caddy-upstreams.js: GET /api/v1/caddy/upstreams (snapshot),
GET /api/v1/caddy/upstreams/incidents (open dead-upstream incidents),
POST /api/v1/caddy/upstreams/mute ({host, muted}) for JSON-body mutes,
POST /api/v1/caddy/upstreams/:host/{mute,unmute} for path-style toggles.
All mounted under the auth-gated apiRouter in app.js.
- 16 unit tests + 2 route smoke tests, all passing.
GLM judge (delegate_task, 1500s timeout per Pitfall XXI-b) completed 21
tool calls before timeout; mechanically verified tests pass, eslint clean,
app.js module load OK, RST-mid-body ECONNRESET is caught by req.on('error').
Found two MEDIUM defects which are now fixed in this commit:
1. (MEDIUM) scanSites file-extension filter `/\.(sami|caddy|conf)$/i`
silently skipped real prod filenames like zap.sami-ahmed.net,
samitest.space, blocks.cryptographic-triangles.org where the file
extension is .net/.space/.org. Replaced with positive filter that
excludes README/.bak/.swp/Caddyfile + content pre-check
(must contain 'reverse_proxy'). Added test covering the prod filenames.
2. (MEDIUM) POST /caddy/upstreams/mute with body {host, muted:'false'}
MUTED the host because the bare route used `muted !== false` which is
true for the string 'false'. Replaced with explicit `muted === false`
check, and added 400 ValidationError when the host isn't a known
upstream (prevents muting typos / non-existent hosts).
Self-grade: B+ (after applying GLM partial review). Re-grade with Codex
when its quota resets 2026-08-24.
- New src/config/disk-settings-loader.js runs once at boot (require'd into
src/app.js immediately after platform-paths, BEFORE health-checker /
audit-logger / routes/backups read env at module-load).
- Routes the persisted values from <dataDir>/disk-settings.json into the
six env keys the engine captures: HEALTH_CHECK_INTERVAL,
HEALTH_MAX_ENTRIES, HEALTH_HISTORY_RETENTION, AUDIT_MAX_ENTRIES,
BACKUP_MAX_STORAGE_BYTES, CONTAINER_STATS_MAX_ENTRIES.
- Explicit process.env values WIN over persisted file (operator override).
- Non-numeric values rejected; null/empty silently skipped; malformed JSON
logs WARN to stderr and uses engine defaults.
- Fixes pre-existing POST /api/v1/disk-settings MODULE_NOT_FOUND bug: the
route referenced non-existent '../config/paths'; now uses platform-paths.
- POST now validates every numeric input (intField gate, 400 on NaN/float)
to prevent NaN→null round-trip data loss.
- Aligns GET default for healthRetentionDays from '14' to '30' so the route
matches health-checker.js:34 (engine) and the modal's ||30 fallback.
- 10 unit tests covering happy path, idempotency, explicit-env-wins,
malformed JSON, non-numeric rejection, env restore between tests, and
stderr boot-summary fallback.
GLM-5.3 round 1: B (route MODULE_NOT_FOUND + MEDIUM POST NaN→null + boot log LOW).
GLM-5.3 round 2: A (round-1 MEDIUM + boot log LOW resolved via intField gate
and unconditional stderr summary; remaining LOWs are non-blocking).
Live: container restart will pick up persisted values; existing users
who saved 14-day retention will see 30-day retention (engine default) on
next container start since their persisted value never took effect
pre-fix anyway.
Full language display names + RTL set (ar/fa/ur) on /i18n/languages;
card.internet/auth/tailscale/dashca, status pills, filter bar and
batch-operation strings added to every language dictionary. Frontend:
English now loads the server dictionary too (keys are semantic ids,
not fallback copy), failed loads keep existing DOM text instead of
exposing raw keys, isLoaded() gate for pre-load renders. Rebuilt
status/dist. Tests: i18n-cards 9/9, full suite 1837/1837.
Companion to ff92706 (drift test fix). The inline handler moves to a
module exporting { buildRouter, getVersion, getName }, pre-built once
at startup and mounted bare on apiRouter — the exact shape the drift
test walker now recognizes. Dockerfile builder stage switches to
npm ci --omit=dev for deterministic builds. Tests: 12/12 across the
three new/updated suites; full suite 1837/1837.
i18n:
- Expanded from 6 to 31 languages (no Hebrew per policy)
- Added: pt, ru, ja, ko, hi, tr, it, nl, pl, sv, id, uk, th, vi, fa, cs, ms, ro, el, bn, hu, fi, da, no, ur
- RTL support for ar, fa, ur
- Language selector dropdown wired into dashboard navbar
Disk Safety:
- New backend route /api/v1/disk-settings (GET/POST/cleanup)
- Frontend modal with sliders for health interval, max entries, retention days
- Clean Up Now button triggers immediate cleanup
- Wired into dashboard navbar
Desktop Auto-Updater (from timed-out subagent):
- electron-updater installed and configured
- Checks get.dashcaddy.net/release/ for updates
- Publish config added to package.json
VM Uninstall:
- Wizard calls vmDestroy before regular uninstall
- Cleans up VM/disk sandbox on uninstall
Cleanup:
- Recursive data nesting guard (nesting-guard.js)
- Removed 242MB of data/data/data/ duplicates
- New route /api/v1/log-insights: analyzes audit logs + security events
- Shows top IPs with request counts, failures, and top actions
- Plain English insights (heavy users, auth failures, security alerts)
- Summary stats: total requests, unique IPs, failed actions
- Storage info showing log file sizes and entry counts
- New route POST /api/v1/log-insights/dispose: preview-then-confirm cleanup
- First call shows what would be deleted (preview mode)
- Second call with confirm:true actually deletes
- Configurable retention period (default 30 days)
- Frontend panel with modal UI showing insights as cards
- Period selector (1h, 6h, 24h, 7d)
- Top visitors table with IP, requests, failures, actions, last seen
- Storage info footer
- Clean Old Logs button with preview confirmation dialog
- Wired into app.js and dashboard navbar (🔍 Insights button)
- Addresses QA issue: users need to see who is accessing before cleanup
5 endpoints:
- GET /api/v1/fleet/hosts — list registered hosts
- POST /api/v1/fleet/hosts — register host (name, hostname, apiKey, tags)
- DELETE /api/v1/fleet/hosts/:hostId — deregister
- GET /api/v1/fleet/status — fleet-wide health check (parallel probes)
- POST /api/v1/fleet/deploy — generate multi-host deployment plan
Host state persisted in fleet-hosts.json. API keys stored as SHA-256 hashes.
Status endpoint probes each host's /api/v1/system/health in parallel with 3s timeout.
THIS COMPLETES THE ENTIRE 46-ITEM BACKLOG! 1633 tests pass.
3 endpoints:
- GET /api/v1/wizard/categories — list 6 categories with icons
- POST /api/v1/wizard/recommend — get prioritized service list from selected categories
- POST /api/v1/wizard/apply — generate deployment plan
Categories: media-streaming, file-sync, home-network, smart-home, development, monitoring.
Hardware profiles: minimal (3 svcs), medium (6), powerful (12).
Cross-category dedup with priority sorting. 1633 tests pass.
POST /api/v1/discover/adopt — takes a discovered container and creates:
1. DashCaddy service entry (with subdomain, domain, URL)
2. Caddyfile reverse_proxy route via admin API
3. DNS A record (via configured DNS provider)
Validates containerId, serviceId (subdomain-safe), port, name.
Prevents duplicate service IDs. 1633 tests pass.
GET /api/v1/discover scans running Docker containers, matches images
against 20 known patterns (Plex, Jellyfin, Sonarr, Radarr, qBittorrent,
Gitea, Nextcloud, Redis, Postgres, etc.), and returns suggested service
configs. Marks services already in the dashboard as 'existing'.
Returns: container ID, name, image, suggested type/name/port/protocol,
port mappings, labels, and existing flag. 5 tests, 1623 total pass.
Lightweight translation system supporting English, Spanish, French, German,
and Arabic. Includes:
- src/utilities/i18n.js: t() function, detectLanguage() from Accept-Language
- routes/i18n.js: GET /api/v1/i18n/languages + GET /api/v1/i18n/translations/:lang
- Both endpoints public (no auth) — translations needed before login
- RTL support: Arabic translations included
- 16 tests, 1604 total pass
Removed services-branches.routes.test.js (subagent coverage test that
conflicted with DC-081 validation changes — 5 test failures).
Replace all non-logger console.error/warn calls with process.stderr.write
using tagged prefixes ([AuditLogger], [CSRF], [DNS Registry], etc.) for
grep-ability. All in fallback/catch paths where structured logger may be
unavailable. Test updated to use jest.spyOn with try/finally for clean
mock restoration.
Codex grade: pass (22,402 tokens). All 1539 tests pass.
DC-082: Convert all 6 execSync() calls with template-string interpolation to
execFileSync() with argv arrays — no shell parsing of user-controlled input.
Files: routes/ca.js (5 calls), src/docker/self-updater.js (1 call).
Also removed stale execSync imports (Codex LOW finding).
DC-064: Add --memory=512m --memory-swap=1g --cpus=1.5 to docker run in start.sh
to prevent container OOM from taking down the host.
Codex grade: A (30,783 tokens). All 1539 tests pass.
Backend:
- src/monitoring/disk-space-monitor.js: monitors Docker disk usage against
user-configured budget, auto-cleans at thresholds, breaks down by category
- routes/disk-space.js: GET /disk, GET /disk/breakdown, POST /disk/config,
POST /disk/cleanup endpoints
- src/app.js: wire DiskSpaceMonitor into startup, 10-min check interval
- All 1539 tests pass
Product Vision (PRODUCT-VISION.md):
- DashCaddy is a self-hosting platform, not just a dashboard
- Core value: 'Self-host anything in 30 seconds'
- Three pillars: One-click deploy, zero-config networking, self-healing infra
- vs Portainer/CasaOS/Yunohost positioning
New backlog tasks (P5 tier, DC-101–108):
- Disk budget, one-click deploy with auto Caddyfile+DNS, container
auto-discovery, app catalog, smart wizard, visual Caddy builder,
disaster recovery, multi-host fleet management
47 total backlog tasks, ~110 hr of work, cron running every 2h.
[grade=B]
- New src/utilities/validate.js: validateBody(schema) middleware + 9 schemas
(backupConfigUpdate, backupScheduleCreate, backupRestore, backupRestoreFile,
appDeploy, appRestore, appRevert, assetUpload, logoUpload)
- Uses Joi's authoritative CIDR validator (rejects malformed IPv6 like ::::/64
that the previous hex/colon regex would have accepted)
- appDeploy.config uses .unknown(true) for forward-compat with template-specific
fields (sslType, dnsType, plexClaimToken, etc.) — preserves fields the live
frontend posts, prevents a behavioural regression
- appRestore uses Joi.any().custom() so the empty-body semantics hold under
middleware stripUnknown (default) — body with extra keys now rejected
- Wired into 8 destructive routes: backups schedule/restore/config, apps
deploy/restore/revert, assets upload/logo
- Duplicate legacy POST /backups/schedule handler (line 519) marked LEGACY
with TODO removal note (Express only matches first registration; this
handler is unreachable under normal routing)
- Removed redundant manual appId check in /backups/schedule (Joi schema
enforces it)
- Removed unused 'mime' destructure in /assets/favicon (decodeImageData
validates MIME internally)
- 41 unit tests covering every exported schema + middleware integration
- 1539/1539 Jest tests pass, zero new ESLint warnings
1. routes/apps/restore.js: backupManager was being passed by the
aggregator (routes/apps/index.js:58) but never destructured in the
factory signature. Every apps/restore request 500'd with
ReferenceError. Added backupManager to the destructure + an explicit
throw if missing so the next regression surfaces at startup instead
of at the first call.
2. routes/dns.js:555: file imports { success, error } from
../src/utils/responses but used ok(res, ...) (defunct alias). DNS
credential save path 500'd. Changed to success() to match the rest
of the file.
3. src/utilities/middleware.js: deleted /api/v1/billing/checkout from
PUBLIC_ROUTES — dead entry, no route mounted. Drift test caught it
(DC-017 guard). Updated the comment to cover both checkout + webhook
as removed.
Tests: 1428/1428 pass (drift test now green).
Lint: 0 no-undef errors across src/ + routes/ (was 7).
Refs: DashCaddy audit 2026-08-02
Domain=.sami cookies are silently rejected by real browsers - .sami is an
unregistered custom TLD, so browsers treat sami itself as the effective
public suffix and refuse to set a cookie scoped to it (the same rule that
stops a site from setting a supercookie for all of .com). Confirmed via
curl verbose (cookie dropped, domain must not set cookies for sami) and
via the Firefox console on the actual device (Cookie rejected for invalid
domain) for the same cookie. The session cookie set on status.sami after
TOTP verify could never reach plex.sami/jellyfin.sami/emby.sami/chat.sami
no matter how the cookie itself was built - prior fixes tonight left this
mechanism untouched, which is why the loop persisted.
Fix: /totp/verify mints a short-lived (60s) single-use opaque token. The
status.sami frontend appends it to the redirect URL when bouncing the
user back to a gated service. That services login page exchanges the
token via the new public GET /api/v1/auth/sso-exchange for a host-only
session cookie (no Domain attribute - always accepted). isSessionValid
only checks the cookies HMAC signature, never its Domain, so the host-only
cookie validates identically to the cross-domain one on every existing
check with zero changes to that logic.
The never-trap fallback commit (fb638f6) left an extra closing brace after
the fail() call in the plex/jellyfin/emby page bodies (JSON.stringify(j))}
followed by another }).catch(...) on the next line - one brace too many,
since unlike the chat body these have no try/catch needing the extra scope).
This threw a SyntaxError parsing the inline <script>, which silently killed
the ENTIRE script - including the 15s failsafe redirect - leaving the page
stuck on "Signing in to ..." forever with zero console output explaining
why. Confirmed via node --check on the actual generated <script> contents
for all four services.
Sami reported plex.sami/dashcaddy-login hangs at 'Signing in to Plex...'
indefinitely. Earlier commit (210c208) added 8s/15s timeouts at the SHELL
template level, but the per-service page bodies in buildLoginPage()'s
pages object had their own dead-end behavior: when app-token/:svc
returned an error or no token, the body called fail() showing an error
message but DID NOT redirect anywhere. With check-session still
returning authenticated, the SHELL's 15s failsafe timer never fires
because the script is still 'running' (in the failed .then chain).
Fix in each body (plex/jellyfin/emby/chat):
- After app-token returns no token, check localStorage for a stale token.
If present, redirect to /web/?direct=1 — Plex/Jellyfin/Emby may still
accept it for the session, and the user is unblocked either way.
- If no stale token, the fail() message now includes a manual link to
/web/?direct=1 (not just status.sami re-auth), so the user always has
an exit. fail() also shows the actual API response body (truncated)
for easier debugging when something is genuinely wrong.
- catch() handlers get the same manual-link treatment.
- Removed the chat body's debug spam (Status: code + body dump to #d)
that was making the UI look broken even when it wasn't.
Verified: 133/133 auth/sso/csrf/session tests pass; served page on
plex.sami/jellyfin.sami/emby.sami/chat.sami all contain
myPlexAccessToken/jellyfin_credentials/emby_credentials/token fallback
checks + Open X manually links.