diff --git a/docs-site/content/docs/updating.md b/docs-site/content/docs/updating.md index b53862e7..0bb68e49 100644 --- a/docs-site/content/docs/updating.md +++ b/docs-site/content/docs/updating.md @@ -50,8 +50,27 @@ Every attempt leaves a result in the card (and an installer log under failed update is never silent. To disable the button entirely on a host, set `PUNKTFUNK_UPDATE_APPLY=0` in `host.env`; the card then shows the manual command instead. -One-click updating for the Linux install methods is on the way — it will be opt-in per host -(joining a `punktfunk-update` group) because it needs a narrowly-scoped root helper. +## One-click updating (Linux — opt-in) + +The apt, dnf, Bazzite-sysext, and rpm-ostree installs can one-click update too, via a small +root helper the packages ship (`pf-update` + a `punktfunk-update.service` oneshot). It's **off +until you opt in**, because a web button that ends in root deserves an explicit decision: + +```bash +sudo usermod -aG punktfunk-update $USER # then log out and back in +``` + +That group membership is the entire grant — a polkit rule lets its members start exactly that +one service, whose only job is "run this system's normal package update for the punktfunk +packages, then prove the new binary runs". The button never chooses versions or URLs; your +package manager's own signed repositories stay the source of truth. The card shows the opt-in +command until you've done this, and the manual command always keeps working. + +Notes per method: on **rpm-ostree** the update is staged and the card will say so — reboot to +finish (the console never reboots your machine). On **Arch/pacman** the button additionally +requires `PACMAN_FULL_SYSUPGRADE=1` in `/etc/punktfunk/update.conf`, because the only safe +pacman update is a full `pacman -Syu` — partial upgrades are how Arch boxes break, and we +won't run one. After a successful update the host restarts itself and the page reconnects. ## Turning the check off diff --git a/web/messages/de.json b/web/messages/de.json index 39b215cf..6e2c37c9 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -476,6 +476,9 @@ "update_stage_restarting": "Der Host startet neu…", "update_stage_reconnecting": "Warte, bis der Host wieder da ist…", "update_result_ok": "Aktualisiert {from} → {to}.", + "update_result_staged": "Update auf {to} ist vorbereitet — zum Abschließen den Host neu starten.", + "update_result_noop": "Deine Paketquelle hatte noch nichts Neueres — in ein paar Minuten erneut versuchen.", + "update_opt_in": "Um Ein-Klick-Updates von hier zu aktivieren, einmal auf dem Host ausführen (danach ab- und wieder anmelden):", "update_result_failed": "Update auf {to} ist in Phase {stage} fehlgeschlagen.", "update_result_log": "Installer-Log:" } diff --git a/web/messages/en.json b/web/messages/en.json index f0c3ef92..63a1a87f 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -476,6 +476,9 @@ "update_stage_restarting": "The host is restarting…", "update_stage_reconnecting": "Waiting for the host to come back…", "update_result_ok": "Updated {from} → {to}.", + "update_result_staged": "Update to {to} is staged — reboot this host to finish.", + "update_result_noop": "Your package source had nothing newer yet — try again in a few minutes.", + "update_opt_in": "To enable one-click updates from here, run this once on the host (then log out and back in):", "update_result_failed": "Update to {to} failed during {stage}.", "update_result_log": "Installer log:" } diff --git a/web/src/sections/Host/UpdateCard.tsx b/web/src/sections/Host/UpdateCard.tsx index cd5f5f99..f1593ce9 100644 --- a/web/src/sections/Host/UpdateCard.tsx +++ b/web/src/sections/Host/UpdateCard.tsx @@ -48,14 +48,15 @@ export const UpdateSection: FC = () => { const check = useForceUpdateCheck(); const s = status.data; - // The apply resolved: the host answers with the target version (success — reconcile wrote - // last_result), or reports a failed attempt at our target. + // The apply resolved: either the host answers with the target version (reconcile wrote + // last_result across the restart), or ANY apply outcome newer than our start arrived — + // which covers failure, "staged, reboot to finish", and "your package source had nothing + // newer yet" (where the version deliberately doesn't change). if (applying && s) { if ( s.current_version === applying.target || (s.last_result && - s.last_result.to === applying.target && - !s.last_result.ok) + s.last_result.finished_unix * 1000 >= applying.startedAt - 60_000) ) { setApplying(null); } @@ -157,12 +158,20 @@ export const UpdateCard: FC<{ timedOut={timedOut} /> ) : s.available ? ( - s.apply === "full" ? ( + s.apply === "full" || s.apply === "staged" ? ( ) : (

{m.update_how()}

+ {s.opt_in_hint && ( +
+

+ {m.update_opt_in()} +

+ +
+ )}
) ) : ( @@ -402,7 +411,11 @@ const LastResult: FC<{ }> = ({ result }) => result.ok ? (

- {m.update_result_ok({ from: result.from, to: result.to })} + {result.staged + ? m.update_result_staged({ to: result.to }) + : result.from === result.to + ? m.update_result_noop() + : m.update_result_ok({ from: result.from, to: result.to })}

) : (