fix(updates): route frontend deploy through host-side updater
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.
This commit is contained in:
@@ -87,12 +87,18 @@ main() {
|
|||||||
|
|
||||||
# Parse trigger.json (uses python3 which is available on all supported distros)
|
# Parse trigger.json (uses python3 which is available on all supported distros)
|
||||||
local action version from_version staging_dir api_source_dir commit
|
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'])")
|
action=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}'))['action'])")
|
||||||
version=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}'))['version'])")
|
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'])")
|
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'])")
|
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'])")
|
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 '')")
|
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 "=== ${action^^}: v${from_version} -> v${version} ==="
|
||||||
log "Staging: ${staging_dir}"
|
log "Staging: ${staging_dir}"
|
||||||
@@ -147,6 +153,39 @@ main() {
|
|||||||
echo "$commit" > "$api_source_dir/VERSION"
|
echo "$commit" > "$api_source_dir/VERSION"
|
||||||
fi
|
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
|
# 5. Rebuild container
|
||||||
log "Rebuilding container..."
|
log "Rebuilding container..."
|
||||||
cd "$api_source_dir"
|
cd "$api_source_dir"
|
||||||
|
|||||||
@@ -51,6 +51,14 @@ class SelfUpdater extends EventEmitter {
|
|||||||
hostUpdatesDir: options.hostUpdatesDir || (platformPaths.isWindows ? options.updatesDir || DEFAULTS.UPDATES_DIR : '/opt/dashcaddy/updates'),
|
hostUpdatesDir: options.hostUpdatesDir || (platformPaths.isWindows ? options.updatesDir || DEFAULTS.UPDATES_DIR : '/opt/dashcaddy/updates'),
|
||||||
apiSourceDir: options.apiSourceDir || DEFAULTS.API_SOURCE_DIR,
|
apiSourceDir: options.apiSourceDir || DEFAULTS.API_SOURCE_DIR,
|
||||||
frontendDir: options.frontendDir || DEFAULTS.FRONTEND_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),
|
maxBackups: parseInt(options.maxBackups || DEFAULTS.MAX_BACKUPS, 10),
|
||||||
channel: options.channel || process.env.DASHCADDY_UPDATE_CHANNEL || DEFAULTS.CHANNEL,
|
channel: options.channel || process.env.DASHCADDY_UPDATE_CHANNEL || DEFAULTS.CHANNEL,
|
||||||
instanceIdFile: options.instanceIdFile || process.env.DASHCADDY_INSTANCE_ID_FILE || DEFAULTS.INSTANCE_ID_FILE,
|
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._cleanDir(stagingDir);
|
||||||
await this._extractTarball(tarballPath, 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');
|
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, [
|
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 });
|
this.emit('update-progress', { step: 'frontend-updated', version: remoteInfo.version });
|
||||||
}
|
}
|
||||||
@@ -265,6 +282,8 @@ class SelfUpdater extends EventEmitter {
|
|||||||
fromVersion: local.version,
|
fromVersion: local.version,
|
||||||
stagingDir: hostApiSrc,
|
stagingDir: hostApiSrc,
|
||||||
apiSourceDir: this.config.apiSourceDir,
|
apiSourceDir: this.config.apiSourceDir,
|
||||||
|
frontendStagingDir: hostFrontendStagingPath,
|
||||||
|
frontendTargetDir: this.config.hostFrontendDir || null,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
channel: this.config.channel,
|
channel: this.config.channel,
|
||||||
instanceId: this.instanceId,
|
instanceId: this.instanceId,
|
||||||
|
|||||||
Reference in New Issue
Block a user