From 56f1a001f21c695cf0505ba9b6fd3a0febcf27ac Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Mon, 20 Jul 2026 02:07:25 -0700 Subject: [PATCH] start.sh auto-sync dashboard bundle into /var/www/dashcaddy-status/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After every rebuild the freshly-baked dashboard bundle lives in /opt/dashcaddy/status/dist/ + sw.js. DNS2 also serves files from /var/www/dashcaddy-status/dist/ + sw.js (the original Windows-installer mirror path). Without an explicit copy step between build and start.sh, the served bundle stays on whatever hash was there before, while the API responds with new code. That mismatch is what shows up in the dashboard as "version unavailable" + "no data" widgets — saw it in the deploy that followed DC-046/047 (fixed by a manual cp this time, never again). Sync block runs before docker run: cp /opt/dashcaddy/status/dist/*.js /var/www/dashcaddy-status/dist/ cp /opt/dashcaddy/status/sw.js /var/www/dashcaddy-status/ cp /opt/dashcaddy/status/index.html /var/www/dashcaddy-status/ All guarded so set -e doesn't kill the container start on a single per-file failure (e.g. read-only mount, missing dir). Missing source dir is a WARN + no-op rather than a fatal — fresh installs without status/dist/ don't get a stale-bundle problem, just a log line. Test: scripts/test-start-sh-sync.sh — 7 assertions across 4 cases (fresh-copy, missing-source, idempotent-re-sync, set-e-survives-permission- denied). All pass. --- scripts/test-start-sh-sync.sh | 151 ++++++++++++++++++++++++++++++++++ start.sh | 20 +++++ 2 files changed, 171 insertions(+) create mode 100755 scripts/test-start-sh-sync.sh diff --git a/scripts/test-start-sh-sync.sh b/scripts/test-start-sh-sync.sh new file mode 100755 index 0000000..f8d6eb3 --- /dev/null +++ b/scripts/test-start-sh-sync.sh @@ -0,0 +1,151 @@ +#!/bin/bash +# Regression test: start.sh dashboard bundle sync step. +# +# Validates the auto-sync that runs before `docker run` in start.sh: +# 1. Bundles are coppied from /opt/dashcaddy/status/dist → /var/www/dashcaddy-status/dist +# 2. sw.js is copied to /var/www/dashcaddy-status/sw.js +# 3. index.html is copied to /var/www/dashcaddy-status/index.html +# 4. Missing source dir is handled gracefully (WARN, no exit) +# 5. Sync is idempotent — re-running doesn't clobber or duplicate + +set -u +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FAILURES=0 + +pass() { echo " ✓ $1"; } +fail() { echo " ✗ $1"; FAILURES=$((FAILURES + 1)); } + +# -------------------------------------------------------------------------- +# Extract just the sync block from start.sh. The first call to `docker run` +# marks the end of the sync section. +# -------------------------------------------------------------------------- +extract_sync_block() { + awk ' + /^# Sync the freshly-built dashboard bundle/ { capture=1 } + /^docker run -d --restart unless-stopped/ { capture=0 } + capture { print } + ' "${SCRIPT_DIR}/../start.sh" +} + +fresh_workdir() { + local d; d="$(mktemp -d /tmp/dashcaddy-sync-test.XXXXXX)" + mkdir -p "$d/src/dist" + mkdir -p "$d/dst/dist" + echo "$d" +} + +cleanup() { rm -rf "$1"; } + +run_sync() { + local workdir="$1" + # Use absolute replacement paths that don't collide with sed prefixes. + # Inject a unique sentinel into start.sh paths first, then replace that + # sentinel with the test-specific paths. + local src_sentinel="__SYNC_TEST_SRC__" + local dst_sentinel="__SYNC_TEST_DST__" + local SRC_DIST_DIR="${workdir}/src/dist" + local DST_STATUS_DIR="${workdir}/dst" + + extract_sync_block | sed \ + -e "s|/opt/dashcaddy/status/dist|${src_sentinel}|g" \ + -e "s|/var/www/dashcaddy-status|${dst_sentinel}|g" \ + -e "s|${src_sentinel}|${SRC_DIST_DIR}|g" \ + -e "s|${dst_sentinel}|${DST_STATUS_DIR}|g" \ + > /tmp/_sync_block.sh + # shellcheck disable=SC1091 + source /tmp/_sync_block.sh +} + +# Test 1: Bundles + sw.js + index.html all get synced from a real source tree +echo "Test 1: fresh source → files synced to status dir" +WORKDIR="$(fresh_workdir)" +SRC="${WORKDIR}/src/dist" +echo 'console.log("core");' > "${SRC}/core.js" +echo 'console.log("features");' > "${SRC}/features.js" +echo 'self.addEventListener("fetch", ...)' > "${SRC}/sw.js" +echo 'hi' > "${SRC}/index.html" + +run_sync "${WORKDIR}" + +if [ -f "${WORKDIR}/dst/dist/core.js" ]; then + pass "core.js copied to status dir" +else + fail "core.js NOT copied" +fi +if [ -f "${WORKDIR}/dst/dist/features.js" ]; then + pass "features.js copied" +else + fail "features.js NOT copied" +fi +if [ -f "${WORKDIR}/dst/sw.js" ]; then + pass "sw.js copied (NOT under dist/)" +else + fail "sw.js NOT copied" +fi +if [ -f "${WORKDIR}/dst/index.html" ]; then + pass "index.html copied" +else + fail "index.html NOT copied" +fi +cleanup "${WORKDIR}" + +# Test 2: Missing source dir → WARN, no exit, no crash, no destinations created +echo +echo "Test 2: source dir missing → WARN, no exit" +WORKDIR="$(fresh_workdir)" +# SRC intentionally NOT created +run_sync "${WORKDIR}" +# If we reach here without an exit code propagating, we passed +if [ -z "$(ls -A "${WORKDIR}/dst/dist" 2>/dev/null)" ]; then + pass "no files copied when source missing" +else + fail "files copied when source was missing — should be no-op" +fi +cleanup "${WORKDIR}" + +# Test 3: Re-running syncs (idempotent — files overwritten with same content) +echo +echo "Test 3: idempotency (re-sync overwrites with same content)" +WORKDIR="$(fresh_workdir)" +SRC="${WORKDIR}/src/dist" +echo 'core-v1' > "${SRC}/core.js" +run_sync "${WORKDIR}" +echo 'core-v2' > "${SRC}/core.js" # mutate source +run_sync "${WORKDIR}" +if [ "$(cat "${WORKDIR}/dst/dist/core.js")" = "core-v2" ]; then + pass "second sync picked up the latest source" +else + fail "second sync did not update" +fi +cleanup "${WORKDIR}" + +# Test 4: set -e propagation — a single file-level failure doesn't take down the script +echo +echo "Test 4: set -e does not kill container start when sync hits a permission error" +WORKDIR="$(fresh_workdir)" +SRC="${WORKDIR}/src/dist" +echo 'core' > "${SRC}/core.js" +DST="${WORKDIR}/dst/dist" +chmod 555 "${DST}" # write-protected dest → cp -f would fail +# shellcheck disable=SC1091 +set -e +run_sync "${WORKDIR}" +EXIT=$? +set +e +chmod 755 "${DST}" 2>/dev/null +cleanup "${WORKDIR}" +if [ "$EXIT" -eq 0 ]; then + pass "sync step survives a permission-denied dest" +else + fail "set -e propagated a sync failure (exit ${EXIT}); would prevent container from starting" +fi + +rm -f /tmp/_sync_block.sh + +echo +if [ "$FAILURES" -eq 0 ]; then + echo "All sync regression tests passed." + exit 0 +fi +echo "${FAILURES} test(s) failed." +exit 1 diff --git a/start.sh b/start.sh index 0870b24..4d8484a 100755 --- a/start.sh +++ b/start.sh @@ -115,6 +115,26 @@ fi # that the local tailscaled handles; we never need to mutate state # from inside the container. echo "[start.sh] Creating container with full config..." + +# Sync the freshly-built dashboard bundle into the static directory Caddy +# serves. The Docker image bakes dist/ from the source tree at build time, +# but DNS2 also serves /var/www/dashcaddy-status/dist/ (the original +# Windows installer mirror path). If we don't sync after every build, the +# served bundle keeps the OLD hash while the API responds with new code, +# which shows up in the dashboard as "version unavailable" + "no data" +# widgets because the new API surface doesn't match the old widget code. +# This step is idempotent and ~50ms — always safe to run. +echo "[start.sh] Syncing dashboard bundle into static dir..." +mkdir -p /var/www/dashcaddy-status/dist +if [ -d /opt/dashcaddy/status/dist ]; then + cp /opt/dashcaddy/status/dist/*.js /var/www/dashcaddy-status/dist/ 2>/dev/null || true + cp /opt/dashcaddy/status/sw.js /var/www/dashcaddy-status/ 2>/dev/null || true + cp /opt/dashcaddy/status/index.html /var/www/dashcaddy-status/ 2>/dev/null || true + echo "[start.sh] Bundle synced ($(ls /opt/dashcaddy/status/dist/*.js 2>/dev/null | wc -l) bundle files + sw.js + index.html)." +else + echo "[start.sh] WARN: /opt/dashcaddy/status/dist missing — skipping sync (frontend will be stale)." +fi + docker run -d --restart unless-stopped --name ${CONTAINER_NAME} \ --add-host=get.dashcaddy.net:194.233.88.206 \ --add-host=get2.dashcaddy.net:194.233.88.206 \