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 && (
+ <>
+