Merge claude/2

This commit is contained in:
librelad 2026-05-26 18:00:26 +01:00
commit 64a0509ea9
5 changed files with 70 additions and 5 deletions

View File

@ -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 {
<h3 style="margin:0">${this.escape(loc.name || 'Location')}</h3>
<span class="backup-card-hint">${(loc.hosts || []).length} other host${loc.hosts.length === 1 ? '' : 's'} backing up here</span>
</div>
${loc.hosts.map(host => `
${loc.hosts.map(host => {
const peerName = (this.hostnameToPeerName || {})[host.hostname];
const headerLabel = peerName
? `<strong style="font-size:1.05em">${this.escape(peerName)}</strong><span class="backup-card-hint" style="margin-left:6px; font-size:.85em">host: <code>${this.escape(host.hostname)}</code></span>`
: `<strong style="font-size:1.05em">${this.escape(host.hostname)}</strong>`;
return `
<div class="backup-migrate-host" style="border:1px solid var(--border-color, #2a2a2a); border-radius:8px; padding:14px; margin-bottom:12px">
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:10px">
<div>
<strong style="font-size:1.05em">${this.escape(host.hostname)}</strong>
${headerLabel}
<span class="backup-card-hint" style="margin-left:10px">${(host.apps || []).length} app${host.apps.length === 1 ? '' : 's'} available</span>
</div>
<button class="backup-primary-btn" data-action="migrate-host"
@ -1846,7 +1860,8 @@ class BackupPage {
}).join('')}
</div>
</div>
`).join('')}
`;
}).join('')}
</div>
`).join('');

View File

@ -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))

View File

@ -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:
#
# <app>_migrate_pre() # called before stop+wipe
# <app>_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
}

View File

@ -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"

View File

@ -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"