diff --git a/containers/libreportal/frontend/core/setup/js/setup-wizard.js b/containers/libreportal/frontend/core/setup/js/setup-wizard.js index 709b4a8..3d77b01 100755 --- a/containers/libreportal/frontend/core/setup/js/setup-wizard.js +++ b/containers/libreportal/frontend/core/setup/js/setup-wizard.js @@ -1141,9 +1141,12 @@ class SetupWizard { this.restoreInfo = data; if (status) { + const nApps = (data.apps || []).length; + const hasSys = !!(data.system && data.system.present); status.innerHTML = `
Found backups from ${this.escapeHtml(data.host || '')} \u2014 - ${(data.apps || []).length} app(s). Continue to see what will happen.
`; + ${hasSys ? 'settings and ' : ''}${nApps} app${nApps === 1 ? '' : 's'}. + Continue to see what will happen.`; } await this.renderRestoreContents(); } catch (e) { @@ -1156,9 +1159,21 @@ class SetupWizard { } } - // What is in there, and what it will mean on this machine. The domain checks - // are the part that has no equivalent anywhere else: a restored domain still - // points wherever DNS says, which after a rebuild is usually the old server. + // What is in there, and what it will mean on this machine. + // + // A repository holds two DIFFERENT kinds of snapshot, and this step shows + // them as two things because they ARE two things: + // + // the system snapshot one snapshot of the settings tree — logins, the + // domains, and every backup repository with its + // credentials. Restored first, because it is what + // makes the others reachable. + // the app snapshots one per app, each with its own size and date, so + // an app can be restored or aged out on its own. + // + // The first version listed "Apps" and "Domains" as peers, which hid all of + // that: the domains are not a third thing in the backup, they are part of + // the system snapshot. async renderRestoreContents() { const box = this.container.querySelector('#sw-rs-contents'); if (!box) return; @@ -1168,34 +1183,74 @@ class SetupWizard { return; } + const system = d.system || {}; const apps = d.apps || []; - const domains = d.domains || []; + const domains = system.domains || []; + const others = (d.hosts || []).filter(h => h !== d.host); box.innerHTML = `From ${this.escapeHtml(d.host || '')}${ - (d.hosts || []).length > 1 - ? ` \u2014 this repository also holds backups from ${this.escapeHtml((d.hosts || []).filter(h => h !== d.host).join(', '))}` + others.length + ? ` \u2014 this repository also holds backups from ${this.escapeHtml(others.join(', '))}` : ''}
-+ Your logins, your domains, and every backup repository you had \u2014 + with its credentials, so this one password brings the rest back. + Restored first, because it is what makes the others reachable. +
+Checking where the domains point\u2026
' + : 'No domains were configured on that machine.
'}+ This repository has no settings snapshot \u2014 only app data. Your + backup repositories and logins will not come back with it. +
`} + ++ One snapshot each, restored after the settings. +
` + + apps.map(a => `No app backups in this repository.
'} -Checking where these point\u2026
' - : 'This backup carries no domains.
'}No app backups in this repository.
'}`; if (!domains.length) return; + await this._checkRestoreDomains(domains); + } - // Checked one at a time through the endpoint the Domains step already - // uses, rather than adding a second way to ask the same question. + // "28 Aug 2026, 13:10" — a snapshot's age is the thing people actually judge + // a backup by, and an ISO timestamp is not something anyone reads at a + // glance. + _restoreWhen(iso) { + if (!iso) return ''; + const d = new Date(iso); + if (isNaN(d.getTime())) return ''; + return this.escapeHtml(d.toLocaleString(undefined, { + day: 'numeric', month: 'short', year: 'numeric', + hour: '2-digit', minute: '2-digit' + })); + } + + // Where each restored domain actually points. Checked one at a time through + // the endpoint the Domains step already uses, rather than adding a second + // way to ask the same question. + async _checkRestoreDomains(domains) { const rows = []; for (const domain of domains) { let verdict = 'unknown', detail = 'could not check'; @@ -1207,15 +1262,18 @@ class SetupWizard { if (j && j.matches) { verdict = 'ok'; detail = 'points at this server'; } else if (j && j.domain_ip) { verdict = 'elsewhere'; detail = `points at ${j.domain_ip}, not this server`; } else { verdict = 'unresolved'; detail = 'no DNS record found'; } - } catch { /* left as unknown — see below */ } + } catch { /* left as unknown — never offered for deletion */ } rows.push({ domain, verdict, detail }); } this.restoreDomains = rows; - const strays = rows.filter(r => r.verdict === 'elsewhere' || r.verdict === 'unresolved'); const el = this.container.querySelector('#sw-rs-domains'); if (!el) return; + // Unverifiable is not the same as wrong, so only a domain that + // demonstrably resolves elsewhere is offered for removal. + const strays = rows.filter(r => r.verdict === 'elsewhere' || r.verdict === 'unresolved'); el.innerHTML = + `Domains it will bring across:
` + rows.map(r => `This will, in order:
-
Apps this version no longer ships, or that will not fit, are skipped
diff --git a/docs/roadmap/first-run-restore.md b/docs/roadmap/first-run-restore.md
index fd7465b..5e1ba8c 100644
--- a/docs/roadmap/first-run-restore.md
+++ b/docs/roadmap/first-run-restore.md
@@ -503,10 +503,35 @@ schedule, no enable toggle. The password leaves through the one-shot
never appears in the payload, since that payload reaches a task command line
and tasks are recorded world-readable.
-**Contents** is §3's reconciliation, rendered. Apps with sizes, and the domains
-with a verdict each — checked through the same `/api/setup/dns-check` the
-Domains step uses rather than adding a second way to ask the question — plus
-the offer to leave the strays out until DNS is repointed.
+**Contents** is §3's reconciliation, rendered — and it has to make the
+repository's *shape* visible, which the first version did not.
+
+A repository holds two different kinds of snapshot:
+
+| | How many | Holds | Restored |
+|---|---|---|---|
+| `system=config` | **one** | the whole configs tree: logins, domains, and every backup repository with its credentials | first — it is what makes the others reachable |
+| `app=