Compare commits

...
Author SHA1 Message Date
enricobuehlerandClaude Fable 5 a8099e0f5b docs(quickstart): the client host list — the fifth get-started screenshot, pulled from the pipeline the previous commit teaches to publish
ci / web (pull_request) Successful in 3m38s
ci / bun-nix (pull_request) Successful in 51s
ci / docs-site (pull_request) Successful in 1m25s
ci / rust-arm64 (pull_request) Successful in 5m53s
ci / docs-drift (pull_request) Successful in 50s
ci / rust (pull_request) Successful in 7m46s
Saved hosts with their pairing state + an unpaired host found on the network, from
linux-client-screenshots run 19593 via the new generic-package publish (fixed 'ci' version,
anonymous GET), cropped to the cards. Completes the handoff's screenshot list: console login,
Waiting for approval + PIN, the Approve dialog, live status, and a client's host list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 01:25:44 +02:00
enricobuehlerandClaude Fable 5 674b16d8eb linux-client-screenshots: publish the PNGs to the generic package registry too — the v3 artifact is browser-only, which blocked reusing the shots for the docs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 01:08:31 +02:00
enricobuehler d801cb72f2 Merge pull request 'Windows client: per-user installer + portable zip as the default download — a stable exe path Steam can launch (overlay, Big Picture); MSIX stays for the Store' (#349) from worktree-win-client-installer into main
ci / rust-arm64 (push) Successful in 2m26s
ci / web (push) Successful in 1m53s
ci / bun-nix (push) Successful in 35s
ci / docs-drift (push) Successful in 31s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 12s
ci / docs-site (push) Successful in 2m11s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 13s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 21s
docker / builders (ci/flatpak-ci.Dockerfile, punktfunk-flatpak-ci) (push) Successful in 11s
docker / builders (ci/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (push) Successful in 11s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
ci / rust (push) Successful in 7m57s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 24s
installer-smoke / smoke (arch) (push) Successful in 45s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m34s
installer-smoke / smoke (fedora-44) (push) Successful in 1m18s
installer-smoke / smoke (debian-13) (push) Successful in 2m26s
docker / builders-arm64cross (push) Successful in 31s
docker / deploy-docs (push) Successful in 32s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m39s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 7m48s
2026-08-19 23:05:32 +00:00
enricobuehler ace01f06a2 Merge pull request 'Cut SDK 0.1.5 — the mgmt-endpoint fix cannot reach a plugin until it ships' (#348) from release-sdk-0.1.5 into main
ci / bun-nix (push) Successful in 42s
ci / rust-arm64 (push) Successful in 1m42s
ci / docs-drift (push) Successful in 23s
ci / docs-site (push) Successful in 2m49s
ci / web (push) Successful in 2m51s
deb / build-publish-gamescope (push) Successful in 47s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 38s
deb / build-publish-client-arm64 (push) Successful in 1m41s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 15s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 15s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / builders (ci/flatpak-ci.Dockerfile, punktfunk-flatpak-ci) (push) Successful in 12s
docker / builders (ci/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (push) Successful in 12s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 18s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m1s
sdk-publish / publish (push) Successful in 42s
deb / build-publish (push) Successful in 5m5s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m34s
deb / build-publish-host (push) Successful in 4m57s
arch / build-publish (push) Successful in 11m7s
docker / builders-arm64cross (push) Successful in 11s
ci / rust (push) Canceled after 40s
docker / deploy-docs (push) Canceled after 0s
deb / smoke-install (push) Successful in 3m9s
windows-host / package (push) Successful in 17m26s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 36s
nix / flake (push) Failing after 24m46s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 27m7s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 29m36s
2026-08-19 22:51:11 +00:00
enricobuehler e4ec4cec31 chore(sdk): cut 0.1.5 — the mgmt-endpoint fix cannot reach a plugin until it ships
ci / rust-arm64 (pull_request) Successful in 1m33s
ci / bun-nix (pull_request) Successful in 22s
ci / web (pull_request) Successful in 2m7s
ci / docs-drift (pull_request) Successful in 30s
ci / docs-site (pull_request) Successful in 2m12s
ci / rust (pull_request) Successful in 7m25s
nix / flake (pull_request) Successful in 15m37s
v0.31.0's changelog recorded this cut as owed and said why: plugins resolve
`@punktfunk/host` from the registry, so a fix sitting in `sdk/` reaches nobody
until a version carries it. Four commits have touched the SDK since sdk-v0.1.4,
the headline one being the mgmt-port fix — a moved `PUNKTFUNK_MGMT_BIND` left
every plugin and the tray dialing 47990.

`SDK_VERSION` moves with it. It is a hand-maintained constant (the bundled
runner has no package.json to read at runtime), and the runner compares it
against the SDK actually installed in the plugins tree to decide whether to
reinstall — so shipping 0.1.5 with the constant still reading 0.1.4 would have
published the fix and then never delivered it. `version.test.ts` exists for
exactly this and caught it.

Gates: 83/83 SDK tests pass.
2026-08-20 00:50:46 +02:00
enricobuehler 230d253b06 Merge pull request 'The Deck learns a host's wake MAC, so Wake-on-LAN can fire there at all' (#347) from worktree-deck-wol-learn-mac into main
ci / web (push) Successful in 1m37s
ci / bun-nix (push) Successful in 28s
ci / docs-drift (push) Successful in 35s
ci / rust-arm64 (push) Successful in 2m42s
ci / docs-site (push) Successful in 2m30s
deb / build-publish-gamescope (push) Successful in 1m40s
deb / build-publish-client-arm64 (push) Successful in 1m58s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 20s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 22s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 20s
docker / builders (ci/flatpak-ci.Dockerfile, punktfunk-flatpak-ci) (push) Successful in 19s
docker / builders (ci/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (push) Successful in 17s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 16s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 13s
ci / rust (push) Successful in 7m16s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 59s
deb / build-publish (push) Successful in 5m8s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m19s
arch / build-publish (push) Canceled after 8m19s
deb / build-publish-host (push) Canceled after 5m10s
deb / smoke-install (push) Canceled after 0s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 1s
docker / builders-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 49s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 48s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 6m43s
android / android (push) Successful in 11m48s
flatpak / build-publish (push) Successful in 4m58s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m14s
Reviewed-on: #347
2026-08-19 22:41:32 +00:00
enricobuehler def215ae8e Merge pull request 'Installer bedding-in (--uninstall, NVIDIA silent-failure checks, detection matrix as gate 7) and screenshots for the get-started track' (#346) from worktree-docs-phase2 into main
android / android (push) Canceled after 44s
arch / build-publish (push) Canceled after 40s
ci / rust (push) Canceled after 38s
ci / rust-arm64 (push) Canceled after 37s
ci / web (push) Canceled after 36s
ci / docs-site (push) Canceled after 35s
ci / bun-nix (push) Canceled after 0s
ci / docs-drift (push) Canceled after 0s
deb / build-publish (push) Canceled after 4s
deb / build-publish-host (push) Canceled after 0s
deb / build-publish-gamescope (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Canceled after 0s
deb / smoke-install (push) Canceled after 0s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 12s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 10s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Canceled after 10s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / builders (ci/flatpak-ci.Dockerfile, punktfunk-flatpak-ci) (push) Canceled after 0s
docker / builders (ci/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (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 / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 2s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
decky / build-publish (push) Successful in 47s
installer-smoke / smoke (arch) (push) Successful in 42s
apple / swift (push) Successful in 2m13s
installer-smoke / smoke (debian-13) (push) Successful in 1m2s
installer-smoke / smoke (fedora-44) (push) Successful in 2m15s
apple / distribute (push) Successful in 11m38s
apple / screenshots (push) Successful in 10m38s
2026-08-19 22:40:55 +00:00
enricobuehlerandClaude Fable 5 dfcc530ee7 installer bedding-in: --uninstall, NVIDIA silent-failure checks, the detection matrix as CI gate 7 — and the get-started track gets its screenshots
ci / docs-drift (pull_request) Successful in 48s
ci / docs-site (pull_request) Successful in 1m30s
ci / web (pull_request) Successful in 2m8s
apple / swift (pull_request) Successful in 2m16s
apple / distribute (pull_request) Skipped
installer-smoke / smoke (debian-13) (pull_request) Successful in 58s
apple / screenshots (pull_request) Skipped
installer-smoke / smoke (fedora-44) (pull_request) Successful in 43s
ci / bun-nix (pull_request) Successful in 30s
ci / rust-arm64 (pull_request) Successful in 3m6s
installer-smoke / smoke (arch) (pull_request) Successful in 1m34s
ci / rust (pull_request) Successful in 7m54s
android / android (pull_request) Successful in 8m7s
Phase 2 of the docs-and-onboarding overhaul (items 1-partial, 2 and 4 of the handoff):

install.sh: --uninstall reverses step 1 + step 6 per family (user units off first, only the
punktfunk packages actually installed, then the repo; config/groups/firewall stay, as
/docs/uninstall states) — smoke-tested as a new installer-smoke step on all three families.
The end-of-run check now catches the two NVIDIA silent failures on every family: no driver at
all, and a module the kernel refused to load (Secure Boot) via an nvidia-smi probe pointing at
the troubleshooting anchor; the Fedora ffmpeg-libs/NVENC warning folds into the same block.

check-docs-drift.sh gate 7: the manual 16-file os-release matrix PR #345 was verified with,
committed — every family's detection, its install line, its removal line and the four
unsupported pointers run through the real script under --dry-run on every push (docs-drift's
container gains curl, the script's own prerequisite).

Screenshots (RFC: "screenshots over prose"): four console shots captured from the same
storybook-fixture pipeline web-screenshots.yml runs — login and the armed Pairing page into
quickstart.md, the Approve dialog (access level + expiry + guest fast-path) into pairing.md
replacing the prose that described it, live status into web-console.md. Files under
docs-site/public/img/, dark-theme, bundled+preloaded by the docs build (verified served).
Still missing: a client host-list shot — linux-client-screenshots run 19546 built it, but its
artifact isn't API-downloadable; add it when a browser session can fetch the zip.

WP5 rider: the release-flow docs-freshness step now includes the website content look-over.

NOT flipped: installer stays preview — the handoff gates the default flip on real-box mileage
(Bazzite above all), which a Mac can't provide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 23:56:19 +02:00
enricobuehler 2b81bd286f fix(clients): the Deck learns a host's wake MAC, so Wake-on-LAN can fire there at all
ci / bun-nix (pull_request) Successful in 27s
ci / web (pull_request) Successful in 1m33s
ci / docs-site (pull_request) Successful in 1m41s
ci / rust-arm64 (pull_request) Successful in 1m58s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 3m22s
ci / rust (pull_request) Successful in 5m19s
android / android (pull_request) Successful in 7m21s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m43s
Every wake gate in the codebase reads `!host.mac.is_empty()` against the saved
record — `ConnectPlan::wake`, the console's `can_wake`, `punktfunk wake`. That MAC
only ever reached the store through `trust::learn_mac`, and `learn_mac` had exactly
two callers: the GTK hosts page and the WinUI one.

Neither runs on a Steam Deck. Gaming Mode has only the Decky panel (which drives the
headless CLI) and the console home — and those learned the management port alone,
never the MAC. So a Deck's records stayed MAC-less forever, every wake gate stayed
false, and Wake-on-LAN was skipped silently: no packet, no error, nothing to see.
It worked on desktop purely because those two hosts pages learn on each discovery
tick. (#322)

Rather than add the missing call twice, collapse the three per-field learners
(`learn_mac`, `learn_os`, `learn_mgmt_port` — three `pub fn`s, three load/save
cycles) into one `learn_from_advert`, and call it at every site where an advert
meets a saved record: both desktop hosts pages, the console home, and the CLI's
`discover`. Remembering one call is not a thing a front-end can half-do; remembering
three is what produced this. It takes the three fields rather than a `DiscoveredHost`
because there are two of those — core's and the WinUI shell's verbatim port.

`discover` is where the panel-only flow is fixed: it is the one verb the Decky panel
runs that ever sees an advert. It keeps `KnownHosts::read()`, so it still mints no
ids and cannot join the race that comment warns about, and `learn_from_advert` writes
only when an advert genuinely taught the record something — a steady-state panel
refresh touches no disk.

Two things fall out of the same root cause: the console home now persists the OS
chain too, so a Deck host's icon stops vanishing the moment mDNS goes quiet; and
`punktfunk wake`'s "connect to it once while it's awake" is replaced, since a MAC
comes from an advert and never from a connect — that wording sent this diagnosis
looking in the wrong place.

The magic-packet sender itself was never at fault (`punktfunk-core::wol` passes its
7 tests) and neither was the flatpak sandbox (`--share=network`). Nothing reached
them.

Closes #322
2026-08-19 14:16:14 +02:00
26 changed files with 296 additions and 144 deletions
+1 -1
View File
@@ -417,7 +417,7 @@ jobs:
# oven/bun ships neither git nor a real node, and the slim base has no CA bundle —
# actions/checkout needs all three (see the web job).
- name: Install git + node + CA certs
run: apt-get update && apt-get install -y --no-install-recommends ca-certificates git nodejs
run: apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git nodejs
- uses: actions/checkout@v4
# OpenAPI snapshot in sync, PUNKTFUNK_* vars in docs still exist, undocumented-var
# ratchet (baseline: scripts/ci/docs-undocumented-env-baseline.txt), host-cli.md commands
+6
View File
@@ -61,3 +61,9 @@ jobs:
punktfunk-host detect-conflicts
- name: Re-running is a no-op install
run: sh scripts/install.sh --yes --no-start | grep -q 'already installed'
- name: --uninstall takes the packages and the repo off again
run: |
sh scripts/install.sh --yes --uninstall
! command -v punktfunk-host
! test -e /etc/apt/sources.list.d/punktfunk.list -o -e /etc/yum.repos.d/punktfunk.repo
! grep -q '^\[punktfunk\]' /etc/pacman.conf 2>/dev/null
@@ -111,3 +111,20 @@ jobs:
name: punktfunk-linux-client-screenshots
path: clients/linux/screenshots
retention-days: 30
# The artifact above is browser-only (Gitea's API doesn't serve v3 artifacts), which
# blocked reusing these shots for the docs. Publish them to the generic package registry
# too — fixed version `ci`, delete-then-PUT so each run overwrites, anonymous GET on a
# public repo:
# https://git.unom.io/api/packages/unom/generic/punktfunk-linux-client-screenshots/ci/<scene>.png
- name: Publish screenshots to the package registry
env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
BASE="https://git.unom.io/api/packages/unom/generic/punktfunk-linux-client-screenshots/ci"
for f in clients/linux/screenshots/*.png; do
name=$(basename "$f")
curl -fsS -o /dev/null --user "enricobuehler:$TOKEN" -X DELETE "$BASE/$name" || true
curl -fsS -o /dev/null --user "enricobuehler:$TOKEN" --upload-file "$f" "$BASE/$name"
echo "published $BASE/$name"
done
+1 -1
View File
@@ -45,7 +45,7 @@ three ABIs, which removes the Compose screenshot scenes.
| Host event schema | 1 | **1** | unchanged (`punktfunk-host/src/events.rs`) |
| `api/openapi.json` | 0.29.0 | **0.29.0** | unchanged — no management-API surface moved this cycle; both copies (`api/` and `docs-site/public/`) are byte-identical to each other and to the tag |
| gamescope patch level (`+pfhdrN`) | 8 | **8** | unchanged; no new patch files. ⚠ `packaging/gamescope/PKGBUILD` still says `pfhdr7` — pre-existing at v0.30.0, not a regression this cycle, but the Arch package builds a binary the host's `>= 8` probe rejects for the keymap path |
| `@punktfunk/host` (SDK) | 0.1.4 | **0.1.4** | unchanged in `package.json` — but `sdk/src/config.ts` and `runner-cli.ts` changed (the `mgmt-endpoint` fix below), so a `sdk-v0.1.5` cut is **owed**; plugins resolve the SDK from the registry and cannot pick the fix up until it ships |
| `@punktfunk/host` (SDK) | 0.1.4 | **0.1.5** | cut — `sdk/src/config.ts` and `runner-cli.ts` carry the `mgmt-endpoint` fix below, and plugins resolve the SDK from the registry, so it could not reach them until it shipped |
| `@punktfunk/plugin-kit` | 0.4.2 | **0.4.3** | cut, for the two `sync-engine.ts` changes that cannot reach a plugin any other way: `minInterval` (below) and the always-apply sync reasons (`startup`/`manual` publish even when the fingerprint matches, so a host-side art drop is recoverable by restarting rather than by deleting the plugin's cache). Note the registry skips 0.4.2: `plugin-kit-v0.4.2` was tagged but its publish never landed, and the tag is left where it is rather than moved |
⚠ The SDK and plugin-kit version independently of the app (`sdk-v*` / `plugin-kit-v*` tags,
+18 -5
View File
@@ -370,15 +370,26 @@ from the config directory for a true factory reset."
.unwrap_or(DISCOVER_DEFAULT_SECS)
.min(DISCOVER_MAX_SECS);
let found = pf_client_core::discovery::discover_for(Duration::from_secs_f64(secs));
// `read`, not `load`: this verb only LOOKS at the records to annotate what it found, and
// never hands their ids back. `load` would mint ids for a pre-mint store and save them
// a write from a read-only verb, and one that races the `hosts list` a caller is very
// likely running at the same moment (the Decky panel issues both together).
// `read`, not `load`: this verb never hands a record's id back, so it has no business
// MINTING one. `load` would mint ids for a pre-mint store and save them, racing the
// `hosts list` a caller is very likely running at the same moment (the Decky panel issues
// both together) — after which the ids one of them already handed out no longer resolve.
let known = KnownHosts::read();
let rows: Vec<(
&pf_client_core::discovery::DiscoveredHost,
Option<&KnownHost>,
)> = found.iter().map(|d| (d, match_saved(&known, d))).collect();
// The one write this verb does make, and why it doesn't contradict the above: an advert
// is the only place a host's wake MAC is ever published, and this verb is the only one
// the Decky panel runs that ever sees one. Without it a Deck in Gaming Mode never learns
// a MAC at all and Wake-on-LAN cannot fire, with nothing to show for it (#322).
// `learn_from_advert` mints nothing either, and writes only when an advert genuinely
// taught the record something new — so a steady-state panel refresh touches no disk.
for (d, saved) in &rows {
if let Some(k) = saved {
trust::learn_from_advert(&k.fp_hex, &k.addr, k.port, &d.mac, &d.os, d.mgmt_port);
}
}
if has(args, "--json") {
let hosts: Vec<serde_json::Value> = rows
.iter()
@@ -733,7 +744,9 @@ from the config directory for a true factory reset."
};
let host = &known.hosts[i];
if host.mac.is_empty() {
eprintln!("no Wake-on-LAN address known for {} — connect to it once while it's awake so the client can learn it", host.name);
// A MAC is learned from the host's mDNS advert, never from a connect — say so, since
// "connect to it once" sent at least one Deck owner looking in the wrong place (#322).
eprintln!("no Wake-on-LAN address known for {} — run `punktfunk discover` while it's awake (the Deck panel does this every time it opens) so the client learns it from the host's advert", host.name);
return UNRESOLVED;
}
if !has(args, "--wait") {
+14 -27
View File
@@ -1087,33 +1087,20 @@ impl HostsPage {
// Online = advertising on mDNS OR proven reachable by the last probe sweep.
let online = self.adverts.values().any(|a| matches(k, a))
|| self.probed.get(&saved_key(k)).copied().unwrap_or(false);
// Learn this host's wake MAC(s) from its live advert while it's online.
if let Some(a) = self
.adverts
.values()
.find(|a| matches(k, a) && !a.mac.is_empty())
{
crate::trust::learn_mac(&k.fp_hex, &k.addr, k.port, &a.mac);
}
// Same for its OS chain — the icon then survives the host going offline.
if let Some(a) = self
.adverts
.values()
.find(|a| matches(k, a) && !a.os.is_empty())
{
crate::trust::learn_os(&k.fp_hex, &k.addr, k.port, &a.os);
}
// Same for its management port — and this one is not cosmetic: without it a host
// that moved off 47990 loses its library the moment mDNS is unavailable, because
// the advert was the only place the real port ever lived.
if let Some(a) = self
.adverts
.values()
.find(|a| matches(k, a) && a.mgmt_port.is_some())
{
if let Some(p) = a.mgmt_port {
crate::trust::learn_mgmt_port(&k.fp_hex, &k.addr, k.port, p);
}
// Learn what this host's live advert teaches while it's online: its wake MAC(s),
// its OS chain (so the icon survives it going offline), and its management port
// — the last one not cosmetic, since a host that moved off 47990 loses its
// library the moment mDNS is unavailable and the advert is the only place the
// real port ever lived.
if let Some(a) = self.adverts.values().find(|a| matches(k, a)) {
crate::trust::learn_from_advert(
&k.fp_hex,
&k.addr,
k.port,
&a.mac,
&a.os,
a.mgmt_port,
);
}
saved.push_back(HostCard {
connecting: self.connecting.as_deref() == Some(k.fp_hex.as_str()),
+14 -4
View File
@@ -800,11 +800,21 @@ impl ServiceState {
|| (d.addr == h.addr && d.port == h.port)
});
let online = advert.is_some() || probed.get(&key).copied().unwrap_or(false);
// Write the advertised mgmt port down while the host is visible, so this console
// keeps working against a moved port once it is not. No-op (and no disk write)
// Write down everything the advert teaches while the host is visible: the mgmt
// port (so this console keeps working against a moved one once it is not), the
// OS chain, and the wake MAC — which matters most here, because this console and
// the Decky panel are the only surfaces a Deck in Gaming Mode ever runs, and a
// record that never learned a MAC can never be woken. No-op (and no disk write)
// when unchanged, so this is safe on every refresh tick.
if let Some(p) = advert.and_then(|d| d.mgmt_port) {
pf_client_core::trust::learn_mgmt_port(&h.fp_hex, &h.addr, h.port, p);
if let Some(a) = advert {
pf_client_core::trust::learn_from_advert(
&h.fp_hex,
&h.addr,
h.port,
&a.mac,
&a.os,
a.mgmt_port,
);
}
let row = HostRow {
key: key.clone(),
+15 -23
View File
@@ -700,31 +700,23 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
.iter()
.any(|h| h.fp_hex == k.fp_hex || (h.addr == k.addr && h.port == k.port))
|| props.probed.get(&k.fp_hex).copied().unwrap_or(false);
// Learn this host's wake MAC(s) from its live advert while it's online, so we can wake
// it once it sleeps (no-op / no disk write when unchanged).
if let Some(a) = hosts.iter().find(|h| {
(h.fp_hex == k.fp_hex || (h.addr == k.addr && h.port == k.port))
&& !h.mac.is_empty()
}) {
crate::trust::learn_mac(&k.fp_hex, &k.addr, k.port, &a.mac);
}
// Same for its OS chain — the tile's mark then survives the host going offline.
if let Some(a) = hosts.iter().find(|h| {
(h.fp_hex == k.fp_hex || (h.addr == k.addr && h.port == k.port)) && !h.os.is_empty()
}) {
crate::trust::learn_os(&k.fp_hex, &k.addr, k.port, &a.os);
}
// Same for its management port — load-bearing, unlike the two above: a host moved off
// 47990 loses its library entirely once mDNS is gone unless we write the port down.
if let Some(p) = hosts
// Learn what this host's live advert teaches while it's online: its wake MAC(s) (so we
// can wake it once it sleeps), its OS chain (so the tile's mark survives it going
// offline), and its management port — the last load-bearing rather than cosmetic, as
// a host moved off 47990 loses its library entirely once mDNS is gone unless we write
// the port down. No-op, and no disk write, when unchanged.
if let Some(a) = hosts
.iter()
.find(|h| {
(h.fp_hex == k.fp_hex || (h.addr == k.addr && h.port == k.port))
&& h.mgmt_port.is_some()
})
.and_then(|h| h.mgmt_port)
.find(|h| h.fp_hex == k.fp_hex || (h.addr == k.addr && h.port == k.port))
{
crate::trust::learn_mgmt_port(&k.fp_hex, &k.addr, k.port, p);
crate::trust::learn_from_advert(
&k.fp_hex,
&k.addr,
k.port,
&a.mac,
&a.os,
a.mgmt_port,
);
}
let can_wake = !online && !k.mac.is_empty();
let menu = {
+1 -1
View File
@@ -29,7 +29,7 @@ pub struct DiscoveredHost {
/// persisted like `mac`. Empty if absent (older host).
pub os: String,
/// The management API's port from the mDNS `mgmt` TXT — where the game library is served.
/// Persisted like `mac` (`trust::learn_mgmt_port`), and load-bearing rather than cosmetic:
/// Persisted like `mac` (`trust::learn_from_advert`), and load-bearing rather than cosmetic:
/// a host moved off 47990 loses its library once mDNS is gone unless we write this down.
/// `None` if absent (older host) — resolve via `library::DEFAULT_MGMT_PORT`.
pub mgmt_port: Option<u16>,
+2 -2
View File
@@ -8,6 +8,6 @@
//! still load via a serde alias in core.
pub use pf_client_core::trust::{
hex, learn_mac, learn_mgmt_port, learn_os, load_or_create_identity, pair_error_message,
parse_hex32, KnownHost, KnownHosts, Settings,
hex, learn_from_advert, load_or_create_identity, pair_error_message, parse_hex32, KnownHost,
KnownHosts, Settings,
};
+82 -54
View File
@@ -675,10 +675,10 @@ pub fn forget_placeholder(addr: &str, port: u16) {
}
}
/// The record [`learn_mac`]/[`learn_os`] should write what an advert taught them onto:
/// the fingerprint match if there is one, else whatever the address resolves to. Fingerprint
/// FIRST — a single pass that took "either" would hand a stale record at the same address the
/// data the live host advertised, purely because it came earlier in the file.
/// The record an advert's lesson should land on: the fingerprint match if there is one, else
/// whatever the address resolves to. Fingerprint FIRST — a single pass that took "either" would
/// hand a stale record at the same address the data the live host advertised, purely because it
/// came earlier in the file.
fn learn_target<'a>(
known: &'a mut KnownHosts,
fp_hex: &str,
@@ -692,61 +692,62 @@ fn learn_target<'a>(
known.hosts.get_mut(i)
}
/// Learn/refresh a saved host's Wake-on-LAN MAC(s) from its live advert (called while the host
/// is online, matched by fingerprint or address). No-op — and no disk write — when unchanged, so
/// the hosts page can call it on every discovery tick without churning the store.
pub fn learn_mac(fp_hex: &str, addr: &str, port: u16, mac: &[String]) {
if mac.is_empty() {
return;
}
let mut known = KnownHosts::load();
let Some(h) = learn_target(&mut known, fp_hex, addr, port) else {
return;
};
if h.mac == mac {
return;
}
h.mac = mac.to_vec();
let _ = known.save();
}
/// Learn/refresh a saved host's OS-identity chain from its live advert (mDNS `os` TXT), matched
/// like [`learn_mac`]: by fingerprint or address. No-op — and no disk write — when unchanged, so
/// the hosts page can call it on every discovery tick without churning the store.
pub fn learn_os(fp_hex: &str, addr: &str, port: u16, os: &str) {
if os.is_empty() {
return;
}
let mut known = KnownHosts::load();
let Some(h) = learn_target(&mut known, fp_hex, addr, port) else {
return;
};
if h.os == os {
return;
}
h.os = os.to_string();
let _ = known.save();
}
/// Learn/refresh a saved host's management-API port from its live advert (mDNS `mgmt` TXT),
/// matched like [`learn_mac`]: by fingerprint or address. No-op — and no disk write — when
/// unchanged, so the hosts page can call it on every discovery tick without churning the store.
/// Copy everything an advert can teach onto a saved record — wake MAC(s), OS-identity chain,
/// management port — and report whether anything actually moved, so the caller writes only when
/// there is something to write. Pure (no disk, no clock), which is what makes it testable.
///
/// This is what makes a moved mgmt port outlive mDNS. Until it existed the port was read straight
/// off the live advert and thrown away, so the library worked on the LAN and went blank over a VPN.
pub fn learn_mgmt_port(fp_hex: &str, addr: &str, port: u16, mgmt_port: u16) {
if mgmt_port == 0 {
return;
/// A field the advert does not carry is left alone, never cleared: an older host simply omits the
/// TXT, and forgetting a MAC already learned would cost the user their wake.
fn apply_advert(h: &mut KnownHost, mac: &[String], os: &str, mgmt_port: Option<u16>) -> bool {
let mut changed = false;
if !mac.is_empty() && h.mac != mac {
h.mac = mac.to_vec();
changed = true;
}
let mut known = KnownHosts::load();
if !os.is_empty() && h.os != os {
h.os = os.to_string();
changed = true;
}
// 0 is how "not advertised" reaches us from a caller whose own type has no `Option`.
if mgmt_port.is_some_and(|p| p != 0 && h.mgmt_port != Some(p)) {
h.mgmt_port = mgmt_port;
changed = true;
}
changed
}
/// Write down everything a live advert teaches the saved record it matched — wake MAC(s), OS
/// chain, management port — matched by fingerprint or address. No-op, and no disk write, when
/// the record already says all three, so a surface can call this on every discovery tick.
///
/// ONE call rather than three. Each field used to be learned by its own function, which meant
/// every front-end had to remember all three, and only the two desktop hosts pages ever did:
/// the console home and the headless CLI learned the management port alone. On a Steam Deck,
/// whose Gaming Mode runs nothing but those two, that left every saved host with no MAC forever
/// — and every wake gate in the codebase reads `!mac.is_empty()` against this record, so
/// Wake-on-LAN there could not fire at all, with no error to show for it (#322).
///
/// [`KnownHosts::read`], not [`KnownHosts::load`]: `punktfunk discover` calls this, and that verb
/// is deliberately not an id-minter (see [`KnownHosts::read`] for the race that avoids). Learning
/// a MAC is no reason to become one.
///
/// Takes the three learned fields rather than a `DiscoveredHost` because there are two of those
/// — core's and the WinUI shell's verbatim port — and this has to serve both.
pub fn learn_from_advert(
fp_hex: &str,
addr: &str,
port: u16,
mac: &[String],
os: &str,
mgmt_port: Option<u16>,
) {
let mut known = KnownHosts::read();
let Some(h) = learn_target(&mut known, fp_hex, addr, port) else {
return;
};
if h.mgmt_port == Some(mgmt_port) {
return;
if apply_advert(h, mac, os, mgmt_port) {
let _ = known.save();
}
h.mgmt_port = Some(mgmt_port);
let _ = known.save();
}
/// Re-key a saved host's address/port after it rediscovered on a new DHCP lease (matched by
@@ -785,7 +786,7 @@ pub fn touch_last_used(fp_hex: &str) {
/// Save a host's management-API port learned from the **session's own `Welcome`**, keyed by
/// fingerprint alone — the identity a just-connected client is certain of.
///
/// This is the mDNS-free path, and the one that matters most: [`learn_mgmt_port`] can only fire
/// This is the mDNS-free path, and the one that matters most: [`learn_from_advert`] can only fire
/// where an advert is visible, whereas this fires on any successful connect, including a host
/// added by IP on a network where discovery has never worked. No-op — and no disk write — when
/// the fingerprint isn't stored or the value is unchanged, so it is safe on every connect.
@@ -2293,6 +2294,33 @@ mod tests {
assert!(learn_target(&mut k, &fp('e'), "10.0.0.9", 9777).is_none());
}
/// What an advert carries lands on the record; what it omits is left alone; and a repeat of
/// the same advert reports no change — which is what lets every surface call this on every
/// discovery tick without churning the store.
#[test]
fn apply_advert_learns_what_it_carries_and_keeps_what_it_omits() {
let mut h = KnownHost::default();
let mac = vec!["aa:bb:cc:dd:ee:ff".to_string()];
assert!(apply_advert(&mut h, &mac, "linux/arch", Some(47991)));
assert_eq!(h.mac, mac);
assert_eq!(h.os, "linux/arch");
assert_eq!(h.mgmt_port, Some(47991));
// The same advert a tick later: nothing moved, so there is nothing to persist.
assert!(!apply_advert(&mut h, &mac, "linux/arch", Some(47991)));
// An older host advertises none of the three. Clearing a learned MAC here is exactly what
// would cost the user their wake, so an absent field must never overwrite a known one.
assert!(!apply_advert(&mut h, &[], "", None));
assert_eq!(h.mac, mac);
assert_eq!(h.os, "linux/arch");
assert_eq!(h.mgmt_port, Some(47991));
// 0 is how "not advertised" reaches us from a consumer that has no Option — not a port.
assert!(!apply_advert(&mut h, &[], "", Some(0)));
assert_eq!(h.mgmt_port, Some(47991));
// A host that genuinely moved: the new value wins.
assert!(apply_advert(&mut h, &[], "", Some(47992)));
assert_eq!(h.mgmt_port, Some(47992));
}
/// Pins render in card order, deduplicated, with deleted profiles simply gone — a pin is
/// presentation state, so a dangling one is never an error surface.
#[test]
+2 -2
View File
@@ -30,13 +30,13 @@ console, and tells you how to pair:
<Installer />
Prefer to read what runs first (it's plain `sh`, ~350 lines):
Prefer to read what runs first (it's plain `sh`, ~450 lines):
<Installer inspect />
It asks before anything optional (Moonlight compat, the shared clipboard, the `punktfunk` group,
starting at boot) and every answer has a default, so `sh install.sh --yes` — or piping it with no
terminal — runs unattended; `--channel canary`, `--mgmt-port`, `--no-start` and the environment
terminal — runs unattended; `--channel canary`, `--mgmt-port`, `--no-start`, `--uninstall` and the environment
twins (`PUNKTFUNK_INSTALL_YES`, `PUNKTFUNK_INSTALL_CHANNEL`, `PUNKTFUNK_INSTALL_GAMESTREAM`,
`PUNKTFUNK_INSTALL_CLIPBOARD`, `PUNKTFUNK_INSTALL_PUNKTFUNK_GROUP`, `PUNKTFUNK_INSTALL_LINGER`,
`PUNKTFUNK_INSTALL_MGMT_PORT`) are listed by `--help`. It covers Ubuntu/Debian, Fedora, Arch-family
+7 -7
View File
@@ -36,15 +36,15 @@ stream on a host this device already trusts.
## Choosing access when you admit a device
Approving and deciding what the device may do are one dialog. Alongside the name:
Approving and deciding what the device may do are one dialog:
- **Access level****Full control**, **Controller only**, or **View only** (an **Advanced**
expander has the individual toggles — [Access levels](/docs/access-levels)).
- **Expires****Forever**, or 1 h / 4 h / 8 h / custom.
![Approve this device: name, access level, expiry, and the one-click Approve as guest](/img/console-approve-device.png)
Defaults are *Full control · Forever* — right for your own new laptop. For a friend's device there
is a one-click **Approve as guest**: Controller only, for 4 hours, then it expires on its own. The
same two controls sit on the **Pair a device** card, and apply to whichever device completes the PIN.
The levels are **Full control**, **Controller only** and **View only** (**Advanced** opens the
individual toggles — [Access levels](/docs/access-levels)); expiry is **Never** or 1 h / 4 h / 8 h /
custom. The defaults are right for your own new laptop; **Approve as guest** is for a friend's
device — Controller only, for 4 hours, then it expires on its own. The same two controls sit on the
**Pair a device** card, and apply to whichever device completes the PIN.
## Managing paired devices
+12 -5
View File
@@ -45,6 +45,8 @@ certificate is the host's own, so your browser warns once — continue) and log
(SteamOS: the install script printed it; it's in `~/.config/punktfunk/web.env`).
- **Windows:** the installer showed it on its last page.
![The console sign-in card: one password field](/img/console-login.png)
Lost it? [Forgot your password](/docs/forgot-password). Everything else about the console:
[The Web Console](/docs/web-console).
@@ -54,11 +56,16 @@ On the device you want to stream *to*, install the app — [Install a Client](/d
has the link for every device (Mac, iPhone/iPad/Apple TV, Linux, Windows, Android, Steam Deck), and
any Moonlight client works too once you [turn GameStream on](/docs/moonlight).
Open the app: your host is already in the list. Select it and **connect**. Back in the console, the
device appears under **Pairing → Waiting for approval** — click **Approve** and it's in, no PIN to
type. (Prefer a PIN? Click **Pair a device** in the console and type the 4-digit code into the
client.) Pairing happens once; the device reconnects on its own from then on.
Details: [Pairing & Trust](/docs/pairing).
Open the app: your host is already in the list.
![The client's host list: saved hosts with their pairing state, and unpaired hosts found on this network](/img/client-hosts.png)
Select it and **connect**, then click **Approve**
next to the device in the console's **Pairing** page — no PIN to type. (Prefer a PIN? **Pair a device**
shows a 4-digit code to type into the client.) Pairing happens once; the device reconnects on its
own from then on. Details: [Pairing & Trust](/docs/pairing).
![The console's Pairing page: two devices waiting for approval, and an armed 4-digit PIN](/img/console-pairing.png)
## 5. Stream
+4
View File
@@ -23,6 +23,10 @@ Jump to what you installed:
## Linux hosts
If you installed with the guided script, `sh install.sh --uninstall` runs this section and the
package removal for your family in one go (fetch it again with `curl -fsSLO https://punktfunk.unom.io/install.sh`);
what it leaves behind is the same list below.
### Stop the services first
The Linux packages ship systemd **user** units, and `systemctl --user enable` writes symlinks into
+4 -3
View File
@@ -112,9 +112,10 @@ appears under **Waiting for approval** instead; approving it pairs it immediatel
Nine destinations in the sidebar (a **More** tab on a phone holds the last five):
- **Dashboard** — live status: whether video and audio are streaming, the active sessions with
their codec, resolution, frame rate and bitrate, which games are running, and how many clients
are paired. Buttons stop a session or ask the encoder for a fresh keyframe.
![Live status during a stream: video and audio streaming, the running game, the session's codec, resolution, frame rate and bitrate](/img/console-live-status.png)
- **Dashboard** — the live status above: what's streaming, which games run, how many clients are
paired. Buttons stop a session or ask the encoder for a fresh keyframe.
- **Host** — this host's identity (hostname, OS, local IP, version, unique id), the codecs it
advertises, its ports, the **Updates** card (see [Updating the Host](/docs/updating)), the
**GPUs** card — Automatic, or prefer one GPU for capture and encode, applied to the next session
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

+2 -1
View File
@@ -19,7 +19,8 @@ release is born complete and the announcement always has something to say.
catches renamed knobs and dead links, not a stale sentence). If an install command, repo URL or
port changed, `data/platforms.json` changed with it — then run `bun run sync-platforms` in
punktfunk-website and commit, because its download page vendors that file and only refreshes
when someone does.
when someone does. Same pass for the website itself: does the landing page still describe what
this release ships (features, platforms, the blog post the CMS expects per release)?
2. **Tag & push.** `git tag -a vX.Y.Z … && git push origin vX.Y.Z` fans out to the build
workflows. Whichever one wins the create race seeds the release body from this file
(`scripts/ci/gitea-release.sh``ensure_release`, and its PowerShell twin). The release page
+34
View File
@@ -28,6 +28,9 @@
# states — every `install` line of an apt/pacman/dnf/sysext host platform must appear in
# the script verbatim (it edits channel/group into the string at run time, never the
# literal), and the script must parse under sh.
# 7. The installer under --dry-run against faked os-release files detects every family it claims
# to (and --uninstall prints each family's removal) — the committed half of the manual
# 16-file matrix PR #345 was verified with. Needs curl on PATH (the script's own prerequisite).
#
# Textual gates, so textual limits: gate 2/3 match token spelling, not env reads — a var name in
# a code comment counts as "exists", and a quoted constant that isn't an env var counts toward
@@ -123,4 +126,35 @@ elif command -v node >/dev/null 2>&1; then
node -e "$installer_check" || fail=1
fi
# ---------------------------------------------------------------- gate 7: installer detection matrix (--dry-run)
# Faked os-release files through the real script, nothing executed: each family must be detected
# and print its own package-manager line, both for the install and for --uninstall; the unsupported
# ones must stop with their pointer. A fix to the installer adds its case here.
osr=$(mktemp -d)
installer_case() { # name os-release-body expected-substring [extra args...]
name=$1; printf '%b' "$2" > "$osr/$name"; want=$3; shift 3
out=$(PUNKTFUNK_INSTALL_OS_RELEASE="$osr/$name" sh scripts/install.sh --dry-run --yes --no-start "$@" 2>&1)
case "$out" in *"$want"*) ;; *)
echo "::error::scripts/install.sh --dry-run $* on a fake $name os-release did not print '$want':"
printf '%s\n' "$out" | sed 's/^/ /'
fail=1 ;;
esac
}
installer_case debian 'ID=debian\nVERSION_ID=13\n' 'sudo apt install -y punktfunk-host'
installer_case ubuntu 'ID=ubuntu\nID_LIKE=debian\nVERSION_ID=26.04\n' 'sudo apt install -y punktfunk-host'
installer_case mint22 'ID=linuxmint\nID_LIKE="ubuntu debian"\nVERSION_ID=22.1\n' 'cannot host'
installer_case fedora 'ID=fedora\nVERSION_ID=44\n' 'sudo dnf install -y punktfunk'
installer_case fedora43 'ID=fedora\nVERSION_ID=43\n' '/rpm/bazzite'
installer_case arch 'ID=arch\n' 'sudo pacman -Syu --noconfirm punktfunk-host'
installer_case cachyos 'ID=cachyos\nID_LIKE="arch"\n' 'sudo pacman -Syu --noconfirm punktfunk-host'
installer_case bazzite 'ID=bazzite\nID_LIKE="fedora"\nVERSION_ID=43\n' 'punktfunk-sysext.sh install'
installer_case nixos 'ID=nixos\n' 'docs/nixos'
installer_case steamos 'ID=steamos\nID_LIKE=arch\n' 'docs/steamos-host'
installer_case gentoo 'ID=gentoo\n' 'build-from-source'
installer_case debian-rm 'ID=debian\nVERSION_ID=13\n' 'sources.list.d/punktfunk.list' --uninstall
installer_case fedora-rm 'ID=fedora\nVERSION_ID=44\n' 'yum.repos.d/punktfunk.repo' --uninstall
installer_case arch-rm 'ID=arch\n' '/etc/pacman.conf' --uninstall
installer_case bazzite-rm 'ID=bazzite\nID_LIKE="fedora"\nVERSION_ID=43\n' 'punktfunk-sysext remove' --uninstall
rm -rf "$osr"
exit "$fail"
+58 -6
View File
@@ -31,6 +31,7 @@ LINGER=${PUNKTFUNK_INSTALL_LINGER:-} # 1/0, empty = ask (default no)
MGMT_PORT=${PUNKTFUNK_INSTALL_MGMT_PORT:-47991} # where the management API moves to on a conflict
START=1
DRY=${PUNKTFUNK_INSTALL_DRY_RUN:-0}
UNINSTALL=0
usage() {
cat <<EOF
@@ -45,6 +46,7 @@ usage: sh install.sh [options]
--linger | --no-linger start the host at boot with nobody logged in (default no)
--mgmt-port N port to move the management API to if Sunshine/Apollo holds 47990 (default $MGMT_PORT)
--no-start install and configure, but don't enable the services
--uninstall stop the services and remove the packages + repo (config stays: $DOCS/uninstall)
--dry-run print every command it would run, change nothing
-h, --help this text
@@ -67,6 +69,7 @@ while [ $# -gt 0 ]; do
--mgmt-port) shift; MGMT_PORT=${1:-} ;;
--mgmt-port=*) MGMT_PORT=${1#*=} ;;
--no-start) START=0 ;;
--uninstall) UNINSTALL=1 ;;
--dry-run) DRY=1 ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown option: $1" >&2; usage >&2; exit 2 ;;
@@ -106,7 +109,10 @@ run() {
cmd=$(printf '%s' "$cmd" | sed \
-e 's/^sudo apt install /sudo apt install -y /' \
-e 's/^sudo dnf install /sudo dnf install -y /' \
-e 's/^sudo pacman -Syu /sudo pacman -Syu --noconfirm /')
-e 's/^sudo pacman -Syu /sudo pacman -Syu --noconfirm /' \
-e 's/^sudo apt purge /sudo apt purge -y /' \
-e 's/^sudo dnf remove /sudo dnf remove -y /' \
-e 's/^sudo pacman -Rns /sudo pacman -Rns --noconfirm /')
fi
printf ' + %s\n' "$cmd"
[ "$DRY" = 1 ] && return 0
@@ -172,6 +178,43 @@ else
fi
say "Detected $PRETTY$FAMILY (guide: $DOCS_PAGE)"
# ---------------------------------------------------------------------------- --uninstall
# The reverse of step 1 + step 6, as $DOCS/uninstall spells it out per family: user units off first
# (package removal can't see the enable symlinks in $HOME), then only the punktfunk packages that
# are actually installed, then the repo. Config, groups and firewall rules stay — the page lists them.
if [ "$UNINSTALL" = 1 ]; then
say "Uninstalling the host ($DOCS/uninstall)"
run 'systemctl --user disable --now punktfunk-host punktfunk-web punktfunk-scripting 2>/dev/null || true'
case "$FAMILY" in
apt)
pkgs=$(dpkg-query -W -f='${Package} ${db:Status-Status}\n' 'punktfunk*' 2>/dev/null | awk '$2=="installed"{printf "%s ", $1}')
[ -n "$pkgs" ] && run "sudo apt purge $pkgs"
run 'sudo rm -f /etc/apt/sources.list.d/punktfunk.list /etc/apt/keyrings/punktfunk.asc'
run 'sudo apt update'
;;
dnf)
pkgs=$(rpm -qa --qf '%{NAME} ' 'punktfunk*' 2>/dev/null)
[ -n "$pkgs" ] && run "sudo dnf remove $pkgs"
run 'sudo rm -f /etc/yum.repos.d/punktfunk.repo'
;;
pacman)
pkgs=$(pacman -Qq 2>/dev/null | grep '^punktfunk' | tr '\n' ' ')
[ -n "$pkgs" ] && run "sudo pacman -Rns $pkgs"
run "sudo sed -i '/^\\[punktfunk\\(-canary\\)\\{0,1\\}\\]\$/,/^Server = /d' /etc/pacman.conf"
;;
sysext)
run 'sudo punktfunk-sysext remove'
;;
esac
cat <<EOF
Removed. Left on purpose: ~/.config/punktfunk (identity, pairings, host.env, plugins — a reinstall
picks them up), the punktfunk / punktfunk-update groups, and any firewall rules you opened.
The one-command cleanups for each are on $DOCS/uninstall#linux-hosts
EOF
exit 0
fi
# Version floors the package can't express: below these the install succeeds and nothing can stream.
major=${VERSION_ID%%.*}
case "$ID" in
@@ -361,11 +404,20 @@ if [ "$START" = 1 ] && [ "$DRY" != 1 ]; then
if command -v ss >/dev/null 2>&1 && ss -lun 2>/dev/null | grep -q ':9777 '; then ok "listening on UDP 9777 (punktfunk/1)"
else warn "nothing on UDP 9777 yet — give it a second, then: journalctl --user -u punktfunk-host -e"; fi
fi
# GPU drivers are the docs pages' job (one step, per distro) — but the one silent failure worth
# calling out: Fedora + NVIDIA with Fedora's own ffmpeg has no NVENC, and the RPM only Recommends
# RPM Fusion's build, so the install succeeded and encoding won't.
if [ "$FAMILY" = dnf ] && grep -qs 0x10de /sys/bus/pci/devices/*/vendor 2>/dev/null && ! rpm -q ffmpeg-libs >/dev/null 2>&1; then
warn "NVIDIA GPU, but RPM Fusion's ffmpeg-libs isn't installed — NVENC won't work until it is: step 1 of $DOCS_PAGE"
# GPU drivers are the docs pages' job (one step, per distro) — but the silent failures worth
# calling out, because the install succeeded and streaming won't: an NVIDIA card whose kernel
# module didn't load (Secure Boot blocks the unenrolled key — nvidia-smi can't talk to it), or no
# driver at all; and Fedora + NVIDIA with Fedora's own ffmpeg, which has no NVENC (the RPM only
# Recommends RPM Fusion's build).
if grep -qs 0x10de /sys/bus/pci/devices/*/vendor 2>/dev/null; then
if ! command -v nvidia-smi >/dev/null 2>&1; then
warn "NVIDIA GPU without the NVIDIA driver — nothing can encode until it's installed: step 1 of $DOCS_PAGE"
elif ! nvidia-smi >/dev/null 2>&1; then
warn "NVIDIA GPU, but nvidia-smi can't talk to the driver — the kernel module didn't load (Secure Boot? run: mokutil --sb-state): $DOCS/troubleshooting#nvidia-smi-says-it-cant-communicate-with-the-driver"
fi
if [ "$FAMILY" = dnf ] && ! rpm -q ffmpeg-libs >/dev/null 2>&1; then
warn "NVIDIA GPU, but RPM Fusion's ffmpeg-libs isn't installed — NVENC won't work until it is: step 1 of $DOCS_PAGE"
fi
fi
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
[ -n "$ip" ] || ip=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1); exit}')
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@punktfunk/host",
"version": "0.1.4",
"version": "0.1.5",
"description": "TypeScript SDK for the punktfunk streaming host: typed management-API client + lifecycle event stream, built on Effect.",
"type": "module",
"license": "MIT OR Apache-2.0",
+1 -1
View File
@@ -8,4 +8,4 @@
*
* `version.test.ts` fails if this and `package.json` disagree, so the duplication cannot rot.
*/
export const SDK_VERSION = "0.1.4";
export const SDK_VERSION = "0.1.5";