From 3c2f8f0691068b9c003ba35567bf327c03409234 Mon Sep 17 00:00:00 2001 From: librelad Date: Thu, 27 Aug 2026 08:57:26 +0100 Subject: [PATCH] docs(roadmap): rewrite phases 3-4 for the installer-based restore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phases 3 and 4 were written when the restore was going to be a WebUI wizard branch. It runs in the installer now, so both change shape. Reconciliation stops being a screen and becomes a preflight report. The key realisation is that it can run BEFORE anything is written: every app's snapshot carries its own .libreportal-manifest.json, and engineDumpFile already pulls a single file out of a snapshot without restoring it. So the installer can read all the manifests, compare them to this machine, and print a verdict per app — skipping an app whose template this version no longer ships, or whose data will not fit, rather than restoring something unstartable. Phase 4 is now the thing the original request actually described. 'Upload or navigate to the backup file' does not match a restic repository, but the want behind it is real: one file, one app, hand it around. 'libreportal app export/import' with a .lpapp (tar.gz of the app dir plus its manifest) serves it directly, gets §3's reconciliation for free, and gives the installer a third answer to 'where is your backup?'. Documented as a courier format, explicitly not a backup. Co-Authored-By: Claude Opus 5 --- docs/roadmap/first-run-restore.md | 81 ++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/docs/roadmap/first-run-restore.md b/docs/roadmap/first-run-restore.md index 416bc0c..03c9bad 100644 --- a/docs/roadmap/first-run-restore.md +++ b/docs/roadmap/first-run-restore.md @@ -1,6 +1,6 @@ # LibrePortal — First-run: New Install or Restore (Roadmap / Proposal) -**Status:** Proposal — **not built**, but most of the machinery is. · **Audience:** us, future-self · **Scope:** make "I'm rebuilding my server" a first-class path at first run, not a CLI expedition · **Origin:** "on the first install/setup we need 2 option blocks (New Install and Restore from Backup)" (2026-08-27) +**Status:** Phases 1–2 **built**; 3–4 specified below. · **Audience:** us, future-self · **Scope:** make "I'm rebuilding my server" a first-class path at first run, not a CLI expedition · **Origin:** "on the first install/setup we need 2 option blocks (New Install and Restore from Backup)" (2026-08-27) --- @@ -51,20 +51,42 @@ So this is mostly **assembly plus one genuinely new screen**, not new plumbing. 4. **What will change on this machine** — the reconciliation screen. §3. The new part. 5. **Restore** — system config first, then the chosen apps, with the task list the wizard already renders. -## 3. The reconciliation screen — the only genuinely new logic +## 3. Reconciliation — a preflight report, not a screen -A backup describes a machine that no longer exists. Restoring it onto different hardware means a set of facts have to be re-decided, and the failure mode we care about is doing that silently. Everything below is *detectable before anything is written*: +Originally written as a WebUI screen. The restore runs in the **installer** +(§2), so it becomes a report printed before anything is written, plus automatic +resolution where the answer is not a judgement call. -| What differs | Where we already know it | Resolution | +A backup describes a machine that no longer exists. The failure mode worth +designing against is re-deciding those facts silently. + +**It is answerable before restoring** because every app's snapshot carries its +own `.libreportal-manifest.json`, and `engineDumpFile` can pull a single file +out of a snapshot without restoring it. So the installer can read all 13 +manifests, compare them to this machine, and print a verdict per app. + +| What differs | Detected from | Resolution | |---|---|---| -| Storage location named in the manifest doesn't exist here | manifest `storage.location` vs the registry | offer the drives this host has, or register a new one — the §9 prompt | -| App's old absolute path ≠ where it goes here | manifest `storage.path` | already handled: `storageRestoreAppTo` stages and moves | -| Domains point at the old host | `migrateUrlRewrite` | rewrite (its existing per-app opt-out still applies) | -| Ports / IPs already taken on this box | port allocation tables | reallocate, and say which changed | -| Drive smaller than the data | manifest `size_bytes` vs `df` | refuse *that app*, not the whole restore | -| Apps in the backup that this version no longer ships | template presence | list them as skipped rather than failing | +| App no longer shipped by this version | template missing under `install/containers/` | **skip**, and say so — restoring an app whose template is gone produces an unstartable directory | +| Data larger than the target drive | manifest `size_bytes` vs `df` | **skip that app**, not the whole restore | +| Storage location named in the manifest doesn't exist here | manifest `storage.location` vs the registry | fall back to the default location, and say which app moved where | +| Old absolute path ≠ where it goes here | manifest `storage.path` | already handled — `storageRestoreAppTo` stages and moves | +| Domains point at the old host | — | already handled — `migrateUrlRewrite` | +| Ports / IPs already taken | port allocation tables | already handled by the install pipeline; reallocated on start | -It should read as a diff, not a wall: **"12 apps · 3 need a decision · 9 restore as-is"**, with the three expanded. +Output is a verdict list, then one confirmation: + +``` + Checking 13 apps against this machine… + + ✓ bookstack 2.1G restores as-is + ✓ nextcloud 44G -> bigdisk (its old location "ssd" is not on this machine) + ✗ obsolete-app 120M skipped — this version no longer ships it + ✗ jellyfin 1.2T skipped — needs 1.2T, /mnt/data has 400G free + + 11 will restore, 2 skipped. + Continue? [Y/n]: +``` ## 4. The password problem, stated plainly @@ -124,7 +146,7 @@ Same components, other direction. After a **New install**, offer: *"Where should Worth doing as its own wizard step even without the restore branch. -## 6. On "upload the backup file" +## 7. On "upload the backup file" — why the format differs Worth being precise, because the mental model doesn't match the engines. restic, borg and kopia back up to a **repository** — a directory or a remote — not a single file. There is nothing to upload. The equivalents are: @@ -133,16 +155,39 @@ Worth being precise, because the mental model doesn't match the engines. restic, If a genuine single-file import is wanted, that is a **different feature**: a portable per-app export (`tar` of the app dir + manifest, optionally encrypted) that could be handed around and imported. Cheap to build on the manifest that already exists, but it is not what the backup engines produce and shouldn't be conflated with them. -## 7. Phasing +## 6. Portable export — the single file people actually mean + +§7 explains why "upload the backup file" does not match a restic repository. +But the underlying want is real and worth serving directly: **one file, one +app, hand it around**. + + libreportal app export [file] # -> -.lpapp + libreportal app import + +The format is deliberately boring: a gzipped tar of the app directory with its +`.libreportal-manifest.json` at the root. That manifest already records the +compose hash, images, volumes, size, databases and storage location, so import +gets the same reconciliation as §3 for free. + +This also gives the installer a third answer to "where is your backup?" — a +`.lpapp` file — which is exactly the "navigate to the backup file" flow that +prompted this document. + +Not a replacement for the backup engines: no deduplication, no history, no +encryption unless the user encrypts it themselves. It is a courier format — +moving one app between machines, or keeping a copy of something before a risky +change — and the docs should say so plainly so nobody uses it as their backup. + +## 8. Phasing | Phase | Deliverable | |---|---| -| **1** ✅ | Backup destination step for the New-install path (§5) — built | -| **2** ⛔ | The two blocks and the restore branch. **Blocked on §4.1** — the typed repository password has no safe channel to the host yet | -| **3** | The reconciliation screen (§3) and the restore itself, driven by `restoreFirstRunBulk` | -| **4** | Portable per-app export/import (§6), if wanted | +| **1** ✅ | Backup destination step in the WebUI wizard (§5) — the *new setup* half | +| **2** ✅ | Two installer paths: New setup / Restore from backup, through connect → discover → system config → apps | +| **3** | Preflight reconciliation report in the installer (§3) | +| **4** | `app export` / `app import`, and a `.lpapp` option in the installer's restore path (§4) | -## 8. Open questions +## 9. Open questions 1. **Does the restore branch also restore the system config's *identity*** — install name, domains, WebUI credentials? Restoring the WebUI login means the user logs into the new box with the old password, which is probably what they expect, but it is a surprise if not stated. 2. **Partial restore of a host** — pick apps individually (already supported by `restoreFirstRunBulk`'s signature) or all-or-nothing at first run?