feat(web,docs): the Updates card — version, channel, install kind, and the exact update command
android / android (push) Canceled after 1m54s
apple / swift (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
ci / docs-site (push) Successful in 1m15s
arch / build-publish (push) Canceled after 1m50s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 14s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 14s
ci / rust (push) Canceled after 59s
ci / rust-arm64 (push) Canceled after 2m31s
ci / web (push) Canceled after 0s
deb / build-publish (push) Canceled after 18s
deb / build-publish-host (push) Canceled after 19s
deb / build-publish-client-arm64 (push) Canceled after 12s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 1m15s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 36s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 2m28s
windows-host / package (push) Canceled after 0s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
decky / build-publish (push) Successful in 50s
android / android (push) Canceled after 1m54s
apple / swift (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
ci / docs-site (push) Successful in 1m15s
arch / build-publish (push) Canceled after 1m50s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 14s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 14s
ci / rust (push) Canceled after 59s
ci / rust-arm64 (push) Canceled after 2m31s
ci / web (push) Canceled after 0s
deb / build-publish (push) Canceled after 18s
deb / build-publish-host (push) Canceled after 19s
deb / build-publish-client-arm64 (push) Canceled after 12s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 1m15s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 36s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 2m28s
windows-host / package (push) Canceled after 0s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
decky / build-publish (push) Successful in 50s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"steamos-host",
|
||||
"windows-host",
|
||||
"web-console",
|
||||
"updating",
|
||||
"---Configure your desktop---",
|
||||
"configuration",
|
||||
"kde",
|
||||
|
||||
@@ -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`.
|
||||
+18
-1
@@ -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"
|
||||
}
|
||||
|
||||
+18
-1
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 <UpdateCard state={status} onCheck={checkNow} busy={check.isPending} />;
|
||||
};
|
||||
|
||||
export const UpdateCard: FC<{
|
||||
state: Loadable<UpdateStatus>;
|
||||
onCheck: () => void;
|
||||
busy: boolean;
|
||||
}> = ({ state, onCheck, busy }) => {
|
||||
const s = state.data;
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>{m.update_title()}</CardTitle>
|
||||
{s?.available && <Badge>{m.update_available_badge()}</Badge>}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<QueryState
|
||||
isLoading={state.isLoading}
|
||||
error={state.error}
|
||||
refetch={state.refetch}
|
||||
>
|
||||
{s && (
|
||||
<>
|
||||
<dl className="grid grid-cols-1 gap-3">
|
||||
<UpdateRow
|
||||
label={m.update_current()}
|
||||
value={
|
||||
<span className="flex items-center gap-2 font-medium">
|
||||
{s.current_version}
|
||||
<Badge variant="secondary">{s.channel}</Badge>
|
||||
<Badge variant="outline" title={m.update_install_kind()}>
|
||||
{s.install_kind}
|
||||
</Badge>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<UpdateRow
|
||||
label={m.update_latest()}
|
||||
value={
|
||||
s.manifest ? (
|
||||
<span className="flex items-center gap-2 font-medium">
|
||||
{s.manifest.version}
|
||||
{s.manifest.notes_url && (
|
||||
<a
|
||||
href={s.manifest.notes_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-sm font-normal underline underline-offset-2"
|
||||
>
|
||||
{m.update_notes()}
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{m.update_never_checked()}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</dl>
|
||||
|
||||
{s.available ? (
|
||||
<div className="space-y-2 rounded-md border p-4">
|
||||
<p className="text-sm">{m.update_how()}</p>
|
||||
<CommandLine command={s.channel_hint} />
|
||||
</div>
|
||||
) : (
|
||||
s.manifest && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{m.update_up_to_date()}
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
|
||||
{s.manifest?.stale && (
|
||||
<p className="rounded-md border border-amber-500/40 bg-amber-500/10 p-3 text-sm">
|
||||
{m.update_stale()}
|
||||
</p>
|
||||
)}
|
||||
{s.last_error && (
|
||||
<p className="text-sm text-destructive">
|
||||
{m.update_error()} {s.last_error}
|
||||
</p>
|
||||
)}
|
||||
{s.check_disabled ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{m.update_disabled()}
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onCheck}
|
||||
disabled={busy}
|
||||
>
|
||||
{busy ? m.update_checking() : m.update_check_now()}
|
||||
</Button>
|
||||
{s.last_checked_unix != null && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{m.update_last_checked()}{" "}
|
||||
{new Date(s.last_checked_unix * 1000).toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</QueryState>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const UpdateRow: FC<{ label: string; value: ReactNode }> = ({
|
||||
label,
|
||||
value,
|
||||
}) => (
|
||||
<div className="flex items-baseline justify-between gap-4">
|
||||
<dt className="text-sm text-muted-foreground">{label}</dt>
|
||||
<dd>{value}</dd>
|
||||
</div>
|
||||
);
|
||||
|
||||
/** 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 (
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="min-w-0 flex-1 overflow-x-auto rounded bg-muted px-2 py-1.5 text-xs">
|
||||
{command}
|
||||
</code>
|
||||
<Button variant="ghost" size="sm" onClick={copy}>
|
||||
{copied ? m.update_copied() : m.update_copy()}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 <HostView host={host} compositors={compositors} gpu={<GpuSection />} />;
|
||||
return (
|
||||
<HostView
|
||||
host={host}
|
||||
compositors={compositors}
|
||||
gpu={<GpuSection />}
|
||||
update={<UpdateSection />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,9 @@ export const HostView: FC<{
|
||||
compositors: Loadable<AvailableCompositor[]>;
|
||||
/** 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 (
|
||||
<Section maxWidth={false}>
|
||||
@@ -88,6 +90,8 @@ export const HostView: FC<{
|
||||
)}
|
||||
</QueryState>
|
||||
|
||||
{update}
|
||||
|
||||
{gpu}
|
||||
|
||||
<Card>
|
||||
|
||||
Reference in New Issue
Block a user