From b275e6d34c8137c33341e54f4ec9ed8e4f951264 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 30 Jul 2026 14:12:11 +0200 Subject: [PATCH] =?UTF-8?q?feat(web,docs):=20the=20Updates=20card=20?= =?UTF-8?q?=E2=80=94=20version,=20channel,=20install=20kind,=20and=20the?= =?UTF-8?q?=20exact=20update=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notify-only (U0): polls /update/status (which keeps the host's manifest cache warm), Check-now with the 30s limit surfaced, release-notes link, stale-feed and check-disabled states, copyable per-install-kind command. en+de strings; docs-site 'Updating the Host' page wired into the install section. Co-Authored-By: Claude Fable 5 --- docs-site/content/docs/meta.json | 1 + docs-site/content/docs/updating.md | 60 +++++++++ web/messages/de.json | 19 ++- web/messages/en.json | 19 ++- web/src/sections/Host/UpdateCard.tsx | 184 +++++++++++++++++++++++++++ web/src/sections/Host/index.tsx | 10 +- web/src/sections/Host/view.tsx | 6 +- 7 files changed, 295 insertions(+), 4 deletions(-) create mode 100644 docs-site/content/docs/updating.md create mode 100644 web/src/sections/Host/UpdateCard.tsx diff --git a/docs-site/content/docs/meta.json b/docs-site/content/docs/meta.json index 841f26cf..28e6276a 100644 --- a/docs-site/content/docs/meta.json +++ b/docs-site/content/docs/meta.json @@ -15,6 +15,7 @@ "steamos-host", "windows-host", "web-console", + "updating", "---Configure your desktop---", "configuration", "kde", diff --git a/docs-site/content/docs/updating.md b/docs-site/content/docs/updating.md new file mode 100644 index 00000000..1d4a00fa --- /dev/null +++ b/docs-site/content/docs/updating.md @@ -0,0 +1,60 @@ +--- +title: Updating the Host +description: How to see when a newer punktfunk host is available — the web console's update card — and the update command for every install method. +--- + +The web console tells you when a newer host is out. The **Host** page has an **Updates** card +showing the version you run, the channel you follow (stable or canary), how this host was +installed, and — once a newer release exists — the exact command that updates it. The +"update available" state also fires an `update.available` event on the host +[event stream](/docs/automation), so hooks and scripts can react to it too. + +The check is a small signed manifest the host fetches from the punktfunk release feed and +verifies against keys built into the host itself — a tampered or replayed feed is rejected, and +the console will tell you when a check failed rather than silently showing stale facts. + +## Updating, per install method + +The console shows the right one of these automatically; for reference: + +| How you installed | How to update | +|---|---| +| Windows installer / winget | `winget upgrade unom.PunktfunkHost`, or run the newer `punktfunk-host-setup.exe` | +| Ubuntu / Debian (apt) | `sudo apt update && sudo apt install --only-upgrade punktfunk-host` | +| Fedora (dnf) | `sudo dnf upgrade punktfunk` | +| Bazzite sysext (recommended) | `sudo punktfunk-sysext update` | +| Bazzite rpm-ostree layer | `sudo /usr/share/punktfunk/update-punktfunk.sh` (staged — reboot to finish) | +| Arch / CachyOS (pacman) | `sudo pacman -Syu` (a normal full system upgrade) | +| Steam Deck (on-device build) | `bash ~/punktfunk/scripts/steamdeck/update.sh --pull` | +| NixOS | update the flake input and rebuild | + +After a Linux package update, restart the host to pick up the new binary: + +```bash +systemctl --user restart punktfunk-host +``` + +(The Windows installer restarts the service itself; `punktfunk-sysext update` prints the same +restart hint when it's needed.) + +One-click updating from the console is on the way, per install method — the card will grow an +**Apply** button where the platform supports it. + +## Turning the check off + +The check contacts `git.unom.io` (the punktfunk forge) and nothing else, and sends nothing but a +normal download request. If you'd rather the host never checks, set: + +```bash +PUNKTFUNK_UPDATE_CHECK=0 +``` + +in the host's environment (`host.env` on Windows, the systemd user unit environment on Linux). +The card then shows checks as disabled; everything else keeps working. + +## If the card says the feed is stale + +"Feed hasn't changed in over 45 days" means checks *succeed* but nothing new arrives. Usually +that just means no release happened for a while; if the [releases page](https://git.unom.io/unom/punktfunk/releases) +shows something newer than the card does, something between this host and the feed is pinning old +data — worth a look at proxies or DNS on the way to `git.unom.io`. diff --git a/web/messages/de.json b/web/messages/de.json index e3ec8b46..9b45849a 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -439,5 +439,22 @@ "display_monitor_unsupported": "Das Übertragen eines dieser Bildschirme wird auf diesem Host noch nicht unterstützt — das gibt es bisher nur auf Linux-Hosts. Die Bildschirme sind hier aufgeführt, damit du siehst, was dieser Computer hat; Clients bekommen weiterhin ihren eigenen virtuellen Bildschirm.", "display_monitor_primary": "primär", "display_monitor_disabled": "aus", - "display_monitor_saved": "Übertragener Bildschirm gespeichert" + "display_monitor_saved": "Übertragener Bildschirm gespeichert", + "update_title": "Updates", + "update_available_badge": "Update verfügbar", + "update_current": "Installiert", + "update_install_kind": "Installationsart", + "update_latest": "Neueste Version", + "update_notes": "Versionshinweise", + "update_up_to_date": "Du bist auf dem neuesten Stand.", + "update_how": "Diesen Host aktualisieren mit:", + "update_check_now": "Jetzt prüfen", + "update_checking": "Prüfe…", + "update_last_checked": "Zuletzt geprüft", + "update_never_checked": "Noch nicht geprüft", + "update_stale": "Der Update-Feed hat sich seit über 45 Tagen nicht geändert — Prüfungen gelingen, aber es kommt nichts Neues an. Falls das nicht stimmen kann, prüfe die Verbindung dieses Hosts zu git.unom.io.", + "update_disabled": "Update-Prüfungen sind auf diesem Host deaktiviert (PUNKTFUNK_UPDATE_CHECK=0).", + "update_error": "Letzte Prüfung fehlgeschlagen:", + "update_copy": "Kopieren", + "update_copied": "Kopiert" } diff --git a/web/messages/en.json b/web/messages/en.json index cd65115a..cad64659 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -439,5 +439,22 @@ "display_monitor_unsupported": "Streaming one of these screens isn't supported on this host yet — it's available on Linux hosts only. The screens are listed so you can see what this computer has; clients keep getting their own virtual screen.", "display_monitor_primary": "primary", "display_monitor_disabled": "off", - "display_monitor_saved": "Streamed screen saved" + "display_monitor_saved": "Streamed screen saved", + "update_title": "Updates", + "update_available_badge": "Update available", + "update_current": "Installed", + "update_install_kind": "Install type", + "update_latest": "Latest release", + "update_notes": "Release notes", + "update_up_to_date": "You're up to date.", + "update_how": "Update this host with:", + "update_check_now": "Check now", + "update_checking": "Checking…", + "update_last_checked": "Last checked", + "update_never_checked": "Not checked yet", + "update_stale": "The update feed hasn't changed in over 45 days — checks succeed but nothing new arrives. If that seems wrong, check this host's connectivity to git.unom.io.", + "update_disabled": "Update checks are disabled on this host (PUNKTFUNK_UPDATE_CHECK=0).", + "update_error": "Last check failed:", + "update_copy": "Copy", + "update_copied": "Copied" } diff --git a/web/src/sections/Host/UpdateCard.tsx b/web/src/sections/Host/UpdateCard.tsx new file mode 100644 index 00000000..ad0b58ad --- /dev/null +++ b/web/src/sections/Host/UpdateCard.tsx @@ -0,0 +1,184 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { Button } from "@unom/ui/button"; +import { type FC, type ReactNode, useState } from "react"; +import { + getGetUpdateStatusQueryKey, + useForceUpdateCheck, + useGetUpdateStatus, +} from "@/api/gen/update/update"; +import type { UpdateStatus } from "@/api/gen/model"; +import { QueryState } from "@/components/query-state"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import type { Loadable } from "@/lib/query"; +import { m } from "@/paraglide/messages"; + +/** + * Container: the host update-check card (U0 — notify-only). Reading status is what keeps the + * host's manifest cache warm (the host kicks its own background refresh when the cache is >6 h + * old), so a modest poll doubles as the check cadence while the console is open. Apply buttons + * arrive with the per-channel apply legs (U1 Windows, U2 Linux helper); until then the card's + * action is the exact update command for this install kind. + */ +export const UpdateSection: FC = () => { + const qc = useQueryClient(); + const status = useGetUpdateStatus({ query: { refetchInterval: 60_000 } }); + const check = useForceUpdateCheck(); + + const checkNow = () => + check.mutate(undefined, { + onSuccess: (fresh) => { + qc.setQueryData(getGetUpdateStatusQueryKey(), fresh); + }, + }); + + return ; +}; + +export const UpdateCard: FC<{ + state: Loadable; + onCheck: () => void; + busy: boolean; +}> = ({ state, onCheck, busy }) => { + const s = state.data; + return ( + + + {m.update_title()} + {s?.available && {m.update_available_badge()}} + + + + {s && ( + <> +
+ + {s.current_version} + {s.channel} + + {s.install_kind} + + + } + /> + + {s.manifest.version} + {s.manifest.notes_url && ( + + {m.update_notes()} + + )} + + ) : ( + + {m.update_never_checked()} + + ) + } + /> +
+ + {s.available ? ( +
+

{m.update_how()}

+ +
+ ) : ( + s.manifest && ( +

+ {m.update_up_to_date()} +

+ ) + )} + + {s.manifest?.stale && ( +

+ {m.update_stale()} +

+ )} + {s.last_error && ( +

+ {m.update_error()} {s.last_error} +

+ )} + {s.check_disabled ? ( +

+ {m.update_disabled()} +

+ ) : ( +
+ + {s.last_checked_unix != null && ( + + {m.update_last_checked()}{" "} + {new Date(s.last_checked_unix * 1000).toLocaleString()} + + )} +
+ )} + + )} +
+
+
+ ); +}; + +const UpdateRow: FC<{ label: string; value: ReactNode }> = ({ + label, + value, +}) => ( +
+
{label}
+
{value}
+
+); + +/** The copy-pastable update command, with a small clipboard affordance. */ +const CommandLine: FC<{ command: string }> = ({ command }) => { + const [copied, setCopied] = useState(false); + const copy = () => { + navigator.clipboard + .writeText(command) + .then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }) + .catch(() => { + /* clipboard denied — the text is selectable, nothing to do */ + }); + }; + return ( +
+ + {command} + + +
+ ); +}; diff --git a/web/src/sections/Host/index.tsx b/web/src/sections/Host/index.tsx index 322518d7..f0532ea2 100644 --- a/web/src/sections/Host/index.tsx +++ b/web/src/sections/Host/index.tsx @@ -2,6 +2,7 @@ import type { FC } from "react"; import { useGetHostInfo, useListCompositors } from "@/api/gen/host/host"; import { useLocale } from "@/lib/i18n"; import { GpuSection } from "./GpuCard"; +import { UpdateSection } from "./UpdateCard"; import { HostView } from "./view"; export const SectionHost: FC = () => { @@ -9,5 +10,12 @@ export const SectionHost: FC = () => { const host = useGetHostInfo(); const compositors = useListCompositors(); - return } />; + return ( + } + update={} + /> + ); }; diff --git a/web/src/sections/Host/view.tsx b/web/src/sections/Host/view.tsx index 0b546dcf..b595230c 100644 --- a/web/src/sections/Host/view.tsx +++ b/web/src/sections/Host/view.tsx @@ -14,7 +14,9 @@ export const HostView: FC<{ compositors: Loadable; /** The GPU inventory/selection card (a self-contained container — see `GpuCard.tsx`). */ gpu?: ReactNode; -}> = ({ host, compositors, gpu }) => { + /** The update-check card (a self-contained container — see `UpdateCard.tsx`). */ + update?: ReactNode; +}> = ({ host, compositors, gpu, update }) => { const h = host.data; return (
@@ -88,6 +90,8 @@ export const HostView: FC<{ )} + {update} + {gpu}