diff --git a/dashcaddy-api/scripts/dashcaddy-update.sh b/dashcaddy-api/scripts/dashcaddy-update.sh index 048e919..4413434 100644 --- a/dashcaddy-api/scripts/dashcaddy-update.sh +++ b/dashcaddy-api/scripts/dashcaddy-update.sh @@ -87,12 +87,18 @@ main() { # Parse trigger.json (uses python3 which is available on all supported distros) local action version from_version staging_dir api_source_dir commit + local frontend_staging_dir frontend_target_dir action=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}'))['action'])") version=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}'))['version'])") from_version=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}'))['fromVersion'])") staging_dir=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}'))['stagingDir'])") api_source_dir=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}'))['apiSourceDir'])") commit=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}')).get('commit') or '')") + # Frontend paths — optional (older self-updaters don't write these). When + # present, this script also syncs the dashboard files (Caddy serves them + # directly from the host; the container path /app/dashboard isn't mounted). + frontend_staging_dir=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}')).get('frontendStagingDir') or '')") + frontend_target_dir=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}')).get('frontendTargetDir') or '')") log "=== ${action^^}: v${from_version} -> v${version} ===" log "Staging: ${staging_dir}" @@ -147,6 +153,39 @@ main() { echo "$commit" > "$api_source_dir/VERSION" fi + # 4b. Sync frontend. Caddy serves the dashboard directly from the host + # filesystem; the container-side copy in older self-updater.js builds wrote + # to /app/dashboard which isn't always mounted, so the real sync happens + # here. Trigger fields take precedence; if absent (older self-updater), + # fall back to: staging dir's sibling status/ + first existing known target. + if [[ -z "$frontend_staging_dir" ]]; then + parent_staging=$(dirname "$staging_dir") + [[ -d "$parent_staging/status" ]] && frontend_staging_dir="$parent_staging/status" + fi + if [[ -z "$frontend_target_dir" ]]; then + for candidate in /var/www/dashcaddy-status /etc/dashcaddy/sites/status; do + [[ -d "$candidate" ]] && frontend_target_dir="$candidate" && break + done + fi + if [[ -n "$frontend_staging_dir" && -n "$frontend_target_dir" && -d "$frontend_staging_dir" ]]; then + log "Syncing frontend: $frontend_staging_dir -> $frontend_target_dir" + mkdir -p "$frontend_target_dir" + [[ -f "$frontend_staging_dir/index.html" ]] && cp -f "$frontend_staging_dir/index.html" "$frontend_target_dir/index.html" + [[ -f "$frontend_staging_dir/sw.js" ]] && cp -f "$frontend_staging_dir/sw.js" "$frontend_target_dir/sw.js" + for sub in dist css vendor js; do + if [[ -d "$frontend_staging_dir/$sub" ]]; then + mkdir -p "$frontend_target_dir/$sub" + cp -rf "$frontend_staging_dir/$sub"/* "$frontend_target_dir/$sub/" 2>/dev/null || true + fi + done + # assets/ is mounted into the container; usually already in sync via bind + # mount, but if a release ships new assets we want them on disk too. + if [[ -d "$frontend_staging_dir/assets" ]]; then + mkdir -p "$frontend_target_dir/assets" + cp -rf "$frontend_staging_dir/assets"/* "$frontend_target_dir/assets/" 2>/dev/null || true + fi + fi + # 5. Rebuild container log "Rebuilding container..." cd "$api_source_dir" diff --git a/dashcaddy-api/self-updater.js b/dashcaddy-api/self-updater.js index 8b7251a..2873e38 100644 --- a/dashcaddy-api/self-updater.js +++ b/dashcaddy-api/self-updater.js @@ -51,6 +51,14 @@ class SelfUpdater extends EventEmitter { hostUpdatesDir: options.hostUpdatesDir || (platformPaths.isWindows ? options.updatesDir || DEFAULTS.UPDATES_DIR : '/opt/dashcaddy/updates'), apiSourceDir: options.apiSourceDir || DEFAULTS.API_SOURCE_DIR, frontendDir: options.frontendDir || DEFAULTS.FRONTEND_DIR, + // hostFrontendDir is the path on the HOST where Caddy serves the dashboard + // from. The in-container `frontendDir` is often a path that isn't mounted + // (e.g. /app/dashboard with no bind mount), so writing there is silently + // useless. When this is set, we pass it to the host-side updater script + // and skip the in-container copy entirely. + hostFrontendDir: options.hostFrontendDir + || process.env.DASHCADDY_HOST_FRONTEND_DIR + || (platformPaths.isWindows ? null : '/var/www/dashcaddy-status'), maxBackups: parseInt(options.maxBackups || DEFAULTS.MAX_BACKUPS, 10), channel: options.channel || process.env.DASHCADDY_UPDATE_CHANNEL || DEFAULTS.CHANNEL, instanceIdFile: options.instanceIdFile || process.env.DASHCADDY_INSTANCE_ID_FILE || DEFAULTS.INSTANCE_ID_FILE, @@ -241,11 +249,20 @@ class SelfUpdater extends EventEmitter { await this._cleanDir(stagingDir); await this._extractTarball(tarballPath, stagingDir); - // 4. Apply frontend files directly (zero-downtime) + // 4. Locate frontend source. The actual deploy is done either here (if no + // hostFrontendDir is configured, e.g. Windows or unusual setups) or by + // the host-side updater script via trigger.json (preferred path on Linux, + // where Caddy serves from /var/www/... outside the container's filesystem). const frontendSrc = this._findDir(stagingDir, 'status'); - if (frontendSrc) { + let hostFrontendStagingPath = null; + if (frontendSrc && this.config.hostFrontendDir) { + // Defer the copy to the host-side script. Just compute the host path + // for staging so it can find the files. + hostFrontendStagingPath = frontendSrc.replace(this.config.updatesDir, this.config.hostUpdatesDir); + } else if (frontendSrc) { + // No host path configured — copy in-container (legacy / Windows path). await this._copyDir(frontendSrc, this.config.frontendDir, [ - 'dist', 'css', 'assets', 'vendor', 'index.html', 'sw.js' + 'dist', 'css', 'assets', 'vendor', 'js', 'index.html', 'sw.js' ]); this.emit('update-progress', { step: 'frontend-updated', version: remoteInfo.version }); } @@ -265,6 +282,8 @@ class SelfUpdater extends EventEmitter { fromVersion: local.version, stagingDir: hostApiSrc, apiSourceDir: this.config.apiSourceDir, + frontendStagingDir: hostFrontendStagingPath, + frontendTargetDir: this.config.hostFrontendDir || null, timestamp: new Date().toISOString(), channel: this.config.channel, instanceId: this.instanceId,