Commit Graph
2135 Commits
Author SHA1 Message Date
enricobuehlerandClaude Fable 5 8e396b8391 feat(cli): every command explains itself
`punktfunk help` was one usage blob, and `punktfunk pair --help` was worse than
nothing: the flag fell through to the verb, which read "--help" as its subject,
printed a terse usage line to stderr and exited 5. For the command that is about to
be the documented door for scripts and plugins (Playnite shells to `library --json`),
"self-documenting" has to actually hold.

Now `punktfunk help <command>` — and `--help`/`-h` after any verb, caught BEFORE
dispatch so no verb can mistake the flag for its subject — prints that command's own
page: flags, what lands on stdout vs stderr, and which exit code means what, which is
the part a script author actually needs. Help goes to stdout and exits 0; an unknown
topic refuses with 5. A unit test walks USAGE and asserts every advertised verb has a
help page that leads with its own invocation, so the overview and the pages cannot
drift apart, and an integration test runs the REAL binary over both spellings.

`reachable` also stops scolding: probing an unsaved address is that verb's documented
use, but it resolved through the saved-host path first, whose "pair it first" advice
printed before the probe ran. It resolves quietly now — same lookup, no lecture.

Verified on the Windows CI runner (clippy -D warnings, fmt, 8/8 tests, and the built
punktfunk.exe by hand: overview, per-verb pages, quiet `reachable` exit 2) and in the
Linux CI image (same gates, 8/8). One field note from the hand run: the exe needs the
FFmpeg DLLs beside it or on PATH — true of the session binary already, and the MSIX
ships them next to both, so packaging is unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 10:58:44 +02:00
enricobuehlerandClaude Opus 5 ff01db67ff fix(client): punktfunk-session is the session binary again — 0.22.0 shipped the shell's stub
Connecting from the 0.22.0 Windows client bounces straight back to the host list, on
every host. The shell is fine; the binary it spawns is not.

b0ea1e6b was a `clients/linux` change that also dropped a verbatim copy of the GTK
shell into `clients/session/src/` — app.rs, cli.rs, the four ui_*.rs, shortcuts.rs,
spawn.rs — and, fatally, OVERWROTE `clients/session/src/main.rs` with the shell's.
That file is `[[bin]] punktfunk-session`. So the binary every shell execs for a stream
stopped being the Vulkan session and became the GTK shell:

  - Windows: the shell's `#[cfg(not(target_os = "linux"))]` arm — print
    "punktfunk-client is Linux-only" to stderr, `exit(2)`. It never writes a single
    line of the stdout contract, so the shell sees EOF with no `ready`, no `error`,
    no `ended`, and returns to the host list with a BLANK banner. Exactly the report.
  - Linux: `app::run()` sees `--connect` in argv and calls `exec_session()`, which
    execs `punktfunk-session` — itself. A stream is an exec loop.

CI could not catch it. The Windows leg of the clobbered file is a three-line stub that
compiles perfectly; `cargo build -p punktfunk-client-session` stayed green while
building the wrong program. Only running it fails, and nothing runs it.

61bdf11e then read the 327 E0433s on the Linux leg as a missing-manifest bug and
declared gtk4/libadwaita/relm4 on this crate. That fixed the build of the wrong file
and cemented the clobber. Both go: the copy is deleted, `main.rs` is restored from
bf981027 (the last commit that touched the real one), and the manifest loses the GTK
block plus the gresource build-dep and `data/` that arrived with 944c03dd to feed it.
serde_json returns to `optional`/`ui`, which is what it always was — the reason
`--no-default-features` didn't compile was cli.rs, and cli.rs was never ours.

Also closes the hole that made this silent. `SpawnEvent::Exited` now carries the
child's exit code, and a child that exits nonzero having said NOTHING gets a banner
naming that code instead of an empty string. Code 0 (stream window closed) and -1
(our own Disconnect/Cancel kill) stay silent, as before. A wrong or crashing session
binary is now a legible failure rather than a connect that quietly does nothing.

Verified. Windows x86_64 on the CI runner: check, clippy -D warnings, rustfmt, tests
(5 passed, incl. the new one) all green, and the rebuilt punktfunk-session.exe answers
`--connect` with `{"error":"presenter: SDL window: …"}` — the real contract — where
0.22.0's answered with nothing. Linux amd64 in the CI image: check with default AND
--no-default-features, clippy -D warnings, rustfmt, tests green, and the built binary
emits `{"error":"presenter: SDL video: …"}` + exit 4 instead of exec-looping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 10:58:44 +02:00
enricobuehlerandClaude Fable 5 5c0cb84fda fix(ci/windows-host): drop the drivers-target cache — act rotates the workspace path
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 12s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 14s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 16s
windows-host / package (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 13s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 21s
ci / rust-arm64 (push) Successful in 1m34s
ci / web (push) Successful in 1m45s
docker / deploy-docs (push) Successful in 37s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 1m29s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 1m30s
ci / docs-site (push) Successful in 1m57s
docker / builders-arm64cross (push) Successful in 9s
ci / rust (push) Successful in 3m51s
I added it yesterday on the theory that cargo's fingerprints would sort out
a restored in-tree target/. They can't: act gives each run a different
absolute workspace (~/.cache/act/<hash>/hostexecutor), and a target dir
restored under a new path carries state pointing at the old one. Measured
today — pf-umdf-util died with 14 x 'unable to create file lock (os error
3)' and failed the job. ~1 min of rebuild is the right price, and it's the
same rotation that made the other Windows jobs use a fixed C:\t.

The web console cache stays: it restored cleanly and skipped its ~2.5 min
build+smoke, taking the job 13.6 -> 8.5 min in the same measurement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 10:46:47 +02:00
enricobuehlerandClaude Fable 5 ba3add85b5 fix(ci/windows): install zstd, or every cache save dies on a missing gzip
apple / swift (push) Successful in 1m19s
ci / docs-site (push) Successful in 1m16s
ci / rust (push) Successful in 4m35s
android / android (push) Successful in 5m11s
ci / web (push) Successful in 5m24s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 11s
deb / build-publish-host (push) Successful in 4m26s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
deb / build-publish-client-arm64 (push) Successful in 1m20s
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 9s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m11s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m34s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 2m35s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 11s
decky / build-publish (push) Successful in 59s
ci / rust-arm64 (push) Successful in 8m59s
docker / deploy-docs (push) Failing after 21s
docker / builders-arm64cross (push) Successful in 1m55s
apple / screenshots (push) Successful in 10m59s
arch / build-publish (push) Successful in 12m32s
deb / build-publish (push) Successful in 14m53s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m10s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m53s
The new windows-host cache steps reported '::warning::Failed to save' and
nothing ever seeded. actions/cache probes for zstd, doesn't find it, falls
back to gzip — and Git's GNU tar then shells out to a gzip that is not on
the runner daemon's PATH: 'Child returned status 127', 'cache.tgz: Cannot
write: Broken pipe', tar exit 2. Since save failures are warnings, the job
stayed green while caching silently did nothing.

zstd (+ a staged gzip.exe as insurance) now installs into its own directory
— never Git's usr\bin on PATH, which would shadow Windows' find/sort/echo.
Applied live to the runner and added to the machine PATH; this step keeps a
rebuilt runner honest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 10:16:43 +02:00
enricobuehlerandClaude Opus 5 b2cf4e908c chore(release): bump workspace version to 0.22.0
audit / bun-audit (sdk) (push) Successful in 17s
audit / bun-audit (web) (push) Successful in 17s
audit / docs-site-audit (push) Successful in 17s
audit / pnpm-audit (push) Successful in 11s
audit / bun-audit (plugin-kit) (push) Successful in 3m15s
audit / cargo-audit (push) Successful in 3m24s
android-screenshots / screenshots (push) Successful in 3m40s
apple / swift (push) Successful in 5m0s
audit / license-gate (push) Successful in 7m24s
decky / build-publish (push) Successful in 37s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m17s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m3s
arch / build-publish (push) Successful in 19m5s
linux-client-screenshots / screenshots (push) Successful in 10m39s
sbom / sbom (push) Successful in 1m16s
docker / deploy-docs (push) Successful in 11s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 24s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 24s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 8s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 19s
docker / builders-arm64cross (push) Successful in 5s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 23s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m23s
windows-host / package (push) Successful in 14m34s
windows-host / winget-source (push) Successful in 17s
android / android (push) Successful in 10m16s
web-screenshots / screenshots (push) Successful in 10m52s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m56s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m36s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m51s
deb / build-publish (push) Successful in 13m51s
deb / build-publish-host (push) Successful in 5m27s
deb / build-publish-client-arm64 (push) Successful in 4m9s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 27m37s
release / apple (push) Successful in 28m45s
apple / screenshots (push) Successful in 21m19s
ci / docs-site (push) Successful in 1m4s
ci / web (push) Successful in 2m46s
ci / rust-arm64 (push) Successful in 3m41s
ci / rust (push) Successful in 3m54s
flatpak / build-publish (push) Successful in 4m24s
256 commits since v0.21.0. Minor, not patch: the headline is settings profiles —
named bundles of overrides bound per host, landing on all five clients at once
(Linux, Windows, Apple, Android) with one settings surface editing either layer,
marked-and-resettable override rows, per-profile colours, host bindings, one-off
"Connect with", and pinned host+profile cards. With them: the punktfunk:// link
grammar (one parser, one 44-case vector file run by the Rust, Swift and Kotlin
suites), double-clickable shortcuts, and `punktfunk` — one headless front-end
over the brain layer, which wakes a sleeping host the way a card click does.

Also: opt-in HDR on the gamescope path plus the cursor-in-the-node patch that
makes those sessions zero-copy; Vulkan Video 10-bit so AMD/Intel HDR keeps the
good path; a zero-copy NVENC HDR leg; host OS detection with marks on every
client and the console; PUNKTFUNK_HOST_NAME, PUNKTFUNK_MAX_FPS and
PUNKTFUNK_VDISPLAY_HZ_MULT; monitor enumeration on Windows; and the release-
integrity work (per-asset SHA256 sidecars, a signed sysext feed, one stable
driver publisher identity, fail-closed signing guards on a v* tag).

Wire protocol stays at 2, the embeddable C ABI at 13 and the Windows virtual-
display driver protocol at 6 — 0.18-0.22 hosts and clients keep mixing freely.
The Windows virtual-GAMEPAD channel protocol goes 2 -> 3 and fails closed both
ways: it carries the fix for a LocalService principal being able to take over a
pad's shared input section and forge HID input into the interactive desktop, so
the host and its drivers must ship together. The installer ships both.

Additive elsewhere: an advisory mDNS `os=` TXT key, HostInfo.os/os_name,
MonitorsResponse.pin_supported, an eighth field on the Android JNI discovery
record, and appended-last StoredHost fields per the frozen app-widget contract.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v0.22.0
2026-07-30 01:39:58 +02:00
enricobuehlerandClaude Opus 5 dee9ecf5c5 docs(release): v0.21.0's notes describe what v0.21.0 actually shipped
The file had accumulated the gamescope-HDR work as it landed — one "New: HDR on
the gamescope path" section plus nine Under-the-hood bullets (the gamescope
patches, Vulkan 10-bit, the per-codec probe, the NVENC HDR leg, the EFC BT.2020
model, the managed-spawn flag check, the CI wiring). All of it landed AFTER
d0889338 was tagged: `PUNKTFUNK_GAMESCOPE_HDR` does not exist at v0.21.0 and
neither does packaging/gamescope/, verified by content rather than by SHA.

The live release body never carried any of it — it was PATCHed from the trimmed
file at c4e80fd4 and not re-synced since — so this only restores the file to the
7030 bytes users actually see on the release page. That matters because
announce.yml's apply_release_notes re-asserts this file over the live body: a
re-announce or a tag re-point would have rewritten a shipped release to
advertise features it does not contain.

The content itself is not lost; it is where it belongs, in v0.22.0.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 01:39:25 +02:00
enricobuehlerandClaude Fable 5 2b82ce6484 fix(ci/flatpak): host networking — ostree's resolver never worked through docker's embedded DNS
ci / rust-arm64 (push) Successful in 1m45s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 7s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 6s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 6s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 13s
ci / web (push) Successful in 4m2s
ci / docs-site (push) Successful in 4m7s
docker / builders-arm64cross (push) Successful in 15s
docker / deploy-docs (push) Successful in 35s
ci / rust (push) Successful in 7m10s
flatpak / build-publish (push) Successful in 7m23s
The flathub fetch has failed 10/10 retries for months, blamed on fleet load
and DNS tuning. It is neither. Measured on home-runner-2, all inside ONE
container: getent resolved dl.flathub.org, curl fetched the same URL with
HTTP 200 (auto and -4), and flatpak still died '[6] Could not resolve
hostname'. Rewriting resolv.conf to a real nameserver didn't help; the
default bridge failed too; --network host works every time. So it is
ostree's own resolver against Docker's embedded 127.0.0.11, and removing
that resolver from the path is the fix. retry.sh stays as the backstop for
genuine upstream blips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 01:29:13 +02:00
enricobuehlerandClaude Opus 5 53ff313046 fix(packaging/gamescope): the shipped compositor starts on SteamOS — a rolling-distro libstdc++ never followed it there
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 14s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 11s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 10s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
ci / web (push) Successful in 52s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 1m6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 1m8s
ci / docs-site (push) Successful in 2m13s
docker / builders-arm64cross (push) Successful in 10s
docker / deploy-docs (push) Successful in 40s
ci / rust (push) Canceled after 5m49s
ci / rust-arm64 (push) Canceled after 5m47s
arch / build-publish (push) Successful in 15m59s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 16m7s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 15m46s
`punktfunk-gamescope-3.16.25.pfhdr2-1` off the pacman repo cannot start on SteamOS 3.8.16:
`/usr/lib/libstdc++.so.6: version 'GLIBCXX_3.4.35' not found`. The Arch container CI builds it in
is on gcc 16.1.1; SteamOS ships libstdc++ 3.4.34 and moves when Valve says so. Nothing else about
the binary was wrong — every other soname resolved on the box, and glibc was never close (it asks
for 2.38 at most against SteamOS's 2.41) — so the one dynamic C++ runtime was the whole reason the
gamescope backend's own most important platform got a package that dies at `--version`.

The C++ runtime therefore goes static, for the same reason wlroots already does: this binary is
built on a rolling distro and has to start on a frozen one. It is safe here because gamescope
links no shared C++ library at all — its NEEDED list is all C, and glslang/SPIRV are build-time
only — so no C++ ABI crosses a shared boundary. Cost is ~1 MB (5.9 → 7.1). The flags are appended
to LDFLAGS rather than passed as `-Dcpp_link_args`, which would replace the value meson derives
from the environment and silently drop makepkg's `-z relro`/`-z now`/`--as-needed`.

A static runtime is invisible in a passing build and only surfaces as a binary that will not start
somewhere else, so the build now asserts it: no `libstdc++` in NEEDED, which needs no version
threshold to check and turns the regression back into a build failure.

Verified by building in `archlinux:base-devel` — the environment arch.yml uses, gcc 16.1.1 and
glibc 2.44, both far newer than the target — and running the result on SteamOS 3.8.16 with nothing
supplied: banner `punktfunk-gamescope version 3.16.25-4-g6bbe157+pfhdr2`, no libstdc++ in NEEDED,
max GLIBC_2.38, every soname resolving. The host's whole HDR gate chain then answers on SteamOS
for the first time (780M / RADV PHOENIX): 10-bit PQ capture offered, cursor painted in-node,
native-plane HDR and GameStream HDR capable both true once `PUNKTFUNK_GAMESCOPE_HDR` is on, false
with the distro's gamescope.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 01:23:35 +02:00
enricobuehlerandClaude Opus 5 31db452ca9 feat(client/apple): the host tile wears its OS mark where the initial was
apple / swift (push) Successful in 4m32s
ci / web (push) Successful in 1m5s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 7s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 6s
ci / docs-site (push) Successful in 1m18s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 8s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 7s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 18s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 21s
docker / builders-arm64cross (push) Successful in 13s
docker / deploy-docs (push) Successful in 40s
ci / rust (push) Failing after 6m10s
ci / rust-arm64 (push) Successful in 14m47s
release / apple (push) Successful in 25m17s
apple / screenshots (push) Canceled after 13m52s
Apple parity with the Android card (a94b1d3c's sibling): the OS mark moves
out of the status row and into the tile, replacing the monogram. The
initial says nothing the name beside it doesn't already say — twice over on
a row of home-worker-N boxes — while the mark identifies the machine at a
glance.

Both host surfaces, because on tvOS the console home is the only one there
is: the touch cards (saved and discovered) and GamepadHomeView's badge,
which needed the chain carried on HomeTile to reach it. Sized to the
monogram's own point size, so it lands at ~48% of the tile everywhere, and
tinted through the same foregroundStyle the letter used — the assets are
template imagesets, so they follow it like an SF Symbol.

A host that advertises no OS chain, or one we ship no art for, keeps its
letter: `osIconImage` already returns nil for both, so a mixed row still
reads as one set. The mark carries the accessibility label now, since the
status row it used to ride no longer names the OS.

Untouched: the widget draws no monogram, and AboutView's is the app's own
logo, not a host's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:53:46 +02:00
enricobuehlerandClaude Fable 5 af817bc03e fix(lock): cursor-probe's pf-frame dep reaches the lockfile
audit / bun-audit (web) (push) Successful in 14s
audit / docs-site-audit (push) Successful in 13s
audit / bun-audit (sdk) (push) Successful in 25s
audit / bun-audit (plugin-kit) (push) Successful in 27s
audit / pnpm-audit (push) Successful in 16s
ci / web (push) Successful in 54s
apple / swift (push) Canceled after 2m17s
apple / screenshots (push) Canceled after 0s
ci / rust (push) Canceled after 1m49s
ci / rust-arm64 (push) Canceled after 1m49s
ci / docs-site (push) Canceled after 47s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 0s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 0s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-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 / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
release / apple (push) Canceled after 0s
audit / cargo-audit (push) Successful in 2m46s
audit / license-gate (push) Successful in 6m35s
deb / build-publish-client-arm64 (push) Successful in 8m10s
flatpak / build-publish (push) Failing after 9m0s
deb / build-publish (push) Successful in 9m47s
android / android (push) Successful in 12m25s
windows-host / package (push) Successful in 16m43s
windows-host / winget-source (push) Skipped
arch / build-publish (push) Canceled after 52s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m44s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m26s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 5m24s
deb / build-publish-host (push) Successful in 23m59s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 21m51s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m22s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 3m50s
1f59498c added pf-frame to tools/cursor-probe (a workspace member) without
the Cargo.lock entry. The pinned 1.96 toolchain shrugs, but Arch's cargo
1.97 refuses to resolve under --locked — every arch build died with
'cannot update the lock file' right after the windows-rs fetch, which made
it look like a cache problem. One line, written by the pinned toolchain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 00:53:18 +02:00
enricobuehlerandClaude Opus 5 a94b1d3ccc fix(client/android): the stream keeps its aspect instead of stretching to the panel
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 7s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 8s
ci / web (push) Successful in 58s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 49s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 1m6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 22s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
ci / docs-site (push) Successful in 1m37s
docker / builders-arm64cross (push) Successful in 14s
docker / deploy-docs (push) Successful in 31s
ci / rust-arm64 (push) Failing after 4m29s
android / android (push) Canceled after 4m35s
ci / rust (push) Canceled after 4m39s
MediaCodec scales whatever it decodes to fill the Surface it renders into,
and the Surface filled the screen — so a stream whose resolution didn't
match the panel's aspect came out stretched. Nothing downstream of the
Surface can correct that; the Surface itself has to carry the aspect.

Size the video to the negotiated mode's ratio, centred, with the remainder
black. The mode is known from the handshake before the first frame arrives,
via a new `nativeVideoSize` (the same `client.mode()` the HUD already
reports as `w×h@hz`); an older native lib returning nothing falls back to
filling, exactly as before.

Input follows the picture. Direct-pointer touch, multi-touch passthrough
and the pen lane all map positions against the size of the node they sit
on, so the gesture layer moves onto the same rect as the video and all
three stay correct by construction instead of each needing an offset
threaded through it. The physical-mouse path can't work that way — its
events arrive from the activity in WINDOW coordinates — so it now measures
against the SurfaceView's rect on screen, subtracting the letterbox origin
and clamping into the picture: a pointer out on a bar has no host position
of its own, and the edge is the honest answer for it.

One deliberate consequence: trackpad swipes that START inside a letterbox
bar no longer register. Trackpad input is relative and could have kept the
whole panel, but one rule — input lands on the picture — beats a mode-
dependent input surface, and the pen lane rides inside trackpad mode too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:48:38 +02:00
enricobuehlerandClaude Fable 5 1f59498c5c fix(host/linux): a no-channel session composites the metadata cursor — Mutter never embeds on a virtual stream
ci / web (push) Successful in 3m26s
ci / docs-site (push) Successful in 3m32s
arch / build-publish (push) Failing after 4m29s
apple / swift (push) Successful in 4m38s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 5s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 5s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 4s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
ci / rust-arm64 (push) Failing after 4m50s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 12s
deb / build-publish (push) Failing after 5m6s
ci / rust (push) Failing after 5m47s
docker / builders-arm64cross (push) Successful in 6s
docker / deploy-docs (push) Successful in 34s
android / android (push) Canceled after 8m16s
apple / screenshots (push) Canceled after 3m46s
deb / build-publish-host (push) Canceled after 4m15s
deb / build-publish-client-arm64 (push) Canceled after 4m7s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 3m6s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 2m58s
windows-host / winget-source (push) Canceled after 0s
windows-host / package (push) Canceled after 8m21s
The capture-latched client (console.rs latched_mouse) never advertises
CLIENT_CAP_CURSOR, so its session resolved cursor_blend=false and asked
Mutter to EMBED the pointer. On a Mutter virtual stream that is a
fiction: since Mutter 48 (7ff5334a, hw-cursor inhibition removed) the
software cursor overlay is suppressed stage-globally whenever any
physical head realizes a HW cursor — dmabuf-recorded frames blit the
view without a pointer, and cursor-only motion schedules no re-record
either (mutter#4939). Probed on-glass on Mutter 50.3: embedded +
relative motion = frozen frame counter; SPA_META_Cursor positions kept
flowing in the same setup.

So the no-channel session now takes the path that was verified end to
end: cursor-as-metadata + the host composites, permanently — the same
arm a channel session lands in after its capture-model flip. Embedded
remains only the can't-blend fallback (libav VAAPI/NVENC, software).

- session_plan::cursor_blend_for grows the no-channel arm (codec +
  depth in, the same CUDA-payload prediction handshake makes);
  gamescope excluded so patch-2+ keeps its native-NV12 zero-copy shape
- the encode loop's composite refresh + one-shot breadcrumbs now cover
  the no-channel session; the park schedule keeps retrying while its
  composite is starved (relative-only clients cannot park themselves)
- the compositor retarget re-applies set_hw_cursor — the rebuilt
  display used to come up EMBEDDED even for channel sessions
- the GameStream virtual source takes the same rule (it never has a
  channel); its stream_body blend flag mirrors the request
- punktfunk-probe grows --cursor-nochannel (the latched-capture client,
  headless); cursor-probe grows --dump (PPM frames + a content-change
  counter, the pixel evidence the embedded A/B lacked)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 00:35:25 +02:00
enricobuehlerandClaude Opus 5 9ed967cbaf feat(client/android): the host card wears its OS mark where the initial was
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 13s
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 10s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 1m7s
android / android (push) Canceled after 2m56s
ci / rust (push) Canceled after 2m37s
ci / rust-arm64 (push) Canceled after 2m25s
ci / web (push) Canceled after 2m11s
ci / docs-site (push) Canceled after 2m11s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 18s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 1s
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
The OS mark rode along at 12 dp in front of the address, competing with the
text it prefixed. The avatar circle above it was showing the host's first
letter — which says nothing the name underneath doesn't already say, twice
over on a row of home-worker-N boxes.

Put the mark in the circle instead, at 24 dp in the avatar's own
onPrimaryContainer tint, and drop it from the address line. A host that
advertises no OS chain — or one we ship no mark for — keeps the initial, so
those cards look exactly as they did and a mixed row still reads as one set.

On glass: the two Arch hosts wear the Arch mark; steamdeck and the Windows
runner, both predating the `os=` TXT, keep their letters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:32:22 +02:00
enricobuehlerandClaude Fable 5 be2fabcfba feat(ci/windows-host): the console and the drivers stop rebuilding what nobody changed
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 9s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 11s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 12s
android / android (push) Canceled after 1m24s
ci / rust (push) Canceled after 1m25s
ci / rust-arm64 (push) Canceled after 1m19s
ci / web (push) Canceled after 1m19s
ci / docs-site (push) Canceled after 1m18s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 44s
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
arch / build-publish (push) Canceled after 4m30s
windows-host / package (push) Canceled after 4m34s
windows-host / winget-source (push) Canceled after 0s
The job's two cache-shaped tails, now actually cached (the runner just got
wired to the central cache server — it had none): web/.output restores and
skips the ~2.5 min bun build+smoke whenever web/ and sdk/ are untouched,
and the UMDF drivers' in-tree target/ (which checkout's clean wiped every
run because wdk-build can't relocate it) restores so cargo's fingerprints
declare it fresh. Typical run: ~13.6 min -> ~10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 00:30:59 +02:00
enricobuehlerandClaude Opus 5 6a6be17ce7 fix(client/android): a session that receives no video at all now asks, and says so
The keyframe backstop added for the black LG TV only arms while AUs are
actually going into the decoder (`fed > fed_at_output`) — deliberately, so
an idle stream never asks for anything. That leaves its mirror image
uncovered: a session that receives NOTHING. A decoder cannot be starved of
output when it was handed no input, so no signal in either loop fires, and
the session sits connected — audio, input and the control plane all alive —
behind a black surface.

That state is what a user just reported as "the stats are all basically 0":
fps and Mb/s are counted at AU receipt (`note_received`), so all-zero stats
with a drawn overlay means the decode thread started and received nothing.
Same bug as the black screen, seen from the HUD.

Both loops now watch for it: nothing received 1.5 s into a session ⇒ request
a keyframe and log it, re-asking every 2 s while it lasts. Where it can help
it does — the host encoding fine while every picture references an IDR this
client never saw is precisely a keyframe request away. Where it can't, the
log line is the point: "no video received N ms into the session" separates
"the host never sent a picture" from "we received AUs and lost them", which
no previous black-screen report could distinguish.

Not a root cause. The remaining occurrences are still unattributed — this
makes the next report diagnosable and recovers the case that is ours to
recover.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:30:59 +02:00
enricobuehlerandClaude Opus 5 bb7baef20b fix(client/android): the menus keep their safe area after a stream
Returning from a stream left every menu laid out against the wrong safe
area: content shoved right, the profile row sliding under the status bar,
the tab labels crowding the gesture pill. Dumped on the reporter's phone,
the window's real insets were bars=[0,162,0,72] cutout=[0,162,0,0] while
the layout was using the landscape immersive set — cutout left=162
(Material3 lays out against systemBars.union(displayCutout)), bars all
zero. No rotation and no IME animation could shake it loose.

Compose attaches its OnApplyWindowInsets and WindowInsetsAnimation
callbacks when the first composable reads an inset and removes them when
the last reader goes away (WindowInsetsHolder.increment /
decrementAccessors). StreamScreen reads no insets at all, so a stream
drops that count to zero for its whole duration.

Survivable on its own — but a session that ends while the app is
BACKGROUNDED is the common case (leaving the app ends the session), and
then the entire window restore runs on a stopped activity. The corrected
insets arrive while Compose has no listener attached; when the menus
recompose, incrementAccessors re-attaches and asks for a fresh pass, but a
stopped window produces no dispatch and on resume nothing has changed any
more, so none ever comes. Compose keeps serving the landscape,
bars-hidden values for the rest of the process.

Hold one inset reader at the root for the activity's whole life, so the
listeners survive the stream and every dispatch lands. It subscribes to no
inset VALUE, only the holder object, so it costs one DisposableEffect and
no recomposition.

Verified on the reporter's device by replaying the real teardown sequence
(forced landscape + immersive, composition swapped to a screen that reads
no insets, torn down while stopped) and reading the layout back through
uiautomator: 3 of 4 runs wrong without this, 4 of 4 correct with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:30:59 +02:00
enricobuehlerandClaude Opus 5 91def82219 fix(client/android): the menus keep their safe area after a stream
Returning from a stream left every menu laid out against the wrong safe
area: content shoved right by the landscape side inset, the profile row
sliding under the status bar, the tab labels crowding the gesture pill.

Compose attaches its OnApplyWindowInsets AND WindowInsetsAnimation
callbacks when the first composable reads an inset, and tears both down
when the last reader goes away (WindowInsetsHolder.increment /
decrementAccessors). The immersive stream reads no insets at all — it is
a bare full-screen surface — so entering one dropped the reader count to
zero right in the middle of the hide(systemBars()) animation StreamScreen
had just started. With the animation callback gone, that animation's
onEnd never arrived, so the listener kept runningAnimation = true for the
rest of the process, and from then on every onApplyWindowInsets was
swallowed (it defers to an onProgress that can no longer come). The
values froze at the last animation frame — landscape, bars hidden — and
that is what the menus got when they came back.

Hold one inset reader at the root for the activity's whole life: the
listeners now survive the stream, the landscape lock and the bar
animations, and every animation gets its onEnd. It subscribes to no inset
VALUE, so it costs nothing per frame. As a backstop, request a fresh
insets pass from onConfigurationChanged — the activity declares
configChanges=orientation|screenSize, so a rotation re-lays out in place
and a dropped dispatch would otherwise go unnoticed until the layout is
already wrong on screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:30:59 +02:00
enricobuehlerandClaude Opus 5 1a18ae1fae fix(packaging/bazzite): the feed publisher signed a redirect page, not the manifest
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 17s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 8s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 8s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
docker / builders-arm64cross (push) Successful in 4s
docker / deploy-docs (push) Successful in 25s
ci / web (push) Successful in 2m46s
ci / docs-site (push) Successful in 2m45s
ci / rust-arm64 (push) Successful in 11m54s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m4s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m12s
ci / rust (push) Canceled after 18m14s
Every Bazzite install on the stable channel has been refusing the feed:

  !! the feed's SHA256SUMS is NOT signed by packages@unom.io (AF245C506F4E4763).

The client was right and the feed was wrong. The registry answers a file GET
with a 303 See Other pointing at presigned object storage, and `curl -f` does
not treat a 3xx as an error — so the publisher's two un-`-L`'d manifest reads
"succeeded" holding the redirect's HTML body, `<a href="…">See Other</a>.`, and
handed that to callers as the manifest.

That broke both of them:

  * --seal signed the HTML page. Its presigned URL is regenerated per request
    and expires 300s later, so the published .asc covers bytes that exist
    nowhere and can never verify. Stable feeds only publish on a tag, so they
    are only ever sealed — f43 and f44 were re-broken at 19:54Z on 2026-07-29
    by the seal step of a canary run, and every canary push re-broke them.
    f43-canary survived by accident: on the publish path the same polluted
    bytes get both signed and uploaded, so it is at least self-consistent.

  * the publish merge read took the page for the previous manifest, and
    `grep -v " $FNAME$"` kept it — so each publish prepended a stale redirect
    page and dropped every prior image line. f43 runs KEEP=0 (keep all) and
    holds exactly one line, with 0.20.0 and 0.19.2 still in the registry and
    no longer listed. This has been quietly eating feed history since long
    before signing existed; nobody noticed because the client's latest() only
    matches ^punktfunk-.*-x86-64\.raw$, so an HTML line is invisible to it.

Both reads now go through one read_manifest that follows redirects AND keeps
only well-formed "<sha256>  <filename>" lines, so a manifest is never again
whatever the transport happened to return. --seal re-publishes a manifest that
normalizing changed, since the signature has to cover the bytes a client
downloads — that is what repairs the live feeds — and refuses outright when a
manifest lists no images, rather than sealing an empty feed that would read as
"up to date" to `punktfunk-sysext update`. The publish path now logs its
carry-over count; silence there is what let the history loss run for months.

Verified end to end against a fake registry that 303s to a fresh URL per
request, with a throwaway key and the real scripts: HEAD reproduces the
injected page, the lost image line and the client's VERIFY-FAIL; the fix keeps
both images and verifies; and the new --seal over the broken state normalizes
the manifest and turns it back to VERIFY-OK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:09:23 +02:00
enricobuehlerandClaude Fable 5 2e6cd0e235 feat(capture): stall attribution phases A.2+A.3 — micro-probes and DxgKrnl ETW name the class
ci / web (push) Successful in 2m30s
ci / docs-site (push) Successful in 2m49s
apple / swift (push) Successful in 4m36s
ci / rust-arm64 (push) Successful in 10m10s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 6s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 5s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 4s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 43s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
android / android (push) Successful in 12m44s
deb / build-publish (push) Successful in 11m34s
docker / builders-arm64cross (push) Successful in 23s
windows-drivers / probe-and-proto (push) Successful in 29s
docker / deploy-docs (push) Successful in 30s
arch / build-publish (push) Successful in 15m48s
ci / rust (push) Successful in 16m21s
windows-drivers / driver-build (push) Successful in 1m46s
deb / build-publish-client-arm64 (push) Successful in 9m22s
deb / build-publish-host (push) Successful in 19m7s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 12m34s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 11m40s
apple / screenshots (push) Successful in 20m22s
windows-host / package (push) Successful in 14m39s
windows-host / winget-source (push) Skipped
Phase A.2: a refcounted micro-probe engine (fence round-trip per adapter,
DwmGetCompositionTimingInfo tick, watchdogged DwmFlush, Level-Zero
D3DKMTGetScanLine, CPU jitter sentinel) samples continuously on detached
sacrificial threads; each stall report reads the window back and the verdict
matrix folds it with the driver telemetry into a named class: ours-worker /
ours-delivery / CLASS-1 adapter freeze / CLASS-2 compositor blocked /
frame-generation / unattributed. The metronomic WARN carries the per-class
session tally.

Phase A.3: an event-id-filtered real-time ETW session on
Microsoft-Windows-DxgKrnl (QueryChildStatus 150/151, SetPowerState 154/155,
IndicateChildStatus 272, SetTimingsFromVidPn 430, DisplayDetectControl
1096/1097) rides every stall line as a DDI bracket summary — naming the
servicing call and its duration instead of 'below Windows'. Degrades to
etw=unavailable without admin; probes degrade per-leg (absence is stated,
never guessed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 23:44:35 +02:00
enricobuehlerandClaude Fable 5 b2168dae6a feat(vdisplay): stall attribution v1 — the driver testifies which leg lost the frames
Every IDD-push capture stall now carries a VERDICT instead of a hypothesis list.
The shared ring header grows a v2 telemetry tail (drain-loop heartbeat QPC,
last-acquire QPC, full-width offered counter) the driver stamps on every drain
pass; the host samples it between fresh frames and attributes each stall:
worker-stalled (our thread starved) / compose-silence (DWM composed nothing —
the disturbance is below capture) / delivery-leg (frames existed, our
publish/ring/consume lost them). The metronomic WARN prints the running tally,
so one pasted log line settles the Branch-1/Branch-2 fork of the
vdisplay-disturbance-immunity program per session, per box.

Both directions stay version-safe: the tail is gated on the HOST-stamped header
version (a v2 driver never writes past a v1 host's 64-byte layout — it maps the
whole section instead of a fixed 88 bytes), and a v2 host reads a zero heartbeat
as pre-telemetry driver, no verdict.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 23:44:35 +02:00
enricobuehlerandClaude Opus 5 75e4f00e50 Merge branch 'chore/windows-rerender-semantics' into main
ci / docs-site (push) Failing after 31s
ci / web (push) Successful in 54s
apple / swift (push) Successful in 1m17s
ci / rust-arm64 (push) Failing after 7m25s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 58s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 16s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 8s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 7s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 49s
deb / build-publish-client-arm64 (push) Successful in 9m13s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m7s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m27s
deb / build-publish (push) Successful in 15m0s
android / android (push) Canceled after 15m44s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 15m46s
ci / rust (push) Canceled after 15m45s
deb / build-publish-host (push) Canceled after 15m12s
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 4m11s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 44s
windows-host / package (push) Canceled after 15m35s
windows-host / winget-source (push) Canceled after 0s
release / apple (push) Successful in 17m52s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m29s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m12s
flatpak / build-publish (push) Failing after 12m18s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 2m59s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 4m12s
Windows 11 tray theming + per-connect device-name announcement, and the
pairing approve button no longer escapes the canvas on portrait phones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:27:36 +02:00
enricobuehlerandClaude Fable 5 a86b4c18ee feat(ci): arch gets its builder image too — the last per-run gigabyte, and sccache through makepkg
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 5s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 4s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 8s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
ci / docs-site (push) Successful in 1m0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 1m3s
docker / deploy-docs (push) Successful in 30s
docker / builders-arm64cross (push) Successful in 17s
ci / web (push) Successful in 3m1s
ci / rust-arm64 (push) Successful in 10m7s
ci / rust (push) Successful in 13m56s
arch / build-publish (push) Successful in 12m30s
The arch job was the one still paying full freight every run: ~1 GB of
pacman across its two install steps (never cached — container layers die
with the job) and cold cargo builds (arch was skipped in the sccache
rollout). punktfunk-arch-ci bakes base-devel + both makepkg legs' deps +
bun + node + sccache; the in-job installs become --needed no-op guards for
the one push where :latest lags. Rolling-release note in the Dockerfile:
packages now build against the image's snapshot, the same staleness the
gamescope cache already embraces, re-snapshotted on any ci/ edit.

sccache reaches makepkg by crossing the sudo boundary explicitly —
env_reset strips ambient env, so the wrapper env rides the existing
`sudo -u builder env ...` list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 22:49:00 +02:00
enricobuehlerandClaude Fable 5 43a9cf741b fix(client/android): the build resolves cargo from CARGO_HOME before guessing ~/.cargo
ci / web (push) Successful in 2m3s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 1m3s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 11s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
ci / docs-site (push) Successful in 3m27s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 24s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 21s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 31s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 34s
docker / builders-arm64cross (push) Successful in 15s
docker / deploy-docs (push) Successful in 49s
ci / rust-arm64 (push) Successful in 11m57s
android / android (push) Successful in 15m54s
ci / rust (push) Successful in 18m7s
The kts resolves cargo by ABSOLUTE path on purpose (a GUI Android Studio
launch has no ~/.cargo/bin on PATH), but user.home is the wrong anchor in
the CI image, where the shared toolchain lives at CARGO_HOME=/usr/local/cargo
— gradle died starting /root/.cargo/bin/cargo. CARGO_HOME/bin is where
rustup puts binaries whenever the variable is set, so it wins; the
~/.cargo fallback keeps GUI launches working.

Also: android.yml's path filter learns ci/android-ci.Dockerfile — an image
change must exercise its consumer instead of needing a manual rerun to
prove itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 21:47:07 +02:00
enricobuehlerandClaude Fable 5 12b0ce9b2d fix(ci): the android image learns node, and msix finally builds the CLI it packs
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 8s
ci / docs-site (push) Successful in 1m6s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 4s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 1m26s
ci / web (push) Successful in 1m48s
ci / rust (push) Canceled after 2m12s
ci / rust-arm64 (push) Canceled after 2m12s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m8s
docker / builders-arm64cross (push) Successful in 1m33s
docker / deploy-docs (push) Successful in 1m30s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m13s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m36s
Two first-run discoveries. actions/checkout (and every JS action) execs
`node` inside the job container — the android-ci image didn't ship it, so
the job died at checkout with exit 127 (flatpak.yml documents the same
lesson for fedora:43); a trailing layer keeps the fat SDK/NDK layers
cache-valid. And windows-msix has been red since bf981027 required
punktfunk.exe in the package without adding punktfunk-cli to the build —
the same gap 90c84ef4 already closed for deb, now closed here (rpm and
arch already build it; the CLI has no features, so the arm64 leg's
--no-default-features changes nothing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 21:36:57 +02:00
enricobuehlerandClaude Fable 5 a420855b5a feat(tray): the Windows tray dresses for Windows 11 and announces connects
The menu was a stock Win32 popup: light-mode on a dark taskbar,
DPI-virtualized (blurry on every scaled laptop), no icons. Now the
process opts into the system dark mode via the uxtheme ordinals
(135/136 - the same undocumented calls Explorer/PowerToys/Notepad++
make; menus never got a documented opt-in, and there is no WinUI tray
API to move to), a PerMonitorV2 manifest makes menu and icon crisp, the
multi-size .ico serves the DPI-correct frame, and items carry Segoe
Fluent glyph bitmaps - with the UAC shield on the elevated service
actions, per Explorer's convention. Rounded corners come free on
Windows 11. Everything degrades gracefully: missing ordinals mean the
classic light menu, a missing icon font means no glyphs.

New: a connect toast on the idle-to-streaming edge - title "<device>
connected" (from the summary's new client_name), body the mode
("Streaming 2560x1440 @ 120 fps") - via NIF_INFO, which Windows 11
renders as a native toast. The tray tags itself with the AUMID
unom.punktfunk.tray and the installer registers it under
Classes\AppUserModelId (DisplayName "Punktfunk" + the brand icon), so
the toast is attributed to Punktfunk with the logo. Unregistered dev
runs degrade to generic attribution, older hosts to a nameless title;
a tray started mid-session never fires a stale toast.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 21:32:50 +02:00
enricobuehlerandClaude Fable 5 9b2404a580 feat(core): every connect introduces the device by name
The wire always had Hello.name and the host always honored it - but the
connect path hardcoded None (only the PIN-pairing ceremony sent a name),
so every no-PIN "request access" knock surfaced as the fingerprint
placeholder "device abcd1234", and approving one without retyping a
name persisted that placeholder into the trust store forever.

NativeClient::connect now takes the device name. The session workers
and the probe connects pass trust::device_name() (the hostname), the C
ABI defaults to the same without a signature change (an ex10 variant
can make it explicit if an embedder wants a custom label), and Android
threads Build.MODEL through nativeConnect - the same convention its
pairing dialogs already use for nativePair.

The host, in turn, resolves the streaming client's display name (trust
store first, so an approval-time rename wins; else the sanitized Hello
name) and exposes it as client_name in GET /api/v1/local/summary for
the tray's connect toast - a deliberate, documented loosening of that
route's "no device names" contract, in the local user's favor: it tells
them who is on their machine. A paired-but-idle device's name still
never appears, which the mgmt tests now pin explicitly. openapi.json,
its docs-site copy, and the SDK bindings regenerate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 21:32:50 +02:00
enricobuehlerandClaude Fable 5 bf9d101be8 fix(web): the approve button stops hiding off-canvas on portrait phones
The pending-devices table's four p-card cells overflow a 360-430 px
viewport, and the actions cell - Approve - ended up past the right edge
of an overflow-auto wrapper inside the page's overflow-x-hidden column:
the button existed but could not be seen or reached (landscape is wide
enough, which is why it "worked" there). The name cell now flexes and
truncates, and the fingerprint/age columns collapse into a sub-line
under the name on narrow screens instead of widening the row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 21:32:49 +02:00
enricobuehlerandClaude Fable 5 171f08184f fix(ci/android): cargo-ndk v4 refuses direct invocation — probe it as the subcommand it is
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 8s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 4s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 42s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m45s
docker / builders-arm64cross (push) Successful in 30s
docker / deploy-docs (push) Successful in 38s
ci / rust (push) Canceled after 3m42s
ci / web (push) Successful in 2m29s
ci / docs-site (push) Successful in 2m45s
apple / swift (push) Successful in 6m46s
android / android (push) Failing after 3m54s
ci / rust-arm64 (push) Canceled after 6m44s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m39s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m35s
release / apple (push) Successful in 35m40s
apple / screenshots (push) Successful in 20m41s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 21:30:46 +02:00
enricobuehlerandClaude Fable 5 395daa8e98 fix(packaging/rpm): the CLI's %files entry had wandered into %install
bf981027 added the punktfunk CLI to the spec, but its `%{_bindir}/punktfunk`
files entry landed one section too early — inside %install, where rpm's
shell dutifully executed /usr/bin/punktfunk (which doesn't exist on a
builder) and killed both rpm legs at %install ever since. Move it where it
was headed: out of the script, into %files client — which was missing it,
so the fixed %install would otherwise have died again on an unpackaged
/usr/bin/punktfunk.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 21:30:07 +02:00
enricobuehlerandClaude Fable 5 9cda05dffb feat(ci): android gets a baked builder image; apple gets sccache and a pinned DerivedData
Android was the last job still downloading its world every run: ~3 GB of
SDK/NDK/CMake from Google, a from-source cargo-ndk build, and one monolithic
cache key where a gradle edit invalidated the cargo registry and the Rust
target/ with it. punktfunk-android-ci (content-keyed, LAN registry) bakes
JDK 21 + SDK + NDK + cargo-ndk + sccache; the workflow shrinks to checkout →
two caches → gradle. The cargo-home cache joins the fleet-wide namespace
(same lockfile, same layout — it was the same bytes under a private key),
gradle gets its own key shared with android-screenshots (which stored the
identical content under a second name), and target/ leaves the cache —
sccache covers the three ABI builds now.

Apple: sccache (self-healing ~/.local/bin install + already provisioned on
the mini) covers every cargo invocation build-xcframework.sh makes across
the swift job, the screenshots job and release.yml — three jobs that each
recompiled the same core. screenshots.sh learns PF_SHOT_DERIVED_DATA so CI
pins one stable DerivedData root instead of cold-building into two throwaway
mktemp trees per run (release.yml's disease, same cure), and the Simulators
get shut down after capture (the 846-leaked-sims lesson).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 21:20:59 +02:00
enricobuehlerandClaude Fable 5 fcdb4d147d feat(ci): sccache everywhere a Rust job runs — one warm compile cache for the whole fleet
audit / bun-audit (plugin-kit) (push) Successful in 25s
audit / bun-audit (sdk) (push) Successful in 25s
audit / bun-audit (web) (push) Successful in 37s
audit / docs-site-audit (push) Successful in 36s
audit / pnpm-audit (push) Successful in 29s
audit / cargo-audit (push) Successful in 2m57s
decky / build-publish (push) Successful in 43s
deb / build-publish-client-arm64 (push) Failing after 1m9s
deb / build-publish-host (push) Failing after 1m16s
ci / rust-arm64 (push) Failing after 1m35s
ci / web (push) Successful in 1m2s
ci / docs-site (push) Successful in 1m11s
apple / swift (push) Successful in 5m32s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 2m3s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 2m53s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3m30s
audit / license-gate (push) Successful in 7m50s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 2m38s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9m43s
flatpak / build-publish (push) Successful in 10m55s
android / android (push) Successful in 21m25s
windows-host / package (push) Successful in 20m6s
windows-host / winget-source (push) Skipped
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 13m57s
docker / deploy-docs (push) Successful in 57s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 13m36s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 9m32s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 3m20s
deb / build-publish (push) Successful in 16m37s
docker / builders-arm64cross (push) Successful in 2m27s
apple / screenshots (push) Successful in 21m3s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 3m12s
ci / rust (push) Canceled after 10m44s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m42s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 7m29s
arch / build-publish (push) Successful in 15m41s
Backend = the existing RustFS (storage.unom.io, S3, region home-central;
LAN-pinned to home-central's address by ci-core's unbound so cache traffic
never hairpins the router). Repo secrets SCCACHE_ACCESS_KEY_ID/SECRET carry a
keypair scoped to the unom-ci-sccache bucket; keys embed compiler hash +
target + flags, so the Ubuntu, Fedora, cross-arm64 and MSVC universes share
one bucket without ever colliding.

Wired: ci (rust, rust-arm64), deb (all three), rpm, bench,
linux-client-screenshots, windows, windows-msix, windows-host.
CARGO_INCREMENTAL=0 alongside (sccache and incremental are mutually
exclusive, and incremental artifacts are what bloated the persistent Windows
target dirs anyway). The binary is baked into the builder images; a per-job
ensure-step (same pattern as the GTK4 packages step) keeps jobs green while
the running :latest predates the bake, and ensure-windows-toolchain.ps1
self-provisions sccache.exe on the Windows runner. windows-drivers stays
unwrapped (wdk-build owns its build env), arch/android/apple are follow-ups.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 21:04:05 +02:00
enricobuehlerandClaude Fable 5 fcdb90c39b feat(ci): builder images move to the LAN registry, content-keyed; fan-out scoped by paths
The five builder images now live on home-ci-core's LAN registry
(192.168.1.58:5010) under content keys — a hash of the ci/ tree (+
rust-toolchain.toml for the cross image). docker.yml builds one only when its
key has no manifest yet, so a push that doesn't touch ci/ costs a curl per
image instead of seven WAN pushes and a set of per-SHA tags that no plain
prune could ever reclaim. Releases pin builders by copying the key manifest
to a vX.Y.Z tag via the registry API — no rebuild, no bytes moved.

Around that: deb/rpm/arch/android/apple/decky get path filters so docs-only
pushes stop lighting up the whole fleet (branch pushes only — tag runs match
tags:, as flatpak/windows-msix releases have proven for months); the
report-only bench job moves to bench.yml (nightly + dispatch) and stops
occupying a fleet slot per push; flatpak caches its Flathub runtimes and
builder state instead of re-downloading multi-GB every run; rpm's cargo
registry cache gets its own key namespace instead of sharing the Ubuntu
jobs'; audit caches cargo bin+registry rather than the whole toolchain dir;
docker-prune.sh loses the local act-cache cap/burst-clear (the cache is
central now — deleting it under disk pressure was how runner-2 ended up
cold-building everything) and gains a leaked-network prune.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 21:04:05 +02:00
enricobuehlerandClaude Opus 5 90c84ef4bc fix(packaging/deb): build the CLI the client .deb installs
ci / web (push) Successful in 1m4s
ci / docs-site (push) Successful in 1m12s
apple / swift (push) Successful in 5m23s
arch / build-publish (push) Failing after 5m59s
ci / bench (push) Failing after 6m2s
decky / build-publish (push) Successful in 41s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 30s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 39s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 17s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 23s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 18s
ci / rust-arm64 (push) Successful in 10m24s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Failing after 12s
docker / build-push-arm64cross (push) Skipped
docker / deploy-docs (push) Skipped
deb / build-publish-client-arm64 (push) Failing after 4m53s
android / android (push) Failing after 11m21s
deb / build-publish (push) Successful in 10m27s
deb / build-publish-host (push) Successful in 12m45s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 11m29s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 13m19s
ci / rust (push) Successful in 22m37s
apple / screenshots (push) Successful in 21m5s
With the client crates compiling again, deb got as far as "Build .debs" and died
on `install: No such file or directory` — a confusing way to report that
punktfunk-cli was never built.

Two halves of the same omission, from when the `punktfunk` CLI was added. deb.yml
pre-builds `-p punktfunk-client-linux -p punktfunk-client-session` and stops
there, while build-client-deb.sh installs THREE binaries. Its build-if-missing
guard tested only the first two, so the pre-built pair satisfied it, the fallback
build (which does list punktfunk-cli) was skipped, and the install of a binary
nobody had built failed.

deb.yml now builds all three, and the guard tests every binary it goes on to
install rather than a subset — either change alone fixes it; both together mean
the script is correct however it is called. That asymmetry is also why the arm64
leg passed throughout: it pre-builds nothing, so its guard always fired and built
all three.

arch was already right (`-p punktfunk-cli` in its PKGBUILD, installed at line 255)
and the rpm spec ships no CLI, so this was deb-only.

Verified: guard truth-tabled (skip when all three present, BUILD when the CLI is
missing, BUILD when nothing is), shellcheck clean, and punktfunk-cli builds to
target/release/punktfunk on a real Linux box.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 20:02:36 +02:00
enricobuehlerandClaude Opus 5 ab6d7727d3 fix(ci/runner): the disk guard has to fire before the disk is tight, not after
ci / web (push) Successful in 57s
ci / docs-site (push) Successful in 2m23s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 21s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 14s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 15s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 19s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 57s
ci / bench (push) Successful in 6m45s
ci / rust-arm64 (push) Successful in 10m32s
ci / rust (push) Failing after 10m39s
decky / build-publish (push) Successful in 38s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 22s
docker / build-push-arm64cross (push) Successful in 23s
android / android (push) Successful in 12m39s
docker / deploy-docs (push) Successful in 1m18s
apple / swift (push) Successful in 4m56s
arch / build-publish (push) Successful in 20m44s
apple / screenshots (push) Successful in 22m0s
deb / build-publish-host (push) Successful in 10m47s
deb / build-publish-client-arm64 (push) Successful in 7m53s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 7m27s
deb / build-publish (push) Failing after 6m6s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 11m40s
Measured over six hours after the last change: zero burst clears fired, while deb
still died of ENOSPC between polls. Both 30 min and 10 min lost the same race —
three concurrent Rust builds fill the disk and drain it again inside the interval,
so every poll landed on a healthy df and the guard concluded all was well.

Two changes, both aimed at that gap rather than at the symptom. The interval goes
to 2 min: the script is a few docker calls and no-ops in about a second, so
sampling five times more often costs nothing worth counting. And MIN_FREE_GB goes
45 -> 60, because the clear only reclaims idle images (~18 G measured) while three
jobs can eat the remainder inside one interval — a guard that waits for "tight"
has already lost. It has to act while there is still room to act in.

This narrows the window; it does not close it. With a 43 G image baseline on a
172 G disk, three concurrent heavy jobs are working inside ~129 G and can exceed
it. Sub-interval spikes are a polling problem, and the honest fixes are fewer
concurrent replicas or more disk.

Deployed to home-runner-1; deployed md5 matches the repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 18:58:32 +02:00
enricobuehlerandClaude Fable 5 7fc2775fad chore: the third-party notices catch up with the rebase
audit / bun-audit (sdk) (push) Successful in 20s
audit / bun-audit (plugin-kit) (push) Successful in 20s
audit / bun-audit (web) (push) Successful in 21s
audit / pnpm-audit (push) Successful in 9s
audit / docs-site-audit (push) Successful in 1m16s
audit / cargo-audit (push) Successful in 3m51s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 2m54s
ci / web (push) Successful in 3m3s
ci / docs-site (push) Successful in 1m51s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 1m55s
audit / license-gate (push) Successful in 7m7s
arch / build-publish (push) Failing after 7m56s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m47s
apple / swift (push) Successful in 5m44s
deb / build-publish (push) Failing after 5m39s
ci / rust-arm64 (push) Successful in 10m45s
ci / bench (push) Successful in 6m56s
android / android (push) Failing after 11m37s
decky / build-publish (push) Successful in 27s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 13s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 18s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 17s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 45s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 25s
deb / build-publish-client-arm64 (push) Successful in 8m57s
deb / build-publish-host (push) Successful in 11m13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7m55s
docker / deploy-docs (push) Successful in 59s
flatpak / build-publish (push) Failing after 9m41s
windows-host / package (push) Successful in 13m44s
windows-host / winget-source (push) Skipped
ci / rust (push) Successful in 22m43s
apple / screenshots (push) Canceled after 0s
docker / build-push-arm64cross (push) Canceled after 4m51s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 12m5s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 11m37s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m39s
release / apple (push) Successful in 27m44s
Regenerated after rebasing the Windows-client series onto main: the union of the bumped
windows-rs family and main''s os-icons vendored marks, with the conflict the generated
file picked up mid-rebase resolved by regeneration — the only honest edit for a file the
generator owns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:34:22 +02:00
enricobuehlerandClaude Fable 5 6edd7c3377 feat(client/windows): the tile menu grows real submenus
The flyout CAN nest at the current reactor rev (the "no submenus" comment predated the
bump), so the per-profile families move into MenuFlyoutSubItems: one "Connect with"
(bare profile names + Default settings as leaves) and one "Pin tiles" (verb-prefixed
leaves). The top level stays a fixed handful whatever the catalog grows to. The backend
wires submenu clicks recursively and reports LEAF text, so the connect-with leaves are
matched as the fall-through arm against the catalog, and the pin leaves keep their
prefix — that prefix is what tells the two families apart in the shared callback.
UIA-verified through the nesting: Pin tiles -> Pin tile: Work persists and the pinned
tile appears in the same gesture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:33:11 +02:00
enricobuehlerandClaude Fable 5 a9d3ca2fb1 feat(client/windows): the sheets learn to be dismissed, and the profile sheet says Save
Escape closes every overlay — the profile sheet, the host editor, the add-host modal —
and so does a tap on the scrim. The scrim-tap needed one trick: WinUI bubbles `Tapped`
out of the card into the scrim (reactor cannot mark it handled), so the card raises a
shared flag first and the scrim's handler swallows exactly that tap; only a genuine
outside tap dismisses. The profile sheet's dismiss button reads Save (with the Save
glyph) instead of Close — every field commits as you type, so Save is the promise the
button already kept, and a sheet full of edits wants a verb.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:33:11 +02:00
enricobuehlerandClaude Fable 5 9614e1a92e fix(client/windows): the switcher becomes what it pretended to be — one native control
The fused combo-plus-pencil was three controls squeezed into a wrapper, and hover gave the
trick away (each kept its own radius and hover fill; the pencil clipped). The honest
verdict: WinUI can build a real input group — CornerRadius is per-corner on every control —
but windows-reactor exposes only a uniform radius on Border, so the fake was never going
to hold. The native answer was next to it all along: the switcher is now ONE
DropDownButton — its label is the scope in play, its menu holds Default settings, the
profiles, "New profile…" and "Edit …" — with one hover state and no seams, and the pencil
folded into the menu. Retired with the ComboBox: the items/selected_index remount hazard
(a button label is one plain prop) and the scope-sentinel id.

UIA-verified: menu -> Work applies the scope (the section repaints, markers included),
menu -> Edit opens the sheet, Close closes it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:33:11 +02:00
enricobuehlerandClaude Fable 5 6703be377f feat(client/windows): pinning moves to the tile menu, and the chrome quiets down
Pin/unpin lives in the tile's "…" menu now (review decision, reversing the editor-owns-it
rule): one flat prefix-matched entry per profile — "Pin tile: X" / "Unpin tile: X" —
beside Copy link and Create shortcut, the other tile-shaped actions. The write is paired
with a new `hosts_rev` bump (the hosts-page mirror of `settings_rev`), so the pinned tile
appears — or vanishes — in the same gesture instead of on the next discovery tick.
UIA-verified: menu -> Pin tile: Work -> the pin is in the store and the second tile is on
the grid immediately.

The rest is the review list:
* Both sheets (host editor, profile) put their content in a scroll_view — a window shorter
  than the card scrolls it instead of clipping the bottom controls.
* The home header keeps ONE labelled action: Add host, in accent. Console/Shortcuts/
  Settings drop to icons with tooltips — four written-out buttons read as four competing
  calls to action, and icon-only removes the compact-width special case too.
* The profile switcher becomes one combined element: combo + pencil (icon only, the label
  is gone) inside a shared control-look wrapper — stock 4-epx radius, CardStroke outline,
  the pencil a borderless segment. In the defaults scope the bare combo stands alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:33:11 +02:00
enricobuehlerandClaude Fable 5 8783f11e90 fix(client/windows): pinning works where you can see it, and the rows learn one skeleton
"Can't pin hosts with a certain profile" — live diagnosis found the pin switches WORKED
but sat below the fold: the host editor rendered as an in-grid tile, its grid cell parked
mid-page, and on an ordinary window the pin section's visible rect was a 9-px sliver a
mouse can't hit (UIA's pattern-toggle, which bypasses hit-testing, persisted fine — that
is what isolated it). The editor is now a centred sheet — scrim + card, titled with the
host, Save/Cancel right-aligned — in a stable overlay slot beside the add modal and the
forget dialog. Verified end-to-end with a real mouse click: toggle flips, the pin lands in
client-known-hosts.json, and the pinned tile is on the grid after Save. Pin toggles also
log now — this hunt was blind until they did.

The settings rows learn one skeleton, label / (marker) / input / caption, per review:
* The Overridden capsule ("Overridden │ Reset", one tinted pill, whole-pill tap target)
  sits BETWEEN the row's label and its input, left-aligned — not trailing controls of
  wildly different widths, not mixed into the caption.
* Labels move out of the widgets (`.header` is gone from the row builders) into the row
  itself — a widget-embedded header allows nothing between label and box, and uniform
  6-epx row spacing needs the row to own all four lines.
* The scope bar's combo and Edit button share an explicit 36-epx height, and the bar shows
  the active profile's colour as a 12-epx chip beside the combo (dropdown items are plain
  strings in this toolkit, so the chip cannot ride inside the list).
* The sheet's colour swatches size the disc itself, not its child — they rendered as
  squashed ovals under the sheet's layout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:33:10 +02:00
enricobuehlerandClaude Fable 5 ba7667ee2b fix(client/windows): dialogs stop panicking the shell, and the chrome stops shouting
Deleting a profile reliably killed the app with E_BOUNDS ("Daten außerhalb des gültigen
Bereichs"): a ContentDialog is a PHANTOM child in the reactor backend — tracked logically,
never attached to the panel — and the reconciler unmounts a child before removing it, so
by the time `remove_child` runs the dialog's handle is gone, the backend no longer
recognises it as phantom, and it RemoveAt()s a visual child that never existed. (The third
upstream windows-reactor bug this client documents.) Both confirmation dialogs — delete
profile, forget host — are now ALWAYS MOUNTED with `is_open` doing the arming, and both
live in STABLE overlay slots (settings: [nav, sheet slot, dialog]; hosts: [page, add-modal
slot, dialog], each closed slot a same-kind background-less Border) so no pass ever
removes or repositions a dialog. UIA-verified: create profile -> delete -> confirm, the
shell survives.

The rest is the review feedback:
* A group with no fields renders NOTHING — Decoding and Library showed a heading over an
  empty card in profile scope (device facts, never per profile).
* The override marker leaves the caption: a small "Overridden" chip and the Reset button
  sit beside the control, bottom-aligned; the description below stays a description,
  identical in both states.
* Host tiles stop wearing three badges: Paired is the resting state and earns no chip
  (a chip is for a decision — Trusted/PIN/Open), and the bound profile becomes a small
  dot in its accent colour plus the name in caption text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:33:10 +02:00
enricobuehlerandClaude Fable 5 7bbee0eeb6 feat(client/windows): the profile selector settles above the settings surface
Out of the NavigationView for good. The pane footer clipped in the compact rail, the
clip-aware monogram variant read as broken, and reactor offers no pane-opened event to
adapt on — so the switcher is now a slim bar riding an Auto grid row above the nav: an
"Editing" label, the scope combo, and Edit profile… (with its glyph), each vertically
centred against the combo and sharing the content column's 24/28 page margins. Visible
from every section at every width, in no one's way, and the nav below keeps its Star row
so the surface still fills the window. The sheet drops its embedded scope combo — the bar
owns the choice now — and opens only with a profile in scope.

UIA-verified live: scope switch from the bar repaints in place (marker 5 -> 6), the sheet
opens from the bar and closes, Default scope restores.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:50 +02:00
enricobuehlerandClaude Fable 5 69743e3a09 feat(client/windows): the profile surfaces get their icons
Reset carries Undo, Edit profile carries Edit, Duplicate/Delete/Close carry Copy, Delete
and Accept — the icon support the bumped reactor added (#4736), applied where a glyph
disambiguates faster than a word. The rest of the shell already had its icons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:50 +02:00
enricobuehlerandClaude Fable 5 e8eb803a3f feat(client/windows): 4:4:4 reaches Windows, and the Overridden marker stops moving the page
The GTK client grew its "Full chroma (4:4:4)" switch in the same commit that fixed its
override marker; Windows got neither until now. The row lands in the Quality group under
HDR with the GTK wording verbatim (same setting, same constraints), a full overridable
profile row — the core overlay and the session binary already spoke `enable_444`, so the
shell was the only gap. OverrideFlags grows the matching flag, and a unit test now pins
the whole overlay -> row-flag mapping (tri-state resolution included): a field that
records without marking its row is the original Overridden-row bug wearing a new face.

And the marker obeys the GTK rule learned the hard way there: nothing that appears on an
edit may move what was edited. Both states of a profile-scope row reserve the same
caption-line height, so the marker + Reset materialise in place instead of shoving every
row below them down by a button's height.

Scope-blind audit (the bitrate-box failure class): clean. Decoder and GPU are hidden in
profile scope as device facts (GTK parity), the forwarded controller stays gated, and
every visible profile row commits through `commit`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:50 +02:00
enricobuehlerandClaude Fable 5 09d1691af4 fix(client/windows): the switcher footer survives the pane clip instead of guessing at it
Binding the footer's form to the WINDOW width was wrong twice over: the pane's open state
belongs to the user (the hamburger), so a wide window with a closed pane still clipped the
combo, and a narrow window with the pane opened still showed the compact disc. Reactor
exposes no pane-opened/closed event — so the footer now needs no pane state at all: its
LEADING 48 epx is the scope's monogram disc (profile accent when set) and the combo + Edit
button sit after it, so the compact rail's clip line falls exactly between them. Closed
pane → a clean disc (which opens the profile sheet); open pane → disc + combo. The same
trick NavigationViewItems use, at every width and in every mode — the display mode goes
back to Auto, the width threshold and the root window-size read are gone.

The sheet is now self-sufficient (title "Profiles"): it always carries the scope combo —
whether the pane's own combo is visible depends on state this page cannot observe — plus
the name/colour/duplicate/delete rows whenever a profile is in scope, so the disc is a
complete profile entry point from the bare rail.

UIA-verified: scope pick from the pane combo, Overridden marker 5 -> 6 in place, the
sheet opens from Edit profile… and closes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:49 +02:00
enricobuehlerandClaude Fable 5 4b5bca93a0 fix(client/windows): the scope switcher stays in the nav at every width
The width-adaptive fallback moved the switcher OUT of the pane on narrow windows, and in
the pane it could still clip — both read as broken. Now the nav is the switcher's only
home: `Left` mode (always-expanded 280 pane, combo sized to fit) while the content column
gets a workable width, and below that a `LeftCompact` rail whose footer swaps to a fitting
compact form — a 40-square monogram disc for the scope (profile accent when set) instead
of a clipped ComboBox. The disc opens the profile sheet, which in compact mode carries the
scope combo too (keyed per scope against the items/selected_index diff hazard), so
switching, creating, and editing profiles all stay reachable from the rail. The inline
content-column fallback is gone.

UIA-verified on the expanded path: scope pick from the pane, Overridden marker 5 -> 6 in
place, sheet opens and closes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:49 +02:00
enricobuehlerandClaude Fable 5 2409ec9a4b docs(client/windows): the harness passes where the real backend fails, and the note says so
Case 3 of the characterization suite (a sync use_state write from a backend-fired handler
re-renders) is true of the ENGINE and was taken as license to de-hoist event-driven page
state — and the live app immediately disproved it: with settings componentized and its
scope local, a real ComboBox pick changed nothing on screen (UIA-verified, no repaint).
The real WinUI backend's handler wiring still never pumps the pass the harness pumps.

The de-hoist is reverted one commit back; this one makes the record honest. The module
discipline note now separates the three tiers of evidence — engine rules (harness-pinned),
real-backend behaviour (live-verified, hoisting stays), and the tween-keyed corollary —
and case 3's comment warns it must not be read as de-hoist permission without a live UIA
check. The upstream gap worth reporting grows to two: the AsyncSetState HostId registry
miss, and the backend event wiring that bypasses the render pump.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:49 +02:00
enricobuehler 27039e04ff Revert "refactor(client/windows): page-only state climbs back down from root, on the measured rules"
This reverts commit 427f99a06647fd99e5c72d18cb7a710d8a7ac885.
2026-07-29 18:31:49 +02:00
enricobuehlerandClaude Fable 5 69c13b5aae fix(client/windows): the settings pane stops auto-collapsing under the default window
WinUI Auto mode collapses the NavigationView pane below 1008 epx — narrower than the
app's own 1000-wide default window, so the scope switcher kept getting booted out of the
nav into its inline fallback. The page now forces the mode: Left (always expanded, at a
tighter 250 pane length) down to 720 epx, LeftMinimal below that — the switcher lives in
the pane wherever the content column still gets a workable width.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:48 +02:00
enricobuehlerandClaude Fable 5 6ab2ee4819 refactor(client/windows): page-only state climbs back down from root, on the measured rules
The root component had absorbed every page's UI state because of re-render
folklore; the characterization suite (tests/reactor_semantics.rs) replaced the
folklore with measured rules at the current windows-reactor pin: a child's sync
use_state re-renders its owner reliably, including from backend-wired event
handlers (MenuFlyout clicks, pointer enter/exit, ComboBox selection) and under
element-equal wrappers. Only a background thread's AsyncSetState write is still
dropped. So user-event-driven page state moves into the pages that own it:

* hosts_page: forget, rename and hover become local sync use_state; the
  HostsProps fields, their PartialEq arms and the root plumbing are gone.
* settings_page: converted from a hook-free 14-parameter fn into a real
  component(...) with a SettingsProps (data-only PartialEq, the HostsProps
  pattern). Scope, the delete confirmation, the edit-modal flag and the repaint
  revision are its own sync use_state; commit() and the row helpers take the
  sync setter now.

What deliberately stays rooted, each with its reason in place: show_add and the
settings section drive root tween workers (a thread writing root async state
can only be started by the owner of the trigger), the tween progress values are
those workers' output, and everything thread-fed (discovery, HUD, probes,
pads, deep links, speed, library) keeps the async-drop rule. window size stays
a root read so resize re-renders the tree.

One deliberate behaviour shift: page-local state now resets when its page
unmounts, so re-entering Settings lands on the Default settings scope (the
section still persists, being root state).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:48 +02:00