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
The /api/v1/services/status endpoint (dashboard card ON/OFF) uses an
HTTPS agent to probe each service. When /app/pki/root.crt is missing
inside the container, it fell back to new https.Agent() which rejects
self-signed certificates. This caused all .sami domain probes to fail
with UNABLE_TO_GET_ISSUER_CERT_LOCALLY, making dashboard cards randomly
flip between ON and OFF depending on whether the Pylon relay responded
before the 10s deadline.
Fix: use rejectUnauthorized: false as fallback when CA cert is absent.
The 1.5.0 API cutover removed the legacy `/api` mount, but the
`dashcaddy_auth` Caddy snippet generated by the installer still called
`uri /api/auth/gate/{args[0]}`. forward_auth then hit the notFoundHandler
and propagated a DC-404 to the browser instead of a 401-redirect to the
login flow (seen on requests.sami).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove legacy /api/ mount; all routes now under /api/v1/ only
- Update path matchers (CSRF excludes, public routes, audit log, rate limits)
- Move standalone routes (/api/network/ips, /api/docs, /api/docs/spec) to v1
- Update openapi.yaml (110 paths), CA pages, and 4 lingering frontend files
- Add LICENSE (proprietary EULA), CHANGELOG.md (Keep a Changelog format)
- Add .gitea/workflows/ci.yml (test+lint and security audit jobs)
- Fix 9 pre-existing no-empty lint errors so CI starts green
- Drop ad-hoc scratch reports and *.bak files from repo root
All 739 jest tests pass. Lint is clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
build.js rewrites three things during `node build.js`:
- status/dist/*.js (bundle output)
- status/index.html (CSP hash for inline bootstrap)
- status/sw.js (cache name derived from bundle content)
release.sh was only staging status/dist/. Result: when a release didn't
touch index.html or sw.js source, the post-build modifications to those
two files were left unstaged, the commit included only dist/, and the
tarball shipped the stale sw.js. Clients then kept the previous SW
cache name -> activate handler never wiped the cache -> precached old
bundles served forever even after they were "updated" on disk.
Now stage index.html and sw.js too. They're tracked (not gitignored)
so a plain `git add` is enough; the commit is a no-op when nothing
actually changed.
container-logs.js called `wireModal(modal, null, closeModal)` — passing
the local `closeModal()` function as a third arg where wireModal expects
button elements. wireModal then did `closeModal.addEventListener('click',...)`,
threw TypeError, and because each module's IIFE is a top-level statement
in the concatenated features.js bundle, every IIFE *after* container-logs
silently skipped: snapshot, smart-arr-connect, notification-settings,
panel-tabs, backup-restore, resource-monitor, health-check, update-
management, docker-resources, compose-import, container-exec, audit-log,
weather, clock, card-badges, theme-builder, and license. Symptoms:
"Customize Theme" did nothing on click, license badge stuck at "FREE TIER"
(because license.js never ran), no weather, etc.
- container-logs.js: drop the wireModal call, wire backdrop click directly
to the local closeModal so the SSE log stream actually stops on close.
- globals.js: harden wireModal — skip any closeBtn that isn't a real
EventTarget. One typo upstream shouldn't take down the rest of features.js
init silently.
When the build runs on a Windows checkout, fs.readFileSync returns the
file with CRLF intact, and the hash of the inline bootstrap script's
body reflects those CRLFs. The release tarball / git transport / Linux
file system strip CRLF on the publishing host, so the browser sees the
LF-only version and computes a different sha256. CSP then blocks the
script — disabling the version widget, theme switcher, and any other
DOM bindings set up in that inline block.
Normalize CRLF -> LF before computing the hash (the on-disk file keeps
its native line endings; only the hash input is normalized). The CSP
allowlist now matches whatever Caddy actually serves.
Three merge-fallout bugs that combined to leave the services grid empty
and most UI inert:
1. error-handler.js was bundled into onboarding.js (loaded 3rd), but
globals.js in core.js (loaded 1st) does `const errorHandler = new
ErrorHandler()` at top level. ErrorHandler was undefined when core.js
ran -> ReferenceError -> globals.js stopped, so window.APPS,
_showTotpOverlay, loadServices, etc. were never set, and init.js
blew up on every call into core's exports.
Moved error-handler.js to the start of the core.js bundle so the
class is on window before any other script touches it.
2. setup-wizard.js also declared `const errorHandler = new ErrorHandler()`
at top level. Classic scripts share the document's top-level lexical
environment, so this collided with globals.js's declaration ->
redeclaration SyntaxError in features.js. Removed setup-wizard.js's
copy; it picks up the global one.
3. tooltip-definitions.js closed its `(function(window){...})(window);`
IIFE at line ~171 ("Validation module loaded"), then the TOOLTIP_
DEFINITIONS array, getter helpers, window.TooltipDefinitions export,
and final `debug(...)` log all sat at top level — outside the IIFE,
where `debug` was no longer in scope. Removed the early close and
added one at EOF so the whole file is in one IIFE.
The service worker uses staleWhileRevalidate on /dist/*, so after a
release it would serve old bundles from cache indefinitely (cache is
only wiped when the cache *name* changes, which was hardcoded to
'dashcaddy-shell-v10'). Result: dashboard appears unchanged after a
self-update until the user manually unregisters the SW.
build.js now hashes the concatenated dist bundles and writes
`dashcaddy-shell-<10-hex-chars>` into sw.js. Any change in dist/
produces a fresh cache name; on the next page load the SW's activate
handler deletes all older caches and the new bundles are fetched.
When DashCaddy is installed without `${DASHBOARD_DIR}:/app/dashboard` bind
mounted into the container (e.g. legacy DNS2 setup where Caddy serves from
/var/www/dashcaddy-status/), the self-updater's in-container copy to
/app/dashboard was a silent no-op — leaving the dashboard stale across
self-updates, which led to CSP-hash mismatches and a broken UI.
- self-updater: new hostFrontendDir option (default `/var/www/dashcaddy-status`
on Linux, overridable via DASHCADDY_HOST_FRONTEND_DIR). When set, defer the
frontend copy to the host-side updater by passing frontendStagingDir +
frontendTargetDir in trigger.json. Now also includes `js/` in the copy list.
- dashcaddy-update.sh: read those new trigger fields and sync the dashboard
files on the host. Auto-detect fallback for older self-updaters (no fields
in trigger.json) so a single release upgrade self-heals.
- csrf-protection: skip CSRF validation on /api/system/update-notify. The
endpoint has its own X-DashCaddy-Notify-Secret auth and is only ever called
machine-to-machine; browsers never reach it. Without this, the CSRF cookie
check rejects the notify POST before the secret comparison runs.
- release.sh: the verify step piped curl into `node -p ".../dev/stdin"` which
works on Linux but blows up on Windows/git-bash. Replaced with portable
grep+sed extraction so the same script works on both publisher OSes.
- self-updater: per-instance notify secret (auto-generated), notifyAndApply()
triggers an immediate check+apply for the publishing host
- routes: POST /api/system/update-notify (X-DashCaddy-Notify-Secret gated,
added to public-routes allowlist so TOTP doesn't block machine-to-machine)
- dashcaddy-update.sh: include VERSION in backup/deploy/rollback copy lists;
belt-and-suspenders write trigger.json commit to VERSION post-deploy.
Fixes drift where /app/VERSION stayed at the old commit after self-update.
- release.sh: mirror failures are non-fatal+loud; HTTP-verify get2 after
rsync; auto-notify co-located instance via /opt/dashcaddy/updates/notify-secret
(or honour DASHCADDY_NOTIFY_TARGETS for multi-instance setups).
- Service Filter Bar: search by name, filter by status (online/offline)
- Batch Operations: multi-select containers for start/stop/restart
- Container Snapshots: create and manage Docker checkpoints
- Added filter bar and batch action bar to index.html
- Added snapshot button to Admin tools section
- New JS modules: service-filter.js, batch-operations.js, snapshot.js
- Updated build.js to include new modules in bundle
- Added window.openContainerLogsModal(containerId, containerName) function
- Service cards (grid.js) already call this when clicking the 📋 logs button
- Modal now pre-selects the correct container when called from a card
- Rebuilt dist files
- New container-logs.js module for viewing Docker container logs
- Integrated with existing API endpoints (/logs/containers, /logs/container/:id, /logs/stream/:id)
- Features:
- Select container from dropdown
- View logs with stdout/stderr color coding
- Real-time log streaming via SSE
- Search/filter within logs
- Download logs as text file
- Line count and filter indicators
- Added '📜 Container Logs' button to Tools section in index.html
- Added to features.js bundle via build.js
- Rebuilt dist files
Wrapped 22 console.log calls across 6 files with a debug() helper
that only logs when window.DASHCADDY_DEBUG is true in the browser console.
Files:
- tour-manager.js: 10 calls
- theme-adapter.js: 4 calls
- keyboard-shortcuts.js: 4 calls
- tooltip-definitions.js: 2 calls
- progress-tracker.js: 1 call
- live-events.js: 1 call
console.error and console.warn calls preserved — those indicate
real issues worth seeing in production.
Dockerfile never received DASHCADDY_COMMIT at build, so /app/VERSION held
'unknown'. _isNewer then treated same-version-different-commit as newer,
making the auto-updater rebuild the container indefinitely (each rebuild
still produced commit='unknown').
- self-updater._isNewer: normalize commits; treat unknown/null/empty as no
commit info and fall back to pure version comparison
- self-updater._autoCheckAndApply + routes/updates: refuse to apply when
local version >= remote version (belt-and-suspenders)
- update-management.js: hide '(unknown)' from version label
- Dockerfile: COPY VERSION instead of writing from build arg
- VERSION: committed placeholder ('dev'); scripts/release.sh now writes
the real short SHA into the tarball's VERSION before tar-ing, so every
published release ships with an accurate commit
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Automates what was previously a six-step manual process that, twice
in this codebase's history, has produced version skew between git and
the released tarball (v1.2.0 was published with package.json 1.2.0 in
the tarball but the bump was never committed back to gitea — making
"what code is in v1.2.0?" answerable only by extracting the tarball).
The script:
- Refuses to run with a dirty tree, off main, or already at the
target version.
- Bumps dashcaddy-api/package.json, rebuilds status/dist/, commits
+ pushes to gitea — so the released artifact and gitea HEAD are
always in lockstep.
- Clones gitea HEAD on the release host, verifies the cloned commit
matches what we just pushed (catches a stale clone or a missed
push), tars it, computes sha256, writes version.json.
- Refreshes install.sh on the release host alongside the tarball
(fresh installs use the install.sh from the latest release).
- Mirrors the release dir to the get2 backup via rsync.
- Verifies live by curling version.json and re-hashing the served
tarball.
Hosts overridable via DASHCADDY_RELEASE_HOST / DASHCADDY_MIRROR_HOST
/ DASHCADDY_GITEA_URL env vars.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cloud backups (Dropbox / WebDAV / SFTP):
- backup-manager.js: save + load handlers per provider, credential
resolution via credentialManager, destination probe.
- routes/backups.js: /credentials/{provider} (masked GET, POST, DELETE),
/test-destination, scheduling endpoints.
- status/js/backup-restore.js: destination picker, provider-specific
credential forms, test button wired to backend probe.
- npm deps already present (dropbox 10.34.0, webdav 5.7.1,
ssh2-sftp-client 11.0.0).
Resource history:
- resource-monitor.js: three-tier rollup storage — raw 10s samples
(7-day retention), hourly rollups (30-day), daily rollups
(365-day). getHistoryByRange() auto-selects the appropriate tier.
- routes/monitoring.js: /monitoring/history/:containerId now supports
startTime/endTime range mode (legacy ?hours=N still works).
- status/js/resource-monitor.js + dashboard.css: "History" tab with
range buttons (1h/24h/7d/30d/1y), SVG sparklines for
CPU / memory / network. Renderer handles raw and rolled-up shapes.
status/dist/features.js rebuilt from source via build.js.
Lifted out of wip/cloud-backups-and-history; the half-finished
app-deps feature from that branch (frontend calls /api/v1/apps/
check-dependencies but the endpoint doesn't exist) is preserved
separately on wip/app-deps for later.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- install.sh now deploys the src/ directory alongside routes/.
Without this, fresh installs of v1.3.0+ produce containers whose
Dockerfile references src/ but the directory is missing on the host
filesystem, so docker build fails with "/src: not found".
- The fallback heredoc that writes /etc/systemd/system/dashcaddy-
updater.path drops MakeDirectory=yes for the same reason it was
removed from the on-disk unit (e994ad1): systemd creates the watched
trigger.json path as an empty directory on unit start, blocking
every subsequent update with EISDIR.
Bumped to 1.3.1 so the existing v1.3.0 instance auto-updates and
picks up these and the host-script fix from 0cf6323.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>