# Evidence file for DC-PRODUCTION-GRADE-BACKLOG.md P6 section (rev14) Verbatim excerpts captured 2026-09-14 from the production tree `/opt/dashcaddy` and the live DNS2 deployment, so every live-state claim in the backlog can be checked against this file. ## E1. Self-updater contract — dashcaddy-api/src/docker/self-updater.js (production tree /opt/dashcaddy) Verbatim grep output (`grep -n "UPDATE_URL\|MIRROR_URL\|CHANNEL\|checkInterval\|INTERVAL" ...`): ``` 24: CHECK_INTERVAL: 30 * 60 * 1000, // 30 minutes 25: UPDATE_URL: process.env.DASHCADDY_UPDATE_URL || 'https://get.dashcaddy.net/release', 26: MIRROR_URL: process.env.DASHCADDY_MIRROR_URL || 'https://get2.dashcaddy.net/release', 35: CHANNEL: process.env.DASHCADDY_UPDATE_CHANNEL || 'stable', 46: checkInterval: parseInt(options.checkInterval || DEFAULTS.CHECK_INTERVAL, 10), 47: updateUrl: options.updateUrl || DEFAULTS.UPDATE_URL, 48: mirrorUrl: options.mirrorUrl || DEFAULTS.MIRROR_URL, 64: channel: options.channel || process.env.DASHCADDY_UPDATE_CHANNEL || DEFAULTS.CHANNEL, ``` Revoked kill switch, verbatim (same file, lines 531/535): ```js if (remote?.revoked === true) { ``` ```js reason: 'release revoked', ``` Proves: 30-min poll interval, update + mirror feed URLs, channel selection, and the `revoked: true` kill switch — all in `self-updater.js` of the production tree. ## E2. Shipdeck v0 DoD — shipdeck/docs/SPEC.md, verbatim ``` ## Definition of done (v0) — MET 2026-09-14 - [x] `shipdeck deploy` lands a real Go hello-world on samihost end-to-end: build → systemd active → caddy gate (tailnet 200 / public 403) → DNS on DNS2+DNS1 → HTTP health 200 → journal row (epochs 1789360016, …0128, …0610, …0672; rollback …0128→…0016 verified with content diff) - [x] `shipdeck status` all green; `shipdeck rollback` swaps + re-verifies green ``` ## E3. Version endpoint — production tree /opt/dashcaddy, verbatim grep output PUBLIC_ROUTES membership (`src/utilities/middleware.js`): ``` 488- { path: '/api/v1/monitoring/stats', exact: true, method: 'GET', monitoring: true }, 489- { path: '/api/v1/health-checks/status', exact: true, method: 'GET', monitoring: true }, 490: { path: '/api/v1/version', exact: true, method: 'GET' }, ``` Route registration at startup (`src/app.js`, verbatim `sed -n '519,523p'`): ```js appName = versionRoute.getName(); // Pre-build the version router once at startup and reuse it. const versionRouter = versionRoute.buildRouter(); apiRouter.use(versionRouter); log.info('app', `Version endpoint available at /api/v1/version (v${appVersion})`); ``` (The version HANDLER source lives in the route module wired by `versionRoute.buildRouter()` — `/api/v1/version` returns `{ name, version, node, platform, arch, uptime, instanceId }` from `package.json` read at startup, per the E5 live response below.) ## E4. Installer build targets — dashcaddy-installer/BUILD_GUIDE.md, verbatim ``` # Windows (creates portable .exe and installer) # macOS zip (from any host; electron-builder's native target) npm run build:mac # macOS real drag-install .dmg — built ON LINUX, no Mac needed # (one-time toolchain: see scripts/build-dmg-linux.sh header) # Linux (creates AppImage and .deb) npm run build:linux ``` Output artifacts (same file): portable + NSIS .exe (Windows), mac .zip + Linux-built .dmg (macOS, unsigned), AppImage + .deb (Linux). ## E5. Live deployment state on DNS2 — complete captured output (rev14), capture start 2026-09-14T11:50:10Z, all three exit codes 0 The block below is the COMPLETE, unedited terminal capture of the three commands (per-command timestamp + exit status embedded). The first command's stdout is the full version.json — no ellipses; the long changelog string is part of the real response. ``` === E5 CAPTURE 2026-09-14T11:50:10Z — per-command timestamps below === $ curl -sk --resolve get.dashcaddy.net:443:127.0.0.1 https://get.dashcaddy.net/release/version.json { "version": "1.16.0", "commit": "70e252c", "channel": "stable", "url": "https://get.dashcaddy.net/release/latest.tar.gz", "sha256": "f4dd3a6efe70a99c64b2b2093af13b79943846f83546c76fba9f99b24e489357", "publishedAt": "2026-09-13T12:45:26Z", "changelog": "DashCaddy v1.16.0 \u2014 self-updater hardening + auto-update enabled + docker disk discipline\n\nFIXES\n- self-updater: same-version releases are NEVER \"newer\" again (DC-122). Commit\n labels are opaque build stamps; only a semver bump counts. This bug made\n same-version installs with any commit-string difference report\n \"update available\" forever and would have re-applied stale tarballs in a\n loop once auto-update was enabled.\n- self-updater: _autoCheckAndApply skips re-applying an identical\n version@sha256 within a process lifetime (defense-in-depth against apply\n loops).\n- dashcaddy-update.sh: data-dir cp fallback copied the directory INTO the\n destination (nested data/data), so rollback restored nothing. Now copies\n contents (\"dir/.\") \u2014 backup AND restore paths fixed.\n- dashcaddy-update.sh: frontend is now snapshotted before deploy and restored\n on build-failure and health-check-failure rollbacks. Previously a failed\n update left the NEW frontend paired with the ROLLED BACK API.\n- start.sh: bundle sync is newer-source-only. The old unconditional copy\n reverted self-updater-deployed frontends on every container restart.\n- dashcaddy-update.sh: docker prune now runs on the success path, both\n failure paths, and after rollbacks (shared prune_docker helper).\n\nCHANGES\n- start.sh: DASHCADDY_UPDATE_ENABLED=true \u2014 auto-update is ON (Sami 2026-09-13).\n- start.sh + fallback docker run: json-file log caps (10MB x 3) so container\n logs can never grow unbounded again.\n\nTESTS\n- __tests__/self-updater-isnewer.test.js: 8 regression cases covering the\n same-version commit-mismatch bug, semver ordering, and edge inputs." }curl_exit=0 at 2026-09-14T11:50:10Z $ curl -s -m 6 http://127.0.0.1:3001/api/v1/version {"success":true,"name":"dashcaddy-api","version":"1.16.0","node":"v20.11.1","platform":"linux","arch":"x64","uptime":1768.092256197,"instanceId":null}curl_exit=0 at 2026-09-14T11:50:10Z $ docker inspect dashcaddy-api --format '{{range .Config.Env}}{{println .}}{{end}}' | grep UPDATE DASHCADDY_UPDATE_ENABLED=true grep_exit=0 at 2026-09-14T11:50:10Z ``` Proves, AS OF the capture timestamp (a dated deployment snapshot, not necessarily current state at review time): production DNS2 ran DashCaddy v1.16.0, channel `stable`, auto-update ON, feed reachable, and no `revoked` key in the live feed (kill switch not engaged). Re-run the three commands above to refresh. ## E6. Update-stamp contract — live + source capture, rev14 (a) Live stamp file — `/var/www/dashcaddy-status/update-stamp.json` (verbatim `cat`): ```json {"version":"1.16.0","at":"2026-09-13T12:47:56Z"} ``` (b) The ACTUAL writer: `/opt/dashcaddy/scripts/dashcaddy-update.sh` (host-side helper executed by the self-updater's update flow; the Node self-updater orchestrates, this script writes the stamp — see excerpt, verbatim `sed -n '714,725p'`): ```bash cp -rf "$frontend_staging_dir/assets/"* "$frontend_target_dir/assets/" 2>/dev/null || true fi # DC-122: host-side deployment stamp — start.sh treats a stamped, newer # deployment as authoritative and skips its source-bundle sync (this is # the only writer that can reach the web root with real host paths). local esc_ver esc_ver=$(json_escape "$to_version") printf '{"version":"%s","at":"%s"}\n' "$esc_ver" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ > "$frontend_target_dir/update-stamp.json" 2>/dev/null || true fi # DC-122 close: validated frontend-target branch fi ``` (version JSON-escaped, UTC timestamp, written into the validated frontend-target dir; `|| true` only guards the stamp write itself — the deployment result is recorded separately in updates/result.json.) (b2) Self-updater call-site — `/opt/dashcaddy/dashcaddy-api/src/docker/self-updater.js` lines 303-313 (verbatim `sed -n '303,313p'`), showing the orchestrator writing trigger.json and deferring stamp writing to the host-side helper: ```js await fsp.writeFile( path.join(this.config.updatesDir, 'trigger.json'), JSON.stringify(trigger, null, 2) ); // DC-122 note: the frontend deployment stamp (update-stamp.json) is // written by the HOST-side dashcaddy-update.sh after it syncs the // frontend — the container has no bind mount for the web root, so // writing the stamp here would silently target the container layer. // The host-side systemd service will handle the rest. ``` So the identity chain is: Node self-updater (orchestrator, writes trigger.json) → host-side `dashcaddy-update.sh` (executed via the systemd path unit; writes update-stamp.json into the web root). The word "writer" in (b) refers to dashcaddy-update.sh specifically. A repo-wide search was NOT run to prove it is the only writer; the excerpt above proves the intended division of labor, not uniqueness. (c) Sync-gating side: `/opt/dashcaddy/start.sh` (verbatim `sed -n '150,170p'`) — start.sh treats the stamp as the frontend authority and skips its source-bundle sync while the stamp file's MTIME beats the source tree's mtime — authority is decided by comparing file mtimes, not by parsing version values inside the files (prevents restart-reverts): ``` # Sync the freshly-built dashboard bundle into the static directory Caddy # serves — decided by VERSION METADATA, not file mtimes (mtimes are not # reliable: scp/tar/cp can preserve or shuffle them). DC-122 contract: # - The self-updater writes a STAMP (update-stamp.json) into the live web # root when it deploys a frontend; while that stamp is newer than the # source tree's VERSION file, start.sh must NOT touch the live bundle. # - Normal builds: publishing bumps the source VERSION (mtime = build time) # and clears any stale stamp, so source wins and the sync happens. echo "[start.sh] Syncing dashboard bundle into static dir (metadata-driven)..." mkdir -p /var/www/dashcaddy-status/dist if [ -d /opt/dashcaddy/status/dist ]; then NEEDS_SYNC=1 STAMP=/var/www/dashcaddy-status/update-stamp.json SRC_VERSION=/opt/dashcaddy/dashcaddy-api/VERSION if [ -f "$STAMP" ] && [ -f "$SRC_VERSION" ] && [ "$STAMP" -nt "$SRC_VERSION" ]; then # A self-updater deployment is newer than the last source build: hands off. echo "[start.sh] Deployed frontend stamp newer than source VERSION — skipping sync to preserve deployed frontend." NEEDS_SYNC=0 fi if [ "$NEEDS_SYNC" = "1" ]; then ``` (d) Rollback interplay: `dashcaddy-update.sh` restore_frontend() REMOVES the stamp on rollback (verbatim, lines 356-358) so start.sh's source sync resumes authority — the restored frontend is not a self-updater deployment: ```bash # Rollback removes the deployment stamp: the restored frontend is NOT a # self-updater deployment, so start.sh's source sync must resume authority. rm -f "${target:?}/update-stamp.json" ``` Proves the update-stamp-on-frontend-deploy behavior cited by DC-112 end to end: writer identity, stamp content (version + UTC timestamp), sync gating, and rollback semantics.