diff --git a/containers/libreportal/frontend/js/components/backup/backup-page.js b/containers/libreportal/frontend/js/components/backup/backup-page.js index a901950..05997a1 100644 --- a/containers/libreportal/frontend/js/components/backup/backup-page.js +++ b/containers/libreportal/frontend/js/components/backup/backup-page.js @@ -331,17 +331,26 @@ class BackupPage { async refreshAll() { const ts = Date.now(); - const [dashboard, locations, , schema, migrate] = await Promise.all([ + const [dashboard, locations, , schema, migrate, peersData] = await Promise.all([ this.fetchJson(`/data/backup/generated/dashboard.json?t=${ts}`), this.fetchJson(`/data/backup/generated/locations.json?t=${ts}`), this.loadSystemConfigs(), this.fetchJson(`/data/backup/generated/schema.json?t=${ts}`), - this.fetchJson(`/data/backup/generated/migrate.json?t=${ts}`) + this.fetchJson(`/data/backup/generated/migrate.json?t=${ts}`), + this.fetchJson(`/data/peers/generated/peers.json?t=${ts}`) ]); this.dashboard = dashboard; this.locations = locations; this.locSchema = schema; this.migrate = migrate; + // Build hostname → friendly-name lookup once so renderMigrate can show + // "homelab (host: homelab.lan)" instead of bare hostnames. + this.hostnameToPeerName = {}; + for (const p of (peersData?.peers || [])) { + if (p.kind === 'backup-channel' && p.config?.hostname) { + this.hostnameToPeerName[p.config.hostname] = p.name; + } + } this.snapshotsByLoc = {}; if (!this.engines.length) await this.loadEngines(); @@ -1811,11 +1820,16 @@ class BackupPage {

${this.escape(loc.name || 'Location')}

${(loc.hosts || []).length} other host${loc.hosts.length === 1 ? '' : 's'} backing up here - ${loc.hosts.map(host => ` + ${loc.hosts.map(host => { + const peerName = (this.hostnameToPeerName || {})[host.hostname]; + const headerLabel = peerName + ? `${this.escape(peerName)}host: ${this.escape(host.hostname)}` + : `${this.escape(host.hostname)}`; + return `
- ${this.escape(host.hostname)} + ${headerLabel} ${(host.apps || []).length} app${host.apps.length === 1 ? '' : 's'} available
- `).join('')} + `; + }).join('')}
`).join(''); diff --git a/scripts/migrate/migrate_apply.sh b/scripts/migrate/migrate_apply.sh index 0064955..f9c30c4 100644 --- a/scripts/migrate/migrate_apply.sh +++ b/scripts/migrate/migrate_apply.sh @@ -81,6 +81,9 @@ migrateApplyApp() migrateEmit phase=pre-backup status=skipped reason=user-opt-out app="$app" fi + # Per-app pre-migrate hook (optional) — declared in the app's tools.sh. + migrateRunHook "$app" pre "$source_host" "restic" + # ---- 3. The actual restore (reuses existing restoreAppStart) -------------- # restoreAppStart already wipes the app folder, restores the snapshot, # re-runs the install-time tag pipeline, and starts the container. The @@ -109,6 +112,9 @@ migrateApplyApp() fi fi + # Per-app post-migrate hook (optional) — last thing before we declare done. + migrateRunHook "$app" post "$source_host" "restic" + local finished_at finished_at=$(date +%s) local duration=$((finished_at - started_at)) diff --git a/scripts/migrate/migrate_hooks.sh b/scripts/migrate/migrate_hooks.sh new file mode 100644 index 0000000..e338ea5 --- /dev/null +++ b/scripts/migrate/migrate_hooks.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Per-app migrate hooks. After a migrate (restic-mediated apply OR direct-SSH +# pull) places the source's data on this host, some apps need app-specific +# fix-ups beyond the standard URL rewrite — rotating a federation key, +# regenerating an OIDC client secret, dropping a SaaS lock, etc. +# +# Convention: an app's tools.sh (auto-sourced by the modular per-app tools +# loader — see [[libreportal-modular-app-tools]]) may declare: +# +# _migrate_pre() # called before stop+wipe +# _migrate_post() # called after restart, before user sees it +# +# Both receive: $1 = source_hostname (peer hostname or backup tag), +# $2 = transport ("restic" | "direct-ssh") +# Hooks are optional — apps without them just inherit the standard flow. + +# Run a single named hook if it exists. Quiet if not defined. +migrateRunHook() +{ + local app="$1" + local stage="$2" # "pre" or "post" + local source="$3" + local transport="$4" + + local hook_name="${app}_migrate_${stage}" + if declare -f "$hook_name" >/dev/null 2>&1; then + isNotice "Running ${stage}-migrate hook for ${app}" + migrateEmit phase="hook-${stage}" status=running app="$app" hook="$hook_name" + if "$hook_name" "$source" "$transport"; then + migrateEmit phase="hook-${stage}" status=complete app="$app" + else + isNotice "Hook ${hook_name} returned non-zero — continuing migrate" + migrateEmit phase="hook-${stage}" status=failed app="$app" + fi + fi +} diff --git a/scripts/peer/peer_pull.sh b/scripts/peer/peer_pull.sh index 0299609..7e007cb 100644 --- a/scripts/peer/peer_pull.sh +++ b/scripts/peer/peer_pull.sh @@ -64,6 +64,9 @@ peerPullApp() migrateEmit phase=pre-backup status=skipped reason=user-opt-out app="$app" fi + # Per-app pre-migrate hook (optional) — declared in the app's tools.sh. + migrateRunHook "$app" pre "$peer_name" "direct-ssh" + # ---- 3. Stop + wipe ---------------------------------------------------- migrateEmit phase=stop status=running app="$app" if declare -f dockerComposeDown >/dev/null 2>&1 && [[ -d "$containers_dir$app" ]]; then @@ -117,6 +120,9 @@ peerPullApp() fi migrateEmit phase=start-app status=complete app="$app" + # Per-app post-migrate hook (optional) — last thing before declaring done. + migrateRunHook "$app" post "$peer_name" "direct-ssh" + local finished_at; finished_at=$(date +%s) local duration=$((finished_at - started_at)) isSuccessful "Pulled $app from $peer_name in ${duration}s" diff --git a/scripts/source/files/arrays/files_migrate.sh b/scripts/source/files/arrays/files_migrate.sh index 8376026..517c41e 100755 --- a/scripts/source/files/arrays/files_migrate.sh +++ b/scripts/source/files/arrays/files_migrate.sh @@ -6,6 +6,7 @@ migrate_scripts=( "migrate/migrate_apply.sh" "migrate/migrate_discover.sh" + "migrate/migrate_hooks.sh" "migrate/migrate_pre_backup.sh" "migrate/migrate_preflight.sh" "migrate/migrate_progress.sh"