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>
Two bugs in the host-side updater script:
1. The Dockerfile (since f5fe32b) does \`COPY src/ ./src/\`, but the
host script never copies src/ from staging into the api source
directory. Result: every update fails with
"failed to compute cache key: ... '/src': not found".
2. \`cp -rf staging/routes api_source/routes/\` does NOT replace the
destination directory — it copies the source dir INTO the
destination, producing api_source/routes/routes/. Means new route
files end up nested one level deep and never get loaded by
server.js, so updates silently regress route handlers even when
the build succeeds.
Switch to "rm -rf dest && cp -rf src dest" semantics for both routes
and src, in all four touch points (deploy + 3 rollback paths).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`MakeDirectory=yes` on a `PathChanged=` directive whose target is a
file (not a directory) causes systemd to create the watched path as
an empty directory on unit start. The container's self-updater then
crashes with EISDIR every time it tries to writeFile() the trigger,
and the host script never runs.
The parent `/opt/dashcaddy/updates/` is already created by the
installer/Docker volume, so the flag is redundant and only here as
a footgun. Drop it.
Reproducer: enable the unit on a fresh system, watch
`/opt/dashcaddy/updates/trigger.json` get materialized as a directory
within milliseconds of `systemctl start dashcaddy-updater.path`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.2.0 was published as a tarball but its package.json bump was never
committed back to git. This release picks up where that gap left off
and includes two fixes that v1.2.0 (commit a216dd8) was missing:
- 6abba43: clear ALL pending self-update history entries (not just
the first), so stuck installs unwind cleanly.
- 0460129: allow apiSourceDir to be overridden via the
DASHCADDY_API_SOURCE_DIR env var, so installs that don't follow
the default /etc/dashcaddy/sites/dashcaddy-api/ layout (e.g. older
deployments under /opt/dashcaddy/) can point the auto-updater at
the right path without patching the constructor.
Without these, instances on the older /opt/dashcaddy/ layout get
stuck in a 30-min retry loop where every update attempt fails with
'cp: cannot create directory /etc/dashcaddy/sites/dashcaddy-api/'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the env-var pattern f5fe32b introduced for channel and
instanceIdFile. Lets installs that don't follow the default
/etc/dashcaddy/sites/dashcaddy-api/ layout (e.g. older deployments
under /opt/dashcaddy/) point the auto-updater at the right path
without having to patch the constructor call site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
checkPostUpdateResult() used history.find() which only ever updated a
single pending entry. When multiple update attempts stacked up, the
extra pending entries stayed stuck in 'pending' forever even though
the actual update completed. Switch to filter() + loop to clear all
matching entries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Container exec/shell via WebSocket + xterm.js (subtle >_ button on cards)
- Live dashboard updates via SSE (resource alerts, health changes, update notices)
- Docker Compose import with YAML parsing, preview, and dependency-ordered deploy
- Volume & network management modal with disk usage overview
- CPU/memory resource limits on deploy and live update
- Email SMTP notifications (nodemailer) alongside Discord/Telegram/ntfy
- Scheduled auto-update scheduler with maintenance windows (daily/weekly/monthly)
New deps: ws, js-yaml, nodemailer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Phase 2.1 refactor wrapped success() responses as { success, data: {...} }
but the frontend expects flat responses like { success, license: {...} }.
This caused license to show FREE TIER and broke other API consumers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These endpoints must be accessible without TOTP auth for the dashboard
to load site config (TLD, DNS servers, custom logo) and service status
(bulk probe results). Without them, the dashboard shows all services
as OFF and loses custom branding after any session expiry.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
100 requests/15min was far too low for a dashboard with auto-refresh
polling every 10-30 seconds, causing 429s on TOTP config, site config,
license status and other endpoints after ~3 minutes of normal use.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
License status, services list, config, and license feature checks
were being rate-limited (429) after ~14 minutes of dashboard polling,
causing the license to show FREE TIER and services to fail loading.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove redundant ctx shim that conflicted with function parameter
- Use destructured notification/safeErrorMessage directly
- Add pylon, customLogoDark, customLogoLight to known config keys
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The lightweight probe endpoint used by the dashboard for live status
checks had no Pylon integration. When DNS2 (Singapore) tried to probe
home network services directly, all probes timed out with 502. Now
falls back to the configured Pylon relay before the domain fallback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The modular refactor changed function signatures to destructured deps but
left internal ctx.* references intact, causing "ctx is not defined" errors
on /api/config, /api/logo, and many other endpoints. Also implements
loadTotpConfig and saveTotpConfig which were left as stubs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add missing fetchT dep to session-handlers.js (fixes ctx is not
defined error that broke jellyfin/emby/plex/syncthing SSO)
- Replace all ctx.fetchT calls with direct fetchT usage
- Remove server-old.js (69K monolith backup) from tracking
- Remove AI-generated doc artifacts from repo root
- Update .gitignore to prevent re-adding these files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve conflict in server.js by accepting the remote's modular
refactor (1960 lines → 230 lines). Local Phase 1/2 changes
(logger swap, unused import) are superseded by the new structure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Accent was #0e0e00 (same as --fg), making buttons and interactive
elements invisible. Changed to #7a4a00/#5c3800 dark amber.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- serviceUrl() now checks service.url before falling back to buildServiceUrl(id)
- Service update no longer overwrites ID with the new subdomain
- Accept "localhost" as valid IP in service update validation
- Find services by ID or URL match when updating
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The dashboard uses /api/v1/services/status (not /api/health/services)
for live status cards. This endpoint was missing pylon relay fallback,
so services unreachable from the Docker container showed as OFF even
when the pylon was running. Also adds Windows VBS startup wrapper for
pylon persistence across reboots.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DashCaddy Pylon is a lightweight probe agent that runs on remote
networks to relay health checks for services the main DashCaddy
instance can't reach directly (e.g., .sami domains, LAN IPs).
- Standalone zero-dependency Node.js script (pylon/dashcaddy-pylon.js)
- Optional API key auth, HEAD→GET fallback, batch probe support
- Health routes now try direct check first, fall back to pylon relay
- New endpoints: /health/probe (act as pylon), /health/pylon (status)
- Config: add "pylon": { "url": "...", "key": "..." } to config.json
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the in-container self-updater downloads a new version, it writes
trigger.json. The new systemd path unit watches for this file and runs
dashcaddy-update.sh, which backs up current API files, copies the new
ones, rebuilds the container, verifies health, and writes result.json.
Automatic rollback on build or health check failure.
Also fixes undefined `isWindows` variable in self-updater.js and adds
DASHCADDY_HOST_UPDATES_DIR env var to the installer's docker-compose
template for correct container-to-host path translation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove eager key generation from crypto-utils module load (was baking
keys into Docker images that conflicted with mounted production keys)
- Add license backup to config.json (survives credential store failures)
- LicenseManager.load() falls back to config.json backup if credential
store decryption fails (e.g. after container rebuild with new key)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix service edit double-write bug (was creating duplicate entries)
- Add editable display name field to service edit modal
- Backend update endpoint now accepts name, logo, and recalculates url
- Fix CSRF token regeneration breaking all POST requests (nonce was
being regenerated on every request, invalidating cached tokens)
- CSRF nonce now persists across requests, rotated only on TOTP login
- Frontend secureFetch auto-retries on CSRF failure with fresh token
- Restore lifetime license activation on DNS2
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server export now includes encryption key, themes, and all config files.
Client export bundles all DashCaddy localStorage keys (19 named + dynamic
widget keys) as browserState. Restore handles both server and browser
state in one operation. Legacy v1.0 import format still supported.
Removed redundant Export/Import toolbar buttons — Backup modal is now
the single entry point.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DNS server IDs (dns1, dns2, dns3) were hardcoded throughout the frontend
and backend. Now config.json's dnsServers object is the single source of
truth — adding or removing a DNS server in config automatically updates
the dashboard cards, credential modal, health checks, and probes.
- credentials.js: rebuild modal sections dynamically from SITE.dnsServers
- globals.js: add getPrimaryDnsId() helper for primary DNS lookups
- service-create.js, service-infrastructure.js: use dynamic DNS ID
- startup-validator.js: dynamic topCardServices from config
- middleware.js: add license endpoints to public routes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add url-resolver.js with single resolveServiceUrl() used by all 5 consumers
(probes, health routes, health checker auto-config)
- Health checker now does full sync (add/update/remove) instead of add-only,
and re-syncs automatically after every services.json mutation
- docker-maintenance and log-digest are now optional imports with try/catch,
preventing container crashes when these files are absent
- Add null guards in routes/logs.js for graceful 503 responses
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mount Tailscale router at /tailscale prefix so all 10 routes resolve
to /api/tailscale/* as expected by middleware, audit logger, and
frontend. Previously 5 routes (status, config, check-connection,
devices, protect-service) resolved to /api/* instead, with config
colliding with the settings route. Strip redundant /tailscale/ prefix
from OAuth routes that were compensating for the missing mount prefix.
Increase default health check timeout from 10s to 20s to reduce false
positives on slower services.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents Docker disk bloat by adding log rotation (10MB max, 3 files)
to all container creation and update paths, auto-pruning dangling
images after deploy/remove/update, and a daily maintenance module
that cleans build cache and warns on disk thresholds.
Saves a deployment manifest in services.json at deploy time so users
can restore all their apps after a Docker purge. Adds restore-all
and restore-single endpoints that recreate containers, Caddy config,
and DNS records from the saved manifests.
Adds an hourly log collector and daily digest generator that
summarizes errors, warnings, and events across all services into
a single human-readable report with guidance on where to investigate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server-side batched /api/v1/services/status endpoint replaces N
individual browser probes with a single API call (HEAD-first with
GET fallback, concurrency-limited, CA-aware HTTPS agent).
Frontend: clock reuses DOM instead of rebuilding innerHTML every
second with drift-correcting timer that pauses on hidden tabs.
Card animations use CSS transitionDelay + requestAnimationFrame.
Internet dot blink moved from JS intervals to CSS keyframes with
prefers-reduced-motion support. Service worker rewritten with
network-first navigation, stale-while-revalidate assets, and
navigation preload. Font faces drop TTF fallbacks, use font-display
swap.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Persist onboardingCompleted flag server-side via /api/v1/config so the
tour only auto-starts once per DashCaddy installation, not on every
new browser that connects.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix container-to-host path mapping in trigger.json (stagingDir
was using container path /app/updates/ instead of host path
/opt/dashcaddy/updates/)
- Fix download race condition: primary download's async unlink
could delete mirror download's file — use unlinkSync before retry
- Fix DASHCADDY_COMMIT build arg not passed to docker compose build
(was set as env var, now uses --build-arg)
- Remove MakeDirectory=yes from systemd path unit (was creating
trigger.json as directory instead of file)
- Remove unused 'tar' npm module import
- Add mirror fallback for tarball downloads (not just version checks)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- self-updater.js: polls for new versions, downloads/verifies tarballs,
triggers host-side rebuild via systemd path unit
- dashcaddy-update.sh + systemd units: host-side container rebuild with
automatic rollback on health check failure
- 7 new /api/v1/system/* endpoints for version info, update check/apply,
rollback, and update history
- Frontend: DashCaddy tab in Updates modal with version display,
changelog, update button, rollback, and notification dot
- install.sh: updater service installation, volume mounts, env vars
- build-release.sh + webhook-handler.js: release server pipeline
(Gitea webhook → build tarball → deploy to get.dashcaddy.net)
- Dockerfile: DASHCADDY_COMMIT build arg → VERSION file
- Version bump to 1.1.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Escape user-input port number in app-selector innerHTML
- Replace inline onclick with addEventListener in backup history (HTML entity decode bypass)
- Add Content-Security-Policy meta tag with script hash
- Replace document.write with textContent for footer year
- Filter __proto__/constructor/prototype in Object.assign calls
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Escape all innerHTML assignments with user/external data across 12 JS files
- Upgrade credential encryption: per-value IV, key moved to sessionStorage
- Fix open redirect in TOTP auth via proper URL hostname validation
- Remove sensitive DNS topology data from localStorage cache
- Add security regression test suite (51 tests)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CSRF: HMAC-signed double-submit cookie (server-bound, not raw compare)
- Keychain: execFileSync with arg arrays to prevent command injection
- Caddy config: always use structured generation, never accept raw config
- Templates: replace {{GENERATED_SECRET}} with crypto.randomBytes
- Caddyfile removal: move regex inside ctx.caddy.modify() to fix TOCTOU race
- Credentials: proper-lockfile for all file operations, fix key rotation
to decrypt with old key before generating new key
- Service removal: filter by ID only, not AND with appTemplate
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Deploy endpoint: validate appId, config, and subdomain before use (prevents 500 crash on empty body)
- Container ops: return 404 instead of 500 for non-existent containers
- Update-subdomain: require oldSubdomain/newSubdomain fields (prevents false 200 with undefined values)
- Global error handler: catch-all that never leaks stack traces or internal paths
- API 404 catch-all: return JSON instead of HTML for unmatched /api/* routes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Apps can now be served at domain.com/appname/ instead of requiring
subdomain DNS records (appname.domain.com). Supports three subpath
modes per template: native (URL base env var), strip (handle_path),
and none (incompatible warning). Tested on Linux with deploy/removal
lifecycle verified.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full codebase including API server (32 modules + routes), dashboard frontend,
DashCA certificate distribution, installer script, and deployment skills.