Commit Graph
2195 Commits
Author SHA1 Message Date
a87fbebc0a feat(client/linux): the client says whether it is current, and can update itself
`punktfunk-client --check-update` fetches the same Ed25519-signed per-channel
manifest the host trusts, works out how this client was installed, and reports
what — if anything — can be done about a newer build. `--apply-update` does the
part it owns. `--version` prints the CI-stamped build string, which is both what
the comparison needs and what a packaging gate can run the binary for.

The engine lives here rather than in the Decky plugin for two reasons: verifying
a signature needs crypto that Decky's embedded Python does not have, and the
plugin is not the only surface that wants the answer. Every UI becomes a caller,
exactly as it already is for --pair, --library and --list-hosts.

What is possible depends on the install, and the module refuses to pretend
otherwise. A flatpak is a per-user install its launcher can update. A .deb/.rpm/
pacman client goes through the packaged root helper — a fixed, parameterless
systemd oneshot polkit authorises for members of the punktfunk-update group, so
the request carries nothing an attacker could influence and the payload comes
from the distro's own signed repositories. A sysext, a nix profile or a source
build gets the command and nothing else: the signed sysext feed carries the HOST
image, so a client sysext has no feed to update from, and a button that can only
fail is worse than one honest line.

A failed check reports the failure. "Could not tell" rendering as "up to date"
is the one answer that must never happen.

The box's capabilities are probed once into a `Caps` struct so the routing rules
are a pure function of (install kind, caps) — all seven of them are tested
without a box, including that the kill switch beats every kind and that pacman
needs its own root-owned full-sysupgrade opt-in on top of the group.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:41:26 +02:00
f6cfe382fd refactor(update): one signed-manifest checker, shared by the host and the client
The host has known how to answer "is a newer build available for this box's
channel?" since the update-from-web-console work. The Linux client is about to
need the same answer from the same signed document, and a trust rule that lives
in two places is a trust rule that will drift.

So the parts where being wrong is a security bug now exist exactly once, in the
new `pf-update-check`: Ed25519 verification against pinned keys, the manifest
schema and its fail-closed validation, the post-redirect fetch, the version
comparison that has to reconcile four different canary spellings, and the
install-kind ladder — the last parameterised by which product is asking, since
the delivery channels are the same ones but the markers are not.

The pinned key list moves with them. Two lists could have disagreed about who
may announce a release, and the one that drifted is the one nobody would have
noticed; `publish-update-manifest.sh` follows to the new path, and a MISSING
keys file there is now fatal rather than a warning that silently skips the
cross-check the step exists to perform.

Host call sites are unchanged throughout: `store::index` and `update::manifest`
re-export from the shared crate under their old names, and `update::detect`
keeps its cached `detect()` and the host's command hints. Verified with clippy
-D warnings on Linux and on Windows (nvenc,amf-qsv,qsv), 340 host tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:41:26 +02:00
enricobuehlerandClaude Fable 5 84c6938562 test(inject): the uhid sweep tests learn the devnode-churn grace
arch / build-publish (push) Failing after 6s
ci / docs-site (push) Successful in 1m53s
ci / web (push) Successful in 1m54s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 8s
ci / rust-arm64 (push) Successful in 2m15s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 9s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 9s
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 8s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 35s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 15s
deb / build-publish-client-arm64 (push) Successful in 1m32s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 28s
docker / builders-arm64cross (push) Successful in 9s
deb / build-publish (push) Successful in 3m50s
docker / deploy-docs (push) Successful in 34s
deb / build-publish-host (push) Successful in 4m26s
apple / swift (push) Successful in 4m54s
android / android (push) Successful in 7m2s
ci / rust (push) Successful in 9m15s
windows-host / package (push) Successful in 19m29s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 16s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m18s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m39s
apple / screenshots (push) Successful in 20m39s
6b0bd59b gave the unplug sweep a 300 ms grace so a mask glitch can't flap
PnP devices — and updated pad_slots' own tests, but not the two uhid_manager
consumer tests that encode the old immediate-sweep contract (its CI run was
cancelled by a superseding push, so the breakage surfaced on later shas).

PadSlots gains a #[cfg(test)] expire_grace() that backdates the armed clocks,
so consumer tests drive the debounce without wall-clock sleeps. The removal
test now also PINS the new hold-inside-grace behavior explicitly.

Verified in the amd64 CI-image container: pf-inject 83/83, clippy
--all-targets -D warnings clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 01:35:51 +02:00
enricobuehlerandClaude Fable 5 6b0bd59b8d fix(gamepad): a mask glitch no longer flaps devnodes — the unplug sweep gets a grace
ci / docs-site (push) Successful in 1m12s
ci / web (push) Successful in 1m26s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 27s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 8s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 5s
ci / rust-arm64 (push) Successful in 2m14s
deb / build-publish-client-arm64 (push) Successful in 1m6s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 7s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 13s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 13s
deb / build-publish (push) Successful in 3m38s
docker / builders-arm64cross (push) Successful in 7s
docker / deploy-docs (push) Successful in 25s
deb / build-publish-host (push) Successful in 6m44s
android / android (push) Successful in 7m55s
ci / rust (push) Failing after 8m1s
windows-drivers / probe-and-proto (push) Successful in 30s
apple / swift (push) Successful in 4m43s
windows-drivers / driver-build (push) Successful in 1m47s
arch / build-publish (push) Failing after 5m39s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m29s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m45s
windows-host / package (push) Successful in 12m1s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 19s
apple / screenshots (push) Successful in 20m47s
The unplug sweep dropped a pad the instant its active_mask bit read
clear — but a pad teardown is a whole PnP device removal (system-wide
device-change broadcasts, and on re-create a full hidclass
re-enumeration), so a client-side mask glitch of a few state frames
cycled real devnodes. The sweep now drops a pad only after its bit has
stayed clear for 300 ms; a bit that returns inside the grace disarms it.
A real unplug tears down one grace later, which nobody observes — the
pad already went quiet. Lands once in PadSlots, so every backend
(uinput/uhid/XUSB/UMDF) inherits it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 00:59:50 +02:00
enricobuehlerandClaude Fable 5 15361b0091 fix(drivers/gamepad): drop the 1 s enumeration wait its own premise doomed, and gate the logger
The PnP-identity fix established that the sealed channel is STRUCTURALLY
unavailable while hidclass enumerates (the DATA section arrives over the
HID interface those very queries create) — so device_type()'s bounded
1 s pump loop, reachable only for a devnode whose hardware ids matched
nothing, could never succeed there: it burned a second of a WUDFHost
dispatch thread mid-enumeration and then fell back to LAST_DEVTYPE
anyway. The fallback chain is now wait-free (attached section → PnP
identity → LAST_DEVTYPE), and the 8 ms timer refreshes LAST_DEVTYPE
whenever the section is attached, so the last resort is always current.

The logger also joins its siblings behind file_log_enabled(): a RELEASE
driver without the opt-in no longer OutputDebugStringA's (+ CString +
format! allocs) per logged event — the rumble OUTPUT hex dumps and the
cyclic GET_STRING polls were sustained paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 00:59:50 +02:00
enricobuehlerandClaude Fable 5 ccf5c922ee fix(core/abi): report_phase joins its siblings behind the quic cfg — the C header parses again
ci / docs-site (push) Successful in 1m6s
deb / build-publish-host (push) Failing after 14s
ci / web (push) Successful in 2m28s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 11s
deb / build-publish-client-arm64 (push) Successful in 1m16s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 10s
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 12s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 12s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 16s
ci / rust-arm64 (push) Successful in 4m30s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m13s
apple / swift (push) Successful in 4m46s
android / android (push) Successful in 5m32s
docker / builders-arm64cross (push) Successful in 11s
deb / build-publish (push) Successful in 6m8s
docker / deploy-docs (push) Successful in 35s
ci / rust (push) Successful in 7m7s
arch / build-publish (push) Failing after 9m7s
flatpak / build-publish (push) Successful in 7m3s
windows-host / package (push) Successful in 18m52s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 12s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m25s
apple / screenshots (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 16m55s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m17s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m23s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 2m50s
release / apple (push) Successful in 25m47s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 4m44s
The new punktfunk_connection_report_phase lacked the #[cfg(feature = "quic")]
its connection-fn siblings carry, so cbindgen emitted it OUTSIDE the guarded
region that declares the opaque PunktfunkConnection — the generated header
stopped compiling as C (CI's harness caught it; my local harness failure was
a pre-existing macOS linker issue that masked this distinct cause — full-suite
comparison attributed the wrong root).

Verified: the harness compiles and passes in the amd64 CI-image container.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 00:38:31 +02:00
enricobuehlerandClaude Fable 5 fa822744ff feat(core/host/android): phase-locked capture — frames arrive on the client's latch schedule
ci / web (push) Successful in 1m59s
ci / docs-site (push) Successful in 2m13s
ci / rust-arm64 (push) Successful in 5m17s
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 4s
android / android (push) Successful in 5m52s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
deb / build-publish (push) Successful in 5m38s
ci / rust (push) Failing after 5m52s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 18s
arch / build-publish (push) Failing after 6m7s
deb / build-publish-client-arm64 (push) Successful in 3m55s
docker / builders-arm64cross (push) Successful in 19s
deb / build-publish-host (push) Failing after 4m24s
apple / swift (push) Successful in 4m35s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m13s
docker / deploy-docs (push) Successful in 31s
windows-host / package (push) Failing after 6m22s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m35s
flatpak / build-publish (push) Successful in 6m33s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m22s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 10m8s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 2m56s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 11m43s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 4m1s
release / apple (push) Successful in 25m5s
apple / screenshots (push) Successful in 20m50s
The host's capture tick and a client's panel vsync are independent ~120 Hz
oscillators; their drifting phase sweeps every frame's wait-for-latch across
a full refresh period (measured on-glass: latch p50 oscillating 5.4-8.9 ms
with a fixed margin) — the beat is the residual judder and the fat p95, and
no client can fix it alone. Design: punktfunk-planning
design/phase-locked-capture.md.

Protocol (punktfunk-core):
- PhaseReport (control 0x32, next to the clock family): the client's next
  display latch ALREADY CONVERTED to host clock (the skew offset lives only
  client-side), panel period, uncertainty, and the measured median
  arrival-lead — the controller's error signal. ~1 Hz, latest-wins.
  CLIENT_CAP_PHASE_LOCK advertises it; CtrlRequest::Phase + report_phase()
  + the C ABI mirror carry it.
- The 0xCF host-timing tail grows a phase ACK (applied_phase_ns, 29-byte
  form) under the same strict-prefix append discipline — old readers parse
  the shorter forms; degradation pinned by tests.

Host engine (arrival-slaved loop — no backend can move the source vsync,
per the tick-ownership audit in the design doc):
- PhaseCtl bridges control task → encode loop (the fec_target pattern,
  multi-field). PhaseController walks a per-frame HOLD before submit toward
  the client's reported lead hitting target = max(2.5 ms, uncertainty+1ms):
  1 Hz adjust, 2 ms max step, 300 µs deadband, period-wrapping (the
  newest-wins capture slot makes a wrapped hold sample fresher content, not
  staler). A loop local, so every mid-stream rebuild keeps the lock; a new
  session re-acquires. PUNKTFUNK_PHASE_LOCK=0 disarms.

Android reporter: the presenter's 1 Hz pf.present flush returns the
window's measured latch p50; the async loop converts the vsync clock's next
timeline monotonic→realtime→host and reports. Inert toward old hosts.

Gates: docker amd64 clippy --all-targets -D warnings (host+core, nvenc)
clean; core suite incl. the new wire tests; cargo ndk arm64 check/clippy
clean. On-glass A/B vs the .173 host owed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:44:09 +02:00
enricobuehlerandClaude Fable 5 f9faab780e tune(android): the latch margin drops to SF's real lead (2.5 ms)
Each ms of submit-margin is a ms on every frame's display stage; SF's latch
runs ~1-2 ms before present and the release is a sub-ms binder call, so 4 ms
was padded. Measured (A024, 120 Hz game load): latch p50 8-10 → 5.4-8.9
(phase-drift dependent), paced stays 1-5/s. A device that misses at this
margin shows it as a paced-counter rise, not stutter — the widen signal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:44:09 +02:00
enricobuehlerandClaude Fable 5 a6ff0350e4 feat(android): frames target SurfaceFlinger's latch, not the GPU-render deadline — display 21→9.5 ms
The remaining ~21 ms display stage was the conservative release target: the
presenter aimed at the first frame timeline whose DEADLINE was still ahead,
and the platform's deadline budgets for GPU rendering the app has yet to
submit (presDeadline = 11.3 ms on the A024 — more than a full 120 Hz
period). A decoded video buffer has no GPU work left; its only real
constraint is SurfaceFlinger's own latch lead. Every frame paid a whole
extra refresh of waiting for a budget it never used.

next_target now gates (and subdivides) on the timeline's EXPECTED PRESENT
minus a 4 ms latch margin; the glass budget reopens at that latch instant
(expected present − margin) rather than the deadline, which under the
aggressive gate can already lie in the past — an instant reopen would let
two releases pile onto one vsync. A mis-gamble presents one vsync later,
which is exactly what the deadline gate paid on every frame — the trade is
one-sided.

On-glass (A024, 2800×1260@120 HDR, game load): latch p50 21→8-10 ms,
display 26,3→9,5 (pace 0,8 + latch 7,7), e2e 43,7→26,4 p50 / 29,2 p95,
released=displays=120, paced≈0, forced=0. The latch now sits under one
refresh interval — the vsync-latch floor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:44:09 +02:00
enricobuehlerandClaude Fable 5 984f7be896 fix(android): the presenter paces to the panel's real grid, not the app's down-rated vsync
On-glass (A024, 120 Hz panel, 120 fps session) the first presenter build
released only 60/s and the HUD display term hit 40 ms. Root cause, in two
layers: Android down-rates a game-category uid's choreographer stream to
60 Hz (frame-rate categories / game default frame rate), and under that
override Display.getRefreshRate REPORTS THE OVERRIDE — so the presenter's
panel grid read 16.67 ms on an 8.33 ms panel and the subdivision became a
no-op, pacing the video at half rate and dropping every other frame.

Three-part fix, verified live on the same device:
- Kotlin passes the panel rate from the supported-modes TABLE
  (MainActivity.streamPanelFps — the mode list is not override-filtered)
  instead of display.refreshRate, and votes the app's render rate up via
  View.requestedFrameRate = streamHz (API 35+) while streaming.
- The native vsync clock LEARNS the panel period from observed timeline
  spacing (downward-only: the finest spacing SurfaceFlinger ever reports is
  the true grid) and next_target subdivides the reported timeline onto it —
  full-rate on down-rated devices, a no-op where callbacks match the panel.
- OnFrameRendered display/latch samples get the e2e clamp (0..10 s): a
  vendor's first callbacks can carry a garbage system_nano (observed: an
  epoch-sized latch max) that would poison every max it lands in.

pf.present gained panelMs next to vsyncMs, and a one-shot cadence
diagnostic logs Δ/timelines/spacing/panel on the third tick.

After: released=120 displays=120 paced=0, pace p50 <1 ms, latch p50 ~16 ms
idle / ~22 ms under game load (2 refresh intervals at 8.33 — the same
composited-pipeline law the Apple client measured), HUD display ~17-26 ms
vs 40 before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:44:09 +02:00
enricobuehlerandClaude Fable 5 e08fd91cd1 feat(android): a timeline presenter — frames reach glass on the panel's schedule, not decode's
The Android port of the Apple client's stage-4 deadline discipline, closing
the side-by-side feel gap (both clients 120 Hz; Android released decoded
buffers the instant they appeared, with zero vsync awareness — the latch
phase inherited every network+decode jitter and bursts queued behind the
display).

The presenter (async loop only; the sync loop stays the untouched escape
hatch behind the Low-latency toggle):

- decode/vsync.rs: an AChoreographer thread (dlsym'd like the other
  above-floor symbols) publishing the panel's vsync grid + frame timelines
  (postVsyncCallback, API 33; postFrameCallback64 fallback on 31/32) and
  ticking the decode loop's event channel. Started lazily on the first
  decoded frame.
- decode/presenter.rs: a newest-wins slot (Lowest latency, default) or a
  1-3 frame smoothing FIFO with preroll/underflow re-arm (Smoothness) between
  decode and release; a glass budget of exactly ONE undisplayed release in
  flight, reopened at the target timeline's DEADLINE (SurfaceFlinger's latch
  — reopening at present time would halve the sustainable rate) with a 100 ms
  stale force-open backstop; the release itself via
  releaseOutputBufferAtTime(expectedPresent) so the latch phase is
  deterministic. debug.punktfunk.presenter=arrival sysprop restores the
  legacy path for a rebuild-free on-device A/B.
- Metrics: DisplayTracker is now always-on and carries the release stamp, so
  the display stage splits into pace (decoded→release) + latch
  (release→displayed); a 1 Hz pf.present logcat line (released/displays/
  paced/noBudget/forced/qDry + pace/latch p50/max + measured vsync) makes a
  HUD-off wireless A/B readable; nativeVideoStats grows to 30 doubles
  (26=paceP50, 27=latchP50, 28=presents, 29=presenterActive; 0-25 frozen)
  and the DETAILED HUD prints the split + presents.
- Intent parity: present_priority/smooth_buffer — the Apple client's
  stored values and labels — as globals, profile-overlay fields (round-trip
  + scope markers), and Settings pickers under Decoding; threaded through
  nativeStartVideo into the presenter config.

Verified: cargo ndk check/clippy clean for arm64 (the two type_complexity
warnings are pre-existing audio/mic ones), armv7 via the kit gradle task,
host cargo check clean, rustfmt clean, gradle :app/:kit unit tests all pass.
On-device before/after on the Nothing Phone 3 still owed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:44:09 +02:00
enricobuehlerandClaude Fable 5 581320df0c fix(android): the stream stops fighting large displays and owns the cutout explicitly
The three Play Console pre-release findings for 0.22.3, resolved:

- Orientation restriction: the in-stream SENSOR_LANDSCAPE lock is now applied
  on compact devices (sw < 600 dp) only. On tablets/foldables/desktop windows
  it is a large-display anti-pattern (Android 16+ ignores it there outright)
  and unnecessary — the aspect-ratio letterbox renders correctly in any
  orientation; the lock was always a phone-ergonomics choice. No manifest
  restriction existed, and resizeability stays unrestricted.
- Edge-to-edge determinism: the stream window now sets
  LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS explicitly (and restores it on the way
  out) — SDK-35 enforcement makes that the immersive default, pre-15 devices
  letterboxed the notch as a dead bar; being explicit gives both the same,
  correct behaviour. The stream's own letterbox is black, so the cutout region
  can never show anything wrong.
- Deprecated edge-to-edge APIs: audited — no in-app use of
  setStatusBarColor/setNavigationBarColor/systemUiVisibility/translucent
  flags or theme attrs; enableEdgeToEdge (androidx.activity 1.13.0) is already
  in place with explicit SystemBarStyle. What the scanner sees are androidx's
  own API-level-guarded compat branches, which Google documents as ignorable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:44:09 +02:00
enricobuehlerandClaude Fable 5 87b6fa8813 feat(android): the panel is pinned to the stream's refresh, and touch skips the vsync batch
Three quick latency wins for phones, ahead of the presenter rebuild:

- setStreamDisplayMode: the window-level preferredDisplayModeId is pinned to
  the stream's refresh (exact rate, else the smallest integer multiple, else
  the highest available) for the session. The surface-level frame-rate hint
  alone is advisory and some OEM refresh governors (Nothing OS's LTPO logic
  among them) ignore it for third-party apps — leaving a 120 Hz session
  presenting on a 60/90 Hz panel. nativeVideoSize gained a trailing
  refreshHz element for this (old readers index only 0/1). TV keeps the
  native HDMI mode switch instead.
- The surface hint itself now passes compatibility = FIXED_SOURCE on every
  form factor: the stream is fixed-rate video the client cannot re-pace;
  DEFAULT invited governors to not switch.
- requestUnbufferedDispatch(SOURCE_CLASS_POINTER) on the hosting view while
  streaming: touch/pointer events were vsync-batched — up to a frame of
  input latency the stream shouldn't pay.
- The HUD polls the panel's live refresh each second and flags '⚠ panel N Hz'
  when it sits below the stream rate, so an unpinned panel is visible instead
  of reading as inexplicable judder. Stale nativeStartVideo kdoc (low-latency
  'off, the default') corrected — it defaults ON under low_latency_mode_v2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:44:09 +02:00
enricobuehler 2748b84933 style(vdisplay/driver): rustfmt one line the audit bundle left unwrapped
ci / web (push) Successful in 1m6s
android / android (push) Canceled after 2m41s
apple / swift (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 2m43s
ci / rust (push) Canceled after 2m41s
ci / rust-arm64 (push) Canceled after 2m41s
ci / docs-site (push) Canceled after 2m39s
deb / build-publish (push) Canceled after 1m33s
deb / build-publish-host (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Canceled after 0s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (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/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
windows-host / package (push) Canceled after 0s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
windows-drivers / probe-and-proto (push) Successful in 23s
windows-drivers / driver-build (push) Successful in 1m46s
`5742ec95` landed `create_monitor`'s EDID call split across two lines where it
now fits on one, so the drivers workspace fails `cargo fmt --all --check`.
Whitespace only — separated from the gamepad work it happened to block.
2026-07-30 23:40:35 +02:00
enricobuehler 98147fb89c fix(gamepad/windows): the channel proof sizes its feature buffer from the descriptor
`HidD_GetFeature`/`HidD_SetFeature` reject any buffer shorter than the
collection's `FeatureReportByteLength`, and the proof hardcoded 64. That held
only because every pad enumerated with the DualSense descriptor; a Deck that
enumerates as a Deck reports 65 (its one feature report is unnumbered and 64
bytes wide, plus the report-id slot Windows always reserves), so both calls
would have failed, the proof with them, and — the gate being fail-closed — the
pad would never receive its DATA section at all.

Take the length from the descriptor, and accept the Deck's reply at either
offset 0 or 1: one driver binary serves four identities and Windows places an
unnumbered report's payload behind the report-id slot.

An answer must also VALIDATE, not merely parse. `from_feature_report`
reinterprets any 17 bytes, and a Deck serves its one unnumbered feature report
for *any* requested id — so it answers the PS `0x85` probe with Steam attribute
bytes that parse into a proof and fail on magic. Returning that first answer
stopped the search while the real proof sat one transport away.
2026-07-30 23:40:35 +02:00
enricobuehler 00c29f82f2 fix(gamepad/windows): a pad enumerates as the controller it IS, not always a DualSense
A Steam Deck client streaming to a Windows host had a stuck stick and a stuck
d-pad. The virtual pad was enumerating with the DualSense VID/PID **and the
DualSense report descriptor**, so Windows parsed the 64-byte Deck frame as
DualSense report 0x01: LX = report[1] = 0x00 (stick hard left), LY = report[2] =
0x09 (hard up), and a d-pad hat of 0 — which is UP, held forever.

The driver picked its identity from `device_type` in the sealed section, but
hidclass asks for the descriptors and attributes while it STARTS the device, and
the section can only be delivered over the HID device interface — which does not
exist until those queries are answered. The channel was structurally unavailable
at the only moment it was needed, so `device_type()`'s bounded wait always timed
out and every identity fell back to DualSense. Not a race: DualShock 4 and the
Edge enumerated as DualSenses too (verified on .173 — both report 054C:0CE6 with
a 64-byte DualSense input report, while their on-demand strings read correctly).

The devnode's own hardware ids carry the identity and are readable at
EvtDeviceAdd, before anything is asked, so resolve it there. The section stays
authoritative once attached; the old wait survives only for a devnode whose ids
match nothing.

`hwid_devtype_table_matches_the_driver` pins the host's hwid → device_type
mapping against the driver's table, including the ordering trap that
`pf_dualsense` is a prefix of `pf_dualsenseedge`.
2026-07-30 23:40:35 +02:00
enricobuehlerandClaude Fable 5 5742ec9548 fix(vdisplay/driver): the audit bundle — one timing formula, honest EDID, scoped watchdog, lock-free drain, D0-resume re-init, knobbed RT priority
ci / web (push) Successful in 1m5s
windows-drivers / probe-and-proto (push) Successful in 1m5s
deb / build-publish-host (push) Failing after 12s
apple / swift (push) Successful in 1m55s
ci / docs-site (push) Successful in 2m56s
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 6s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 7s
windows-drivers / driver-build (push) Successful in 2m10s
ci / rust-arm64 (push) Successful in 3m22s
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 8s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 16s
deb / build-publish-client-arm64 (push) Successful in 2m44s
android / android (push) Canceled after 4m33s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 4m35s
ci / rust (push) Canceled after 4m40s
deb / build-publish (push) Canceled after 4m40s
docker / builders-arm64cross (push) Canceled after 2s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 42s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 39s
windows-host / package (push) Canceled after 1m21s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
flatpak / build-publish (push) Canceled after 3m45s
release / apple (push) Canceled after 5m40s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 2m41s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
One signing pass over the 2026-07-30 audit findings:

- Timing math unified (D4): monitor-description and target modes now come
  from ONE IddSampleDriver-exact builder differing only in
  vSyncFreqDivider; the virtual-display-rs legacy formula (width-less
  pixel rate, deliberately fractional vSync) is gone.
- EDID (D5): the preferred-timing DTD is built from the SESSION's mode
  when it fits the encoding (pf-driver-proto's tested builder; 1080p60
  stays the fallback); the range-limits descriptor covers everything the
  driver can advertise (max clock 150 MHz → 2550 MHz, max-H +255 —
  the old limits were violated by the driver's own 1080p120 default);
  product code 0 → 1. Deliberately still no HDMI VSDB — documented in
  the module doc.
- INF (D6): UmdfFileObjectPolicy=AllowNullAndUnknownFileObjects added
  (the sibling drivers all carry it); the dead DeviceGroupId (inert under
  ProcessSharingDisabled) dropped; the IddCx0102-vs-
  IddMinimumVersionRequired=10 pairing documented as deliberate — 0102
  is the extension's registered identity, not a version request.
- Watchdog lifecycle (D7): device cleanup now stops the host-liveness
  thread (it ran forever and its reap raced device teardown over the
  same monitor list).
- Drain path off the mutex (D8): the per-frame has/take_frame_channel
  checks (≥60 locks/s per worker on the mutex the whole control plane,
  the mode DDIs and the watchdog contend) are gated by a delivery
  generation counter — the steady state takes no lock.
- Adapter cache (D9): last-write-wins slot instead of a OnceLock, and a
  D0 re-entry from a REAL low-power state clears + re-inits — the stale
  pre-power-cycle handle used to wedge every later IOCTL_ADD.
- Realtime GPU priority (D10): IddCxSetRealtimeGPUPriority is now
  A/B-able without a rebuild (PFVD_NO_RT_GPU, machine env) — no
  canonical IDD driver raises it, and it preempts the game's and DWM's
  queues at a level apps can't reach.
- Logging (D2): the logger rides file_log_enabled() as a whole — a
  RELEASE driver without the opt-in no longer OutputDebugStringA's (+2
  allocs) per logged event.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:35:32 +02:00
enricobuehlerandClaude Fable 5 1c15ba89c3 fix(drivers/pads): the XInput path stops re-mapping the mailbox, and release logging gets a real off switch
pf-xusb pumped the sealed-channel bootstrap mailbox — an open+map+close+
unmap plus two heap allocs — on EVERY XInput IOCTL, per pad. A periodic
WDF timer (the pf-gamepad pattern) now owns the pump: adoption,
re-delivery and host-gone detection happen there, and the IOCTL path
reads the cached view. A vanished host still reads as a neutral pad
within one 8 ms tick, and the heartbeat mark still advances per serviced
IOCTL, so the host keeps seeing the GAME-visible polling path move.

Both pad drivers' log() also called OutputDebugStringA unconditionally —
a syscall + CString + format! alloc per logged event in RELEASE builds,
on per-IOCTL paths (SET_STATE hex dumps during rumble, the cyclic
GET_STRING polls). The whole logger now rides the existing
file_log_enabled() gate (debug builds, or PF*_DEBUG_LOG), and dbglog!
skips its format! too.

(pf-gamepad's identical log shape is deliberately untouched: the
unpushed fix/deck-hid-identity branch owns that file right now.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:35:32 +02:00
enricobuehlerandClaude Fable 5 85dd75437b feat(driver-proto): an EDID detailed-timing builder for the session's mode
An 18-byte DTD with fixed reduced blanking (80 px / 45 lines — a virtual
display's blanking only has to be self-consistent), refusing modes the
encoding cannot carry (pixel clock past the u16 10 kHz field, 4K120-class;
>12-bit actives). Lives here rather than in the driver so it is unit-
tested on every platform; pf-vdisplay splices it over its hard-coded
1080p60 preferred-timing descriptor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:35:32 +02:00
enricobuehlerandClaude Fable 5 8140d3f8b3 feat(capture/windows): a degraded stretch logs one summary line at recovery
Per-hole stall lines gate on prior ACTIVE flow, so inside a sustained
~2 fps phase only the first hole is reported and the log goes quiet
exactly while the user suffers — the field shape (deep 15 s stretches at
2 fps) was invisible without a stats recording.

StallWatch now tracks the stretch: opened by a reported stall, fed by
every stall-sized hole while the activity gate stays broken, closed when
sustained flow returns (or a ring recreate cuts it — its holes predate
the recreate and still count). Closure surfaces one INFO line: span,
hole count, summed hole time, worst hole. One-hole stretches dissolve
silently (the stall's own line covers them), and a ≥10 s gap closes the
stretch first so a quit-to-desktop pause never folds into the tally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:35:32 +02:00
enricobuehlerandClaude Fable 5 789dafc95f feat(capture/windows): the stall micro-probes get an off switch, the depth pin a backoff
Two standing costs from the interval-stutter investigation, consolidated:

- PUNKTFUNK_STALL_PROBES=0 now opts a box out of the micro-probe engine
  (per-GPU 10 Hz fence copies, a parked blocking-DwmFlush waiter, the
  5 ms-cadence CPU sentinel). Default stays ON while the field program
  runs; off, stall lines keep the driver telemetry and the cheap ETW
  present/queue discriminator and only lose the corroborating probe legs
  — the verdict matrix already treats an absent probe window honestly.

- The negotiated-depth pin-back stops hammering a display that refuses
  the flip: it was 4 CCD writes + 8 display-config queries per second,
  forever, all on the session-global display-config lock. After ~2 s of
  eager retries it re-attempts every ~4 s — still self-healing, without
  the drumbeat.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:35:32 +02:00
enricobuehlerandClaude Fable 5 ecb3d1ab22 fix(host/windows): the pad service thread no longer waits on pnputil at its leisure
driver_store_inventory() ran `pnputil /enum-drivers` synchronously inside
the attach-failure diagnose path — on the pad service thread, with no
timeout. A busy or wedged driver store blocks that enumeration for tens
of seconds, and the thread must keep draining pad slots.

The query now runs on its own thread (spawned once per process); the
caller waits a bounded 2 s so a fast pnputil still lands in the same
report, and a slower one fills the cache for later diagnoses — the WARN
then says the store could not be queried yet instead of blocking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:35:32 +02:00
enricobuehlerandClaude Fable 5 e71f4696ee fix(host/windows): an unlightable sink stops the teardown warn+force loop
A box whose only external display is an off/standby TV warned "no
external physical display active after the restore (connected=1)" and
forced the EXTEND preset on EVERY teardown — an inert remedy (with one
connected display the preset can't relight anything) that reads like a
fresh failure in every field log.

The backstop now measures its own effect: after a force-EXTEND it
re-reads the inventory, and a dark set that stayed dark THROUGH the
preset is latched as unlightable — subsequent teardowns log a debug line
and leave the topology be. The poisoned-snapshot chain the backstop
exists for is not latched away: there the panel CAN light, so the first
force succeeds and the latch stays clear. Process-lifetime latch, so a
host restart re-probes once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:35:32 +02:00
enricobuehlerandClaude Fable 5 d014cea489 feat(core/abr): a keyframe-begging decoder backs the bitrate off, loss or no loss
The RX-9070 field trace: 14 decode-recovery keyframe requests in 2 s at
~300 Mbps with loss_ppm=0 — and the controller held the rate, because no
loss/OWD/latency signal moved. Repeated keyframe asks ARE the decoder
saying it cannot keep up; now they are an ABR input.

The control task counts outbound CtrlRequest::Keyframe at its send choke
point (the one place every emitter funnels through); the pump drains the
count per 750 ms report window into on_window. Two asks in a window is
ordinary-bad (two-window confirmation, like an OWD rise); four or more is
severe (the emitters throttle at 100 ms, so 4+ means the window was spent
begging) and backs off ×0.7 after one window. RFI asks are deliberately
not counted — they are the routine loss-recovery path and loss_ppm
already prices them in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:35:32 +02:00
enricobuehlerandClaude Fable 5 cd6ceb98e3 feat(capture/windows): stall lines say whether the content or the display path went silent
A compose-silence hole used to be blamed on the frame-generation path by
default — mislabeling benign content pauses (menus, loading, game hitches)
as display-path bugs. The corrected discriminator convicts on witnesses:

- Microsoft-Windows-DXGI Present/PresentMPO (ids 42/55) rides the host's
  filtered ETW session — one event per swapchain present, stamped with the
  presenting pid (named in the stall line).
- BltQueueAddEntry/CompleteIndirectPresent (ids 1071/1068) witness frames
  entering/leaving the virtual display's kernel present queue (the modern
  IDD path; anatomy proven on-glass via xperf, 2026-07-30).
- classify() now reads the window counts: presents flowing while the queue
  starved = FRAME-GENERATION (the OS dropped composed frames — the real
  bug class, never yet observed); no presents anywhere = CONTENT-SILENCE
  (benign for the display path); no working witness = UNATTRIBUTED, never
  a guess.
- DWM_TIMING_INFO.cFrame is demoted to an advisory dwm_frames_frozen=
  print: on Win11 it is refresh-synthesized and advances without real
  composes (proven against a kernel trace), so it convicts nothing.
- DxgKrnl legacy Present (id 184) is retired — it never fires on the
  redirected BltQueue path.

Stall lines carry etw_presents=/etw_queue_adds= plus named presenters;
compiled and validated on-glass (a quit-transition stall correctly labeled
CONTENT-SILENCE presents=0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:35:28 +02:00
enricobuehlerandClaude Fable 5 f87c1e6cec fix(encode): gate multi-slice frames on the client's decoder — the 0.17.0 Chromecast crash
ci / docs-site (push) Successful in 1m9s
apple / swift (push) Successful in 1m15s
ci / web (push) Successful in 3m16s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 7s
deb / build-publish-client-arm64 (push) Successful in 2m13s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 5s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 7s
ci / rust-arm64 (push) Successful in 3m34s
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 8s
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
deb / build-publish-host (push) Successful in 4m20s
docker / builders-arm64cross (push) Successful in 6s
docker / deploy-docs (push) Successful in 28s
deb / build-publish (push) Successful in 6m17s
arch / build-publish (push) Failing after 7m58s
flatpak / build-publish (push) Successful in 9m6s
ci / rust (push) Successful in 13m37s
release / apple (push) Successful in 16m32s
windows-host / package (push) Successful in 18m21s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 32s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m21s
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 (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m24s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m27s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 2m56s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 3m54s
apple / screenshots (push) Successful in 20m45s
android / android (push) Successful in 5m0s
Field report: since 0.17.0 a stream to a Chromecast with Google TV 4K
freezes on the first frame and ~80% of the time crashes + reboots the
DEVICE — with both the Punktfunk app and Moonlight, while an Xbox
Series S is fine. Root cause: LN1 Phase 3 (67b79810) defaulted Linux
direct-NVENC to 4 slices per frame for EVERY session. Amlogic HEVC
decoders wedge on multi-slice AUs — exactly why moonlight-android
requests slicesPerFrame=1 for every hardware decoder (4 only for
software slice-threading) — and our RTSP parser never read the request.
The Phase-3 commit recorded the untested leg ("a live Moonlight re-test
joins the standing owed Moonlight item"); this report is that re-test.

The slicing ceiling now belongs to the CLIENT, threaded as open_video's
new max_slices from both planes:
- GameStream: parse x-nv-video[0].videoEncoderSlicesPerFrame into
  StreamConfig and honor it; absent/out-of-range (pre-auth input) => 1.
- punktfunk/1: new Hello cap VIDEO_CAP_MULTI_SLICE (0x80 — the byte's
  LAST free bit; the next cap needs a second byte + ABI bump).
  SessionPlan.max_slices = 32 with the bit, 1 without, applied to every
  encoder the plan opens so rebuilds can't change the wire shape. The
  desktop session client advertises it (FFmpeg/D3D11VA/Vulkan decode
  stacks are fine); Android/Apple stay off until they can decide
  per-decoder like Moonlight does — the cap is embedder-set decoder
  truth, never OR'd in by the shared pump.
- Linux direct-NVENC clamps its Phase-3 default to the ceiling
  (resolve_slices(codec, 4.min(max_slices))) and logs slices/max_slices
  in the caps-probe line; PUNKTFUNK_NVENC_SLICES stays the explicit
  operator override in both directions. Windows keeps its single-slice
  default untouched.

Also repairs the nvenc_cuda #[ignore] hardware tests: d2c46eaf added
open()'s cursor_blend param without updating them, invisible because CI
never compiles tests with the nvenc feature.

Verified on .21 (RTX 5070 Ti): pf-encode + host check/clippy clean with
nvenc; host unit suite 263/0; rtsp announce tests 7/7 incl. the new
slicesPerFrame coverage; on-hardware smokes — default e2e still 4
chunks/frame, NEW single-slice client-ceiling test clamps + disarms
chunked poll with no env involved, env escape unchanged. rustfmt clean.
Windows leg (prepare_display param) is CI-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 22:23:42 +02:00
enricobuehlerandClaude Opus 5 c04c5be224 docs: USB passthrough is a plugin now, not a recipe you assemble
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 6s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 10s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 9s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 18s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 26s
ci / web (push) Successful in 1m5s
docker / builders-arm64cross (push) Successful in 14s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 51s
ci / docs-site (push) Successful in 1m14s
ci / rust-arm64 (push) Successful in 1m18s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m14s
docker / deploy-docs (push) Successful in 31s
ci / rust (push) Successful in 7m20s
Adds a VirtualHere section to the plugins page — what it is, that both halves of VirtualHere
are yours to install and licence, that the Devices tab writes a name-based rule so it survives
the couch rebooting, and that Diagnostics is where to look when nothing happens.

States the coverage limit up front rather than letting somebody discover it: there is no
VirtualHere server for iOS or tvOS, so those clients cannot pass devices through, and nothing
on our side can change that.

Cuts the automation.md recipe from 75 lines to a pointer. It now leads with "use the plugin"
and keeps only the zero-code two-hook version for people who would rather not install one —
with its trade-offs stated instead of implied: the address is hard-coded so it breaks when the
couch reboots, and an abnormal stream end strands the device on the host. Those two failures
are exactly what the plugin exists to fix, so the reader gets to make an informed choice.

Not build-verified: docs-site does not build standalone in this checkout. Checked by hand that
Callout is in fumadocs' default MDX components with a valid `warn` type, that the JSX balances,
and that the cross-page anchor matches github-slugger of the heading.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 22:07:14 +02:00
enricobuehlerandClaude Opus 5 ef2bb56251 fix(tray): probe /login without redirects, and open the address the probe checked
ci / docs-site (push) Successful in 1m17s
apple / swift (push) Successful in 1m17s
ci / rust-arm64 (push) Successful in 1m48s
ci / web (push) Successful in 1m56s
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 6s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 9s
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 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
deb / build-publish-client-arm64 (push) Successful in 2m6s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
deb / build-publish (push) Successful in 5m3s
docker / builders-arm64cross (push) Successful in 6s
deb / build-publish-host (push) Successful in 4m14s
docker / deploy-docs (push) Successful in 27s
ci / rust (push) Successful in 6m52s
android / android (push) Successful in 7m11s
arch / build-publish (push) Failing after 7m24s
apple / screenshots (push) Successful in 14m27s
windows-host / package (push) Successful in 18m20s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m38s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m10s
Two ways the tray could misreport a healthy console.

The probe hit `/`, which is auth-gated and 302s to /login, and ureq follows
redirects by default — so TLS, `/`, and a full cold Nitro SSR of /login all had
to fit inside one 2s budget, and a console that was merely warming up read as
down. It now probes /login with redirects(0) on the agent: one round trip, and a
302 already counted as up. Neither user of that agent wants redirects; the
summary is a terminal JSON route.

The menu then opened https://localhost:<port> while the poller probed 127.0.0.1.
web-run.cmd binds HOST=0.0.0.0 — IPv4 only — and Windows resolves localhost to
::1 first, so the tray could call the console healthy and hand the browser a URL
that fails. Both are 127.0.0.1 now, which is what the Linux tray already did, so
the menu can no longer disagree with the status printed above it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:42:50 +02:00
enricobuehlerandClaude Opus 5 d9912aa795 fix(windows/update): an update puts the status tray back instead of killing it for good
Every update started from the web console killed the tray permanently. Three
pieces had to line up, and they did: the installer's StopTrays force-kills every
punktfunk-tray.exe (it is one of the files being replaced), its relaunch is a
[Run] entry flagged skipifsilent, and the in-console updater spawns the installer
with /VERYSILENT. So the tray died on every such update and waited for the next
sign-in, because the Run value is a logon trigger. Confirmed on a box whose tray
was absent while its Run key, its exe and a session signed in hours before the
update were all present.

The installer cannot fix this itself. Spawned from the SYSTEM host service, its
`runasoriginaluser` resolves to SYSTEM — so relaunching there would place a
SYSTEM-owned tray in the user's session, where it would hold the per-session
Local\PunktfunkTray mutex and block the real tray at the next sign-in. Strictly
worse than the bug.

The host does it instead, because only the host has the right token. IntentRecord
gains tray_was_running, captured before the installer is spawned (reusing the
conflicting-host scan's process snapshot rather than opening a second one), and
boot reconciliation reads it off the intent before reconcile consumes it and
relaunches through tray::start(). Restored on both terminal outcomes — a
rolled-back install killed the tray just as dead as a successful one — but never
while an apply is still in flight, where the installer may only kill it again.

The field is serde(default), so an intent written by an older host reads as false
and behaves exactly as before; covered by a test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:42:50 +02:00
enricobuehlerandClaude Opus 5 c77823d800 feat(windows/tray): punktfunk-host tray start|stop|status, so a dead tray is one command away
The status tray is a per-user, per-session GUI process with no recovery path of
its own: the HKLM Run value only fires at sign-in, and nothing in the product
ever restarts one. Anything that kills a tray — an upgrade's StopTrays, a crash —
therefore left the operator without an icon until they signed out and back in,
with no way to ask for it back.

windows/tray.rs becomes the one place that knows tray lifecycle (find, start,
stop, is-running), so the CLI and post-update reconciliation share an
implementation rather than growing two.

start() cannot simply spawn the exe, because the launch crosses a privilege
boundary in one direction only:

  - from the host service (SYSTEM) the tray must land in the active console
    session under the LOGGED-IN USER's token, which is WTSQueryUserToken +
    CreateProcessAsUserW and needs SE_TCB — SYSTEM-only;
  - from a shell already in the console session that call fails (an administrator
    does not hold SE_TCB either) and a plain spawn is both sufficient and right.

Trying the privileged path and falling back discriminates the two without
inspecting a token, and adds no unsafe to the crate. But a plain fallback from
ssh/RDP would put a tray in a session nobody can see and report success, so on
failure it consults console_session_mismatch() and refuses with an explanation
instead. stop() is graceful first (--quit lets the tray remove its own icon via
NIM_DELETE, as the uninstaller does), then forces any instance in another session.

Verified on Windows: launched from a SYSTEM scheduled task the tray comes up in
the console session owned by the interactive user, not SYSTEM; from an ssh
session (0 -> console 2) it refuses; a second start is a no-op; stop is graceful
and idempotent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:42:50 +02:00
enricobuehlerandClaude Opus 5 3bb30cb2f6 fix(windows/web-console): a bun exit no longer parks the console until the next sign-in
The PunktfunkWeb task registers RestartCount=10/PT1M, but Task Scheduler honours
restart-on-failure only when the action crashes or fails to start — never for a
plain non-zero exit. So any bun exit left the console down until the next boot or
interactive logon, with the host still streaming perfectly next to it. Seen on
glass on a 0.22.3 upgrade: `web setup` started the task one second after the new
service came up, bun exited at once (Last Run Result 0xFFFFFFFF), and the console
sat dead for hours while its box served clients — which reads to an operator as
"the host is gone".

web-run.cmd now supervises bun instead of exiting with it: restart on any exit,
indefinitely for a console that served a while and then died, but give up after
10 CONSECUTIVE fast exits so a genuinely broken install still surfaces as a failed
task. Any run lasting >= 60s resets that counter. It exits 0 rather than
respawning when the payload disappears, so an uninstall does not make it spin.

Uptime is measured with an unambiguous clock rather than parsed out of %TIME%,
which is locale-formatted (12-hour locales append " PM", many others use ',' as
the decimal separator) — the box this was found on prints 18:36:28,39.

stop_web_console() polls until :47992 is really free instead of sleeping a blind
second: both `schtasks /end` and `taskkill /F` are asynchronous, and web_setup
starts the new task immediately afterwards, where bun cannot bind a port the
corpse still holds. It re-ends the TASK halfway through, not just the listener —
with the launcher now supervising, killing bun alone would be undone by its own
restart loop.

The systemd unit had the same shape of hole: Restart=on-failure leaves a console
that exited 0 down. Now Restart=always, which an explicit `systemctl stop` still
overrides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:42:50 +02:00
enricobuehlerandClaude Fable 5 e90c5d5bcd fix(gamepad): the output-report ring absorbs haptics-rate writers (PadShm v2.2)
ci / web (push) Successful in 1m1s
windows-drivers / probe-and-proto (push) Successful in 1m3s
ci / docs-site (push) Successful in 1m16s
windows-drivers / driver-build (push) Successful in 2m15s
ci / rust-arm64 (push) Successful in 3m27s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 8s
deb / build-publish-client-arm64 (push) Successful in 2m21s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 9s
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 8s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 15s
apple / swift (push) Successful in 4m36s
deb / build-publish-host (push) Successful in 4m2s
docker / builders-arm64cross (push) Successful in 5s
docker / deploy-docs (push) Successful in 29s
arch / build-publish (push) Failing after 6m11s
deb / build-publish (push) Successful in 6m6s
android / android (push) Successful in 8m29s
ci / rust (push) Successful in 10m32s
windows-host / package (push) Successful in 16m18s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 12s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m15s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m42s
apple / screenshots (push) Successful in 20m33s
A field log (2026-07-30) showed a game driving the virtual DualSense's output
endpoint at >2 kHz sustained for tens of seconds. The 8-slot ring — sized on
the assumption that 2 kHz is double any real HID output rate — overflowed
every 4 ms poll, which force-silenced rumble for each storm's whole duration
and flooded the log at ~230 WARN lines/s (96 % of the user's 5000-line
web-console export, evicting the session history it was needed to diagnose).

Three legs, negotiated so every old/new host×driver pairing keeps working:

- pf-driver-proto: the ring grows in place 8 -> 56 slots; PadShm becomes
  exactly one page (4096 B), the hard ceiling that keeps cross-generation
  section views mappable. A new out_ring_len field carries the driver's side
  of the length negotiation. Deliberately NOT a GAMEPAD_PROTO_VERSION bump
  (that fails closed - no pad at all).
- pf-gamepad driver: picks its ring length from the host's out_ring_ver
  stamp (>= 2 + a full-size map -> 56) and echoes it before every ring_head
  bump (now a Release store), so an Acquire-observing drain always reads the
  modulo that indexed the slots it copies.
- host drain: follows the echo (0 = old driver = 8); on genuine overflow it
  now salvages the legacy latest-report slot - the freshest coalesced state -
  instead of total silence, and the per-poll overflow WARN is rate-limited to
  1 line/s per pad with a suppressed count.

Verified on the Windows CI runner (drivers workspace build + clippy -D
warnings against the WDK; 64 pf-inject tests incl. the new negotiation/
salvage/limiter tests; pf-inject clippy -D warnings) and on Linux via the CI
docker image (82 pf-inject tests). DriverVer needs no manual bump - the
installer stamps a strictly-increasing build timestamp per release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 18:36:47 +02:00
enricobuehlerandClaude Fable 5 ec675261fc feat(android): the Sony-pad USB grant is asked on connect, not found in Settings
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 21s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 13s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
ci / docs-site (push) Successful in 1m4s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 19s
ci / rust-arm64 (push) Successful in 1m21s
ci / web (push) Successful in 1m33s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 1m6s
docker / builders-arm64cross (push) Successful in 6s
android / android (push) Canceled after 1m55s
ci / rust (push) Canceled after 1m56s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 47s
docker / deploy-docs (push) Canceled after 0s
On-glass feedback: burying the grant in the Controllers screen made the
user go find it. Now MainActivity asks the moment a Sony pad appears — a
fresh attach while the app is open, or the app foregrounding with one
already plugged in — once per attach (a deny doesn't re-nag; the
Controllers card's button stays as the re-ask). Nothing starts on the
grant: an uncaptured pad is an ordinary InputDevice at menu time, so the
grant is simply recorded and the next stream's capture engages silently.
The grant broadcast is shared with the Controllers card so an open card
refreshes from either dialog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 18:34:57 +02:00
enricobuehlerandClaude Fable 5 1984ddb942 feat(android): a USB Sony pad is captured — rumble, adaptive triggers, lightbar, gyro
A DualSense on a phone had rumble only where the kernel exposed force
feedback, and adaptive triggers / lightbar / player LEDs nowhere — Android
has no platform API for any of them, and Bluetooth offers no raw path
(L2CAP is LE-only; hidraw is root-sealed — Sony's own Remote Play declares
Android triggers unsupported). Claiming the pad's HID interface over USB is
the one unrooted route, so that is what the client now does.

- HidUsbLink: the device-agnostic half of Sc2UsbLink (claim, multiplexed
  UsbRequest loop, newest-wins write queue, signalled-unplug discipline),
  parameterized by device match / interface filter / keep-alive. Sc2UsbLink
  keeps only its SC2 specifics (Puck interfaces 2..5, lizard refresh).
- GamepadFeedback.PadFeedbackSink: 0xCA rumble + 0xCD Led/PlayerLeds/
  Trigger now route to a capture link that owns the pad BEFORE the
  InputDevice vibrator/lights paths — Trigger stops being log-and-drop.
- DsDevice: the byte-exact inverse of the host's dualsense_proto /
  dualshock4_proto — input report 0x01 parse (buttons/sticks/triggers,
  gyro+accel, both touch points; Edge FN/BACK → wire paddles) and output
  builders (DS5 0x02 valid-flag-selective incl. the 11-byte trigger blocks
  and the lightbar-animation release; DS4 0x05 as composed full-state
  writes). Covered by DsDeviceTest (pure JVM).
- DsCapture: stream-mode capture for DualSense / Edge / DS4 — lazy wire
  slot on the first parsed report, typed mirror (exit chord included),
  touch normalized onto the rich plane + per-report motion, feedback
  rendering with a rumble backstop (a USB pad holds its level, so a
  stalled poll thread self-terminates via a scheduled zero-write) and a
  teardown motor-stop over EP0. The claim releases the pad's InputDevice
  slot itself so the wire index hands over deterministically; uncaptured
  (toggle off / permission denied / Bluetooth) the pad stays on the
  ordinary InputDevice path.
- Rich-input shims: nativeSendPadTouch / nativeSendPadMotion →
  RichInput::Touchpad / Motion — the plane the desktop and Apple clients
  already feed; Android pads gain gyro + touchpad on the virtual pad.
- Settings: "DualSense / DualShock passthrough (USB)" (ds_capture, opt-out
  like the SC2 toggle); Controllers screen card with capture status and a
  front-loaded USB grant so streams start without the permission dialog.

The host needs nothing: the DS5/DS4 backends already consume the typed +
rich planes and already emit every feedback event rendered here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 18:34:57 +02:00
enricobuehlerandClaude Fable 5 696386dee7 fix(host/windows): clippy — SAFETY comments on the WinTrust FFI, explicit truncate(false), sort_by_key
deb / build-publish-client-arm64 (push) Failing after 4s
ci / docs-site (push) Successful in 1m18s
ci / web (push) Successful in 2m12s
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 13s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 11s
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 11s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 56s
deb / build-publish-host (push) Successful in 4m28s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 31s
docker / builders-arm64cross (push) Successful in 11s
ci / rust-arm64 (push) Successful in 5m49s
docker / deploy-docs (push) Successful in 31s
android / android (push) Successful in 6m54s
deb / build-publish (push) Successful in 5m0s
apple / swift (push) Successful in 5m6s
ci / rust (push) Successful in 6m13s
arch / build-publish (push) Failing after 7m37s
windows-host / package (push) Successful in 15m8s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 41s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m40s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m39s
apple / screenshots (push) Successful in 21m28s
windows-host.yml's clippy sees what the Linux gate structurally cannot (cfg(windows)
code); verified with the same clippy line on the runner this time, not just cargo check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 17:42:25 +02:00
enricobuehlerandClaude Fable 5 5790a3e334 docs(release): v0.22.3 notes — the Updates card and one-click updating join the installer fixes
ci / web (push) Successful in 2m25s
ci / rust-arm64 (push) Successful in 2m53s
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 13s
ci / docs-site (push) Successful in 1m6s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 24s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 33s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 13s
docker / builders-arm64cross (push) Successful in 9s
docker / deploy-docs (push) Successful in 33s
ci / rust (push) Successful in 7m2s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 17:23:54 +02:00
enricobuehlerandClaude Fable 5 3b1485e2a1 fix(plugins/windows): the runner can read its own bundle, so it actually starts
ci / rust (push) Canceled after 27s
ci / rust-arm64 (push) Canceled after 28s
ci / web (push) Canceled after 28s
ci / docs-site (push) Canceled after 28s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (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/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (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
apple / swift (push) Successful in 4m45s
android / android (push) Successful in 4m58s
deb / build-publish-host (push) Successful in 4m14s
deb / build-publish (push) Successful in 5m51s
deb / build-publish-client-arm64 (push) Successful in 1m19s
arch / build-publish (push) Failing after 6m54s
windows-host / package (push) Failing after 10m59s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
apple / screenshots (push) Canceled after 14m9s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m17s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 14m33s
Enabling the plugin runner did everything right and then nothing happened.
The task went Ready with LastTaskResult 1 within a second of every start, and
the console kept showing it enabled-but-not-running with nothing to explain
why. Task Scheduler discards the action's output, so the reason never
surfaced anywhere:

  error: EPERM reading "C:\Program Files\punktfunk\scripting\runner-cli.js"

This file already knows why. RUNNER_UNIT_DIRS carries the note that bun opens
the files it loads asking for FILE_WRITE_ATTRIBUTES on top of read, so a plain
(RX) grant makes them die with EPERM — found on glass when the plugin and
script directories were fixed. The runner's own entry script was missed. It
lives in {app}\scripting, which carries only Users:(RX), and LocalService
reaches that through Authenticated Users, so bun could never open the one file
it was started to run. The runner has not been able to start since it moved
off SYSTEM, which is a principal that has full control everywhere and so never
met this.

So {app}\scripting now gets the same (OI)(CI)(RX,WA) the unit dirs get, at
enable, revoked at disable like every other grant here. WA moves timestamps
and the read-only bit and cannot touch content, so the three-way split the
module maintains still holds: code read-only, secrets read-only, only
plugin-state writable. A plugin still cannot rewrite the runner.

Measured on glass on .173, same box, same task, only the ACE differing:
without it Ready/lastResult=1 and no runner process; with it Running, a live
runner, and GET /store/runtime answering running:true — which is exactly what
the console reads. Also checked what bun does with the access: nothing. It
opens the entry for write and never writes, and the directory is byte-identical
afterwards.

Windows-only code, so it is rustfmt-clean and type-reviewed but NOT
compile-verified here — the Windows CI job is the gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 17:23:16 +02:00
enricobuehlerandClaude Fable 5 4a540bddc8 feat(host/deck): one-click source rebuild — the update.sh run as a transient user unit
ci / rust-arm64 (push) Successful in 1m36s
ci / docs-site (push) Successful in 1m28s
ci / web (push) Successful in 2m25s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 9s
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 6s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 1m13s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
deb / build-publish-client-arm64 (push) Successful in 2m27s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m47s
deb / build-publish-host (push) Successful in 5m53s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 7s
android / android (push) Successful in 7m2s
docker / builders-arm64cross (push) Successful in 11s
arch / build-publish (push) Failing after 7m33s
ci / rust (push) Successful in 7m35s
docker / deploy-docs (push) Successful in 39s
windows-host / package (push) Failing after 11m1s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
deb / build-publish (push) Successful in 6m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m4s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m21s
apple / swift (push) Successful in 4m58s
apple / screenshots (push) Successful in 21m32s
The U3.1 leg of planning:host-update-from-web-console.md. The Deck's on-device install
gets the Update-now button with no opt-in (user-owned, no root): update.sh --pull runs
under systemd-run --user so the script's own restart of punktfunk-host can't kill it
mid-build (a child in our cgroup would die with us). Outcome without version equality —
which a source rebuild can't promise: a failed build leaves the host alive to report it
(unit watched to failure, log attached); a successful one restarts us, and the new
source_build intent flag makes intent-present-at-boot itself the success signal (the
script only restarts the host after a successful install). The console stops treating
a live long build as a timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 16:28:40 +02:00
enricobuehlerandClaude Fable 5 51d5f6cb29 feat(host/windows): boot-loop auto-rollback — the supervisor re-runs the cached previous installer
ci / docs-site (push) Successful in 1m11s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 7s
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 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
ci / web (push) Successful in 1m58s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 1m8s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 42s
deb / build-publish-client-arm64 (push) Successful in 3m19s
deb / build-publish-host (push) Successful in 6m17s
ci / rust-arm64 (push) Successful in 6m58s
android / android (push) Successful in 4m51s
docker / builders-arm64cross (push) Successful in 12s
windows-host / package (push) Failing after 5m39s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
docker / deploy-docs (push) Successful in 1m58s
arch / build-publish (push) Failing after 8m14s
apple / swift (push) Successful in 4m59s
apple / screenshots (push) Canceled after 0s
deb / build-publish (push) Successful in 11m51s
ci / rust (push) Canceled after 13m5s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 9m38s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 9m11s
The U3.2 leg of planning:host-update-from-web-console.md. The service worker loop —
the one piece that survives an upgrade — detects a fresh update-intent whose target
version is the crash-looping child (≥3 rapid restarts, intent <30 min), then exactly
once: picks the newest cached non-target installer, requires a valid Authenticode
signature, writes a rolled-back result record for the console, deletes the intent,
and spawns the installer silently. No cached installer or a failing signature degrade
to today's backoff loop with a loud log — never a brick, never an installer loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 16:12:22 +02:00
enricobuehlerandClaude Fable 5 3e21398c16 fix(lock): pf-update rides the 0.22.3 workspace bump
audit / bun-audit (plugin-kit) (push) Successful in 22s
audit / cargo-audit (push) Successful in 37s
audit / bun-audit (sdk) (push) Successful in 21s
audit / pnpm-audit (push) Successful in 11s
audit / docs-site-audit (push) Successful in 22s
ci / web (push) Successful in 1m52s
ci / docs-site (push) Successful in 2m4s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m58s
android / android (push) Successful in 5m38s
audit / license-gate (push) Successful in 5m0s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m11s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 48s
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 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 4s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 13s
apple / swift (push) Canceled after 0s
arch / build-publish (push) Failing after 7m37s
apple / screenshots (push) Canceled after 0s
ci / rust (push) Canceled after 6m46s
ci / rust-arm64 (push) Canceled after 6m42s
deb / build-publish (push) Canceled after 4m47s
deb / build-publish-host (push) Canceled after 4m17s
deb / build-publish-client-arm64 (push) Canceled after 2m22s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 29s
docker / deploy-docs (push) Canceled after 0s
audit / bun-audit (web) (push) Successful in 24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 28s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m3s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 28s
windows-host / package (push) Canceled after 6s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m31s
flatpak / build-publish (push) Successful in 10m8s
release / apple (push) Successful in 26m17s
The U2 branch minted the lock entry at 0.22.2 and the rebase onto the version bump
left it stale — --locked builds fail until it matches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 16:04:17 +02:00
enricobuehlerandClaude Fable 5 239c69fd71 feat(web,docs): Linux one-click in the card — opt-in hint, staged and nothing-newer outcomes
audit / bun-audit (sdk) (push) Successful in 18s
audit / bun-audit (web) (push) Successful in 18s
audit / docs-site-audit (push) Successful in 23s
audit / pnpm-audit (push) Successful in 12s
audit / cargo-audit (push) Successful in 37s
audit / bun-audit (plugin-kit) (push) Successful in 1m14s
arch / build-publish (push) Failing after 1m17s
ci / rust-arm64 (push) Failing after 44s
ci / web (push) Successful in 1m5s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 21s
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 5s
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 7s
ci / docs-site (push) Successful in 2m18s
deb / build-publish-host (push) Failing after 2m15s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 53s
deb / build-publish (push) Failing after 3m43s
audit / license-gate (push) Successful in 4m47s
deb / build-publish-client-arm64 (push) Successful in 3m52s
docker / builders-arm64cross (push) Successful in 14s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 2m49s
ci / rust (push) Failing after 3m6s
docker / deploy-docs (push) Successful in 55s
windows-host / package (push) Failing after 6m27s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
android / android (push) Successful in 8m21s
apple / swift (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
flatpak / build-publish (push) Successful in 4m34s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 4m19s
release / apple (push) Canceled after 57s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 4m45s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 2m26s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
The apply panel serves staged kinds too; notify mode shows the group-join command when
the helper is installed but not yet enabled; outcomes render 'staged — reboot to
finish' and 'your package source had nothing newer yet' distinctly. Docs: the Linux
opt-in section (why it's a group, what the grant bounds, the pacman stance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 15:55:27 +02:00
enricobuehlerandClaude Fable 5 06a249e49f feat(host/linux): opt-in one-click updates — the pf-update root helper, group-scoped polkit grant, per-PM legs
The U2 leg of planning:host-update-from-web-console.md. A new dep-free root helper
(crates/pf-update) runs the distro package manager against the INSTALLED punktfunk
packages — apt (index refresh scoped to our list when present), dnf, rpm-ostree
(single-transaction re-resolve, reported staged), sysext (the proven signed-feed
updater), pacman only behind the explicit PACMAN_FULL_SYSUPGRADE opt-in — then the
run-the-binary gate, then a root-written result record. Zero attacker-influenceable
parameters end to end: fixed ExecStart oneshot (punktfunk-update.service), polkit rule
scoped to that one unit's start verb for the shipped-EMPTY punktfunk-update group
(joining it is the auditable opt-in; every postinst creates it, none populate it).
The host starts the unit, interprets the record (staged / nothing-newer-yet / changed),
and crosses its own restart on the same intent/reconcile machinery as the Windows leg.
Status now reports staged results and the opt-in hint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 15:55:27 +02:00
enricobuehlerandClaude Fable 5 bb1f93d90e feat(web,docs): the Update-now flow — password re-entry at the BFF, restart-tolerant progress, same-origin hardening
apply.post.ts intercepts the one proxied route that restarts the host: the console
password is re-verified per apply (login throttle shared, stripped before forwarding) so
a 7-day cookie alone can't do it. New Sec-Fetch-Site same-origin check on every mutating
request (login CSRF included). The card's apply flow: confirm dialog → live-session
force escalation → download/verify/restart progress rendered from the last snapshot
while polls fail (the host and, on Windows, this very server restart mid-flow) →
durable success/failure from last_result. Docs: the one-click section + kill switch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 15:55:27 +02:00
enricobuehlerandClaude Fable 5 bb48225414 feat(host/windows): one-click update apply — verified installer download, intent/reconcile, POST /update/apply
The U1 leg of planning:host-update-from-web-console.md. The apply request carries no
version/url/channel (the zero-parameter invariant): the host installs exactly what its
Ed25519-verified manifest announced. Pipeline: staged download (resume + disk preflight)
→ manifest sha256 → Authenticode (valid signature, untrusted root tolerated while the
cert is self-signed; leaf sha256 pinned via the signed manifest, extracted from the SAME
WinVerifyTrust state) → intent record → detached CREATE_BREAKAWAY_FROM_JOB spawn of the
winget-blessed silent flags. The installer kills this process by design; boot-time
reconciliation reports the outcome (update.applied event / a durable failure with the
installer log path) across the restart. Session guard (force to override),
PUNKTFUNK_UPDATE_APPLY=0 kill switch, single-flight via job + fresh-intent detection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 15:55:27 +02:00
enricobuehler 73e1224345 fix(ci/update): an empty AUTHENTICODE_SHA256 broke the manifest build — jq -R needs one input line
ci / web (push) Successful in 1m5s
ci / docs-site (push) Successful in 2m30s
ci / rust-arm64 (push) Successful in 3m40s
android / android (push) Successful in 4m35s
decky / build-publish (push) Successful in 47s
ci / rust (push) Successful in 6m26s
arch / build-publish (push) Successful in 8m54s
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 23s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 12s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m10s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Failing after 12m27s
docker / deploy-docs (push) Skipped
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 10s
docker / builders-arm64cross (push) Successful in 11s
apple / swift (push) Successful in 4m45s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m47s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m15s
deb / build-publish-host (push) Successful in 4m45s
deb / build-publish-client-arm64 (push) Successful in 1m25s
apple / screenshots (push) Canceled after 7m43s
deb / build-publish (push) Canceled after 3m20s
printf '%s' hands jq -R no line at all for an empty value; its empty output made
--argjson invalid. Seen on the first live canary-manifest run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit a12c8bc99d41c4f7b8e4c426d560171b2b12dd5e)
2026-07-30 15:23:52 +02:00
enricobuehlerandClaude Fable 5 1c836afc02 chore(release): bump workspace version to 0.22.3
ci / web (push) Successful in 2m4s
ci / docs-site (push) Successful in 2m25s
android-screenshots / screenshots (push) Successful in 2m12s
ci / rust-arm64 (push) Successful in 7m8s
apple / swift (push) Successful in 4m38s
decky / build-publish (push) Successful in 52s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m44s
deb / build-publish-client-arm64 (push) Successful in 1m32s
arch / build-publish (push) Successful in 10m25s
linux-client-screenshots / screenshots (push) Successful in 3m16s
audit / bun-audit (plugin-kit) (push) Successful in 28s
audit / bun-audit (sdk) (push) Successful in 27s
audit / bun-audit (web) (push) Successful in 26s
audit / docs-site-audit (push) Successful in 24s
audit / pnpm-audit (push) Successful in 15s
audit / license-gate (push) Successful in 5m11s
sbom / sbom (push) Successful in 35s
android / android (push) Successful in 7m33s
web-screenshots / screenshots (push) Successful in 4m12s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 18s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 11s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 13s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 12s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 13s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 41s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m36s
audit / cargo-audit (push) Successful in 39s
docker / deploy-docs (push) Successful in 42s
docker / builders-arm64cross (push) Successful in 20s
flatpak / build-publish (push) Successful in 6m50s
deb / build-publish-host (push) Successful in 5m19s
deb / build-publish (push) Successful in 6m58s
ci / rust (push) Successful in 17m25s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m37s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m0s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 3m3s
apple / screenshots (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m46s
release / apple (push) Successful in 25m45s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m26s
windows-host / package (push) Successful in 10m58s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Failing after 14s
Windows-installer patch release. 0.22.1 and 0.22.2 shipped without the web
console in them at all — confirmed from the tag builds' own logs, where the
console build step is `skipped` on a cache hit and the job is still green —
which also removed the only code that stopped bun before the installer
replaced it, hence the "DeleteFile failed; code 5" dialog. Both fixed here,
along with updates no longer disabling an operator's plugin runner.

Lock touched for the 30 workspace members only. `base64` stays at 0.22.1 and
the eight gtk-rs crates stay at 0.22.0 — they share our version space and a
blanket sed would move them to versions that do not exist. Diff is
versions-only, 30 insertions and 30 deletions; `cargo metadata --locked`
resolves; `cargo fmt --all --check` clean in both the main and the
packaging/windows/drivers workspaces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v0.22.3
2026-07-30 14:36:46 +02:00
enricobuehlerandClaude Fable 5 beefeaf9d7 fix(ci/windows-host): refuse to pack an installer that is missing a payload
Every payload the job bundles is now asserted before packing: the console, the
bun runtime, the plugin runner, the FFmpeg DLLs and VB-CABLE. Each is optional
to pack-host-installer.ps1 — right for a local debug pack, and the reason
0.22.1 and 0.22.2 shipped with no web console: one unset variable omitted it
behind a single line of log and the build stayed green.

CI knows it bundles all five, so a missing input belongs here as a failure
rather than downstream as a quietly smaller installer. FFmpeg is the one that
would hurt most and was silent too: an amf-qsv host link-imports avcodec, so
an installer missing those DLLs ships a host that cannot start at all, and
FFMPEG_DIR is a fallback to a provisioned path that nothing verified.

The shape is borrowed from the packer's own VB-CABLE check, which already
throws on a supplied-but-empty dir "instead of silently shipping an installer
without the virtual mic - exactly the field regression this bundling fixes".
Same lesson, applied to the rest.

Both paths were exercised on the Windows runner: all five unset fails with
exit 1 naming each one, all five present passes with exit 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 14:36:46 +02:00
enricobuehlerandClaude Fable 5 5b6fe7882a fix(ci/windows-host): a cached console no longer ships an installer without one
WEB_OUTPUT_DIR was exported on the last line of "Build + smoke-boot web
console", a step that is skipped on a cache hit. So the second and every
later build with an unchanged web/ and sdk/ left it unset — and
pack-host-installer.ps1 reads an unset WEB_OUTPUT_DIR as "don't bundle the
console" and says so in one line of log before carrying on happily.

The installers that fell out of that have no {app}\web at all: no
web-run.cmd, so `web setup` bails with "web launcher missing" before it
registers anything, so there is no PunktfunkWeb task, no console, and no
firewall rule for 47992. A user is left with a working host, a tray that says
"Open web console (not responding)" for ever, and nothing to reinstall their
way out of, because every rebuild reproduces it.

It also explains the bun.exe lock reported separately. bun.exe ships under
WithWeb OR WithScripting, but the pre-copy stop was #ifdef WithWeb — so in a
console-less installer bun still shipped while the only code that stopped bun
was compiled out, and replacing a running bun.exe is the "DeleteFile failed;
code 5" modal.

Exporting the variable from its own unconditional step fixes it. The throw
alongside is the actual lesson: a missing web\.output now fails the job
instead of silently redefining what the installer is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 14:36:46 +02:00
enricobuehlerandClaude Fable 5 c64ada5649 fix(windows/installer): an update stops every bun before it replaces bun.exe
Updating to 0.22.1 could die on a modal: "C:\Program Files\punktfunk\bun\
bun.exe — DeleteFile failed; code 5. Access denied." Windows will not delete a
running executable, so that message means a bun was still alive when the copy
reached it, and the installer had no way to recover.

Two things run that bun, and neither is a child of the host service, so
stopping the service — which the installer does correctly wait for — never
touched either. Both are Task Scheduler tasks: PunktfunkWeb (SYSTEM, the
console) and PunktfunkScripting (LocalService, the plugin runner). The only
pre-copy stop we had looked for exactly one of them, and found processes by
task name or by who was listening on 47992/3000. The plugin runner matched
neither clause: different task, and it listens on no port at all. So anyone
who had run `plugins enable` held bun.exe mapped through the whole install and
failed this way on every single update.

The web console could lose the race too. Stop-ScheduledTask returns once
termination has been *requested* and Stop-Process is TerminateProcess, so the
old code went straight from asking to copying; the Rust twin of that routine
has always ended with a deliberate one-second settle. And neither task was
disabled for the duration, while both carry restart-on-failure (web ten times
a minute apart, scripting 999) and the web task also has a logon trigger — so
a force-kill invited a respawn into the middle of a copy that takes well over
a minute at lzma2/max.

StopBunRuntimes now disables both tasks before stopping them, kills any bun
whose image lives under the install dir — by path, so a developer's own bun
survives — keeps the old port sweep for pre-bun installs that ran node on
3000, and then waits until both are actually gone rather than assuming.
bun.exe also gains restartreplace, so if some bun still escapes all of that,
the file lands on the next restart instead of dead-ending the install.

Disabling a task is not free, though: unlike a stopped one it does not come
back at the next boot, so an install that aborts anywhere after that point
would take the console down for good. Hence two restores — a [Run] entry for
the normal flow, and DeinitializeSetup, which Inno calls even when the user
cancels. Both re-enable only what was enabled before the copy, and enabling an
enabled task does nothing, so the pair is idempotent.

Putting things back also turned up a second bug worth naming: the scripting
entry re-registers its task and then unconditionally disables it, which is
right on a first install and has silently switched an operator's plugin runner
off on every upgrade since. The restore runs after both re-registrations and
honours the prior state, so a fresh install still leaves the opt-in runner off.

The Pascal and the PowerShell were both compiled and run on the Windows CI
runner (ISCC, plus the query halves of each command) — but the install path
itself is not yet exercised on glass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 14:36:46 +02:00
enricobuehlerandClaude Fable 5 b275e6d34c feat(web,docs): the Updates card — version, channel, install kind, and the exact update command
android / android (push) Canceled after 1m54s
apple / swift (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
ci / docs-site (push) Successful in 1m15s
arch / build-publish (push) Canceled after 1m50s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 14s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 14s
ci / rust (push) Canceled after 59s
ci / rust-arm64 (push) Canceled after 2m31s
ci / web (push) Canceled after 0s
deb / build-publish (push) Canceled after 18s
deb / build-publish-host (push) Canceled after 19s
deb / build-publish-client-arm64 (push) Canceled after 12s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 1m15s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 36s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 2m28s
windows-host / package (push) Canceled after 0s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
decky / build-publish (push) Successful in 50s
Notify-only (U0): polls /update/status (which keeps the host's manifest cache
warm), Check-now with the 30s limit surfaced, release-notes link, stale-feed and
check-disabled states, copyable per-install-kind command. en+de strings; docs-site
'Updating the Host' page wired into the install section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 14:34:47 +02:00