f2e1b9872c3ec9ecb810604f49ed7ff7ec115bec
2245
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f2e1b9872c |
fix(web): four "fixes" from this branch that did not actually fix anything
A verification pass re-read every finding from the original sweep against the code on this branch rather than against the commit messages. It found that four of them were still broken, two because the edit I made was inert. Commit messages claim; code decides. - **The Storybook typecheck was never on.** `tsconfig.json` listed `.storybook` as a bare directory name, and tsc silently skips dot-prefixed directories in that form — so the entry typechecked nothing at all. Proved it by planting `export const __probe: string = 1` in `.storybook/preview.tsx` and watching `bun run lint` pass. `.storybook/**/*` is what actually pulls it in; the same probe now fails as it should. - **The Moonlight stale-PIN reset was a no-op.** `submit.reset()` sat at the top of `onSubmit`, immediately before `submit.mutate(...)` — which moves the status to pending in the same update, so it cleared a flag that was already changing. The green "PIN sent" note therefore still greeted the next pairing attempt over an empty PIN box. It now resets on the transition that actually matters: `pin_pending` going false → true. - **The session⇄game controls had the enforcement flag inverted**, and I never touched it. `enforced.length === 0 || …` reads an EMPTY list as "this build enforces everything", when the contract says the opposite in as many words: "Empty on a platform with no launch path (macOS), so the console can say so instead of offering a switch that does nothing". On exactly the platform the flag exists for, every control stayed live and reported success for an axis the host would never act on. Absent still means "assume it acts" — that is the compatible reading for an older host, and a different case from present-empty. - **Logout stopped revoking after a restart.** The epoch was a module-level counter starting at 1, so it revoked within one process run and then reset — and since the seal key derives from the stable mgmt token, a cookie captured before a restart unsealed fine and was accepted again for the rest of its 7-day TTL. One service restart undid the whole fix. It persists next to the host's config now. Verified: log out, restart the console, the captured cookie still 401s, a fresh login still works. Two more the pass rated as partial, both worth closing: - The plugin-UI response filter was a denylist of four header names, so `Clear-Site-Data` sailed through — a plugin error page could wipe `pf_session` and sign the operator out of the console, on our own origin, because the iframe is same-origin by design. It is an allowlist now; a plugin-supplied CSP, `X-Frame-Options` or CORS header no longer speaks for us either. - A half-configured TLS setup now refuses to start instead of logging a warning and serving anyway. Neither shape can work — one path missing puts the login password on the LAN in the clear, and PUNKTFUNK_UI_SECURE without TLS marks the cookie Secure so the browser drops it and login can never stick. Exiting with a reason beats a console that looks fine and is not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dc57aa653c |
feat(web): the console can say what just happened, and hand a phone the way in
**Recent activity.** The console could describe the present — a status snapshot — but never the recent past. A client that connected and left while you were on another page left no trace anywhere you could look, and the host's own log is a developer artifact rather than a narrative. The event stream was already open for cache invalidation, so a feed costs one ring buffer next to it: every frame is recorded, labelled per kind, and rendered newest-first on the dashboard. Deliberately in-memory and bounded to 200. It starts empty on a page load and fills as things happen, which is the honest shape for a live tail — an audit trail would need the host to keep one, and pretending otherwise would be worse than not having it. **Connect a device.** The console knew the host's address and identity all along and never offered either in a form you could hand to a phone: pairing meant reading an IP off the Host page and retyping it on a couch. There is a card now with the address and a `punktfunk://connect/<uniqueid>` deep link, both copyable — the link is the shipped client grammar (clients/shared/deeplink-vectors.json), so an installed client opens straight onto this host. No QR: rendering one needs an encoder we do not bundle, and a wrong QR is worse than none. **Installable.** A web manifest and the theme/apple meta tags, so the console can live on a phone's home screen — which is where it is used from as often as from a desk. No service worker on purpose: an offline shell for a console whose every screen is live host state would only ever show stale numbers convincingly. The manifest is reachable without a session (install needs it, and it says nothing the login page doesn't); /api stays gated, verified. Verified in a browser: three host-emitted events appear in the feed with the right labels, the deep link renders and copies as `punktfunk://connect/abc123`, and the manifest serves 200 as application/manifest+json while /api/v1/host still answers 401. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b8f603c8a1 |
feat(web): the numbers behind "it's slow to start" and a way to clean up after a plugin
Three things the host already reports and the console never showed.
**Stream diagnostics.** `RuntimeStatus.stream` has carried the session bring-up
time, the last mid-stream resize cost, the client's FEC parity floor and the
packet size for as long as the endpoint has existed, and the dashboard showed
none of them. So "it takes ages to start" and "it hitches when I change
resolution" had no number attached anywhere in the console — you had to take a
stats capture to see a value the status endpoint was already returning. The two
timings are native-plane only and null until the first frame lands, so each
appears once it means something.
**Loss and FEC recovery while the capture runs.** The health chart existed but
only in the saved-recording view, which is backwards: dropped frames and FEC
recovery are what you watch a live capture for. It now sits under the latency and
throughput charts on the live card, keeping the GameStream caveat (only `frames`
is instrumented on that plane).
**Provider-owned library entries.** A plugin can sync entries into the library,
and the host then refuses to edit or delete them one at a time — correct, and
completely opaque once the plugin is gone: its games sit in the library with no
console-side way to remove them. `DELETE /library/provider/{provider}` is the
documented clean-uninstall path and nothing called it. There is a card now that
names each provider, counts what it owns, filters the grid to it, and removes its
entries in one go.
Also: the dashboard's PIN tile really does say "Waiting"/"None" now. The earlier
commit added the strings but the edit that was supposed to use them silently did
not apply, so the tile still rendered a bare "●". Caught by auditing every
message key for a call site — the other 543 are wired.
Verified in a browser: the providers card shows, counts, and filtering hides
non-provider entries.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
0751265105 |
fix(web): editing a library entry warns before it wipes what the console cannot see
`PUT /library/custom/{id}` replaces the whole entry — the host assigns
`slot.prep = input.prep` and `slot.detect = input.detect` outright
(library/custom.rs). But `GET /library` returns a `GameEntry`, which carries
neither field, so the console builds its payload from a read model that has
already lost them. Editing a title to fix a typo silently cleared any prep/undo
commands and detection hints the entry had.
The console cannot round-trip what the read API will not tell it, so this is a
warning, not a fix: the edit form now says plainly that saving replaces the entry
and that anything configured outside the console will be cleared. The actual fix
is host-side — expose `detect` and `prep` on the library read model — and is
noted in the code where it belongs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
b9b0df349d |
fix(web): the charts stop lying about time, and logging out logs you out
The stats charts drew every sample as an evenly-spaced slot, because recharts defaults to a category axis. A capture that idled for two minutes rendered that gap as a single step — so the one view you open specifically to find where the time went was the view least able to show it. All three charts use a numeric time axis now, so the spacing is the elapsed time. They also joined samples across a session boundary into one continuous line, implying a continuity that never existed: the stream stopped and somebody else started a new one. A capture is split at each `session_id` change now. And the live card plotted the whole capture-so-far every 2 s, re-serialising and re-plotting an unbounded series for a capture left running all evening; it plots a bounded tail and says so, with the full series still in the saved recording. Logging out only deleted the browser's copy of the cookie. The session is stateless, so a value captured beforehand — a shared machine, a shell history, a TLS-inspecting proxy — stayed valid for its full 7-day TTL and there was nothing the operator could do about it. Sessions carry an epoch now and logging out bumps it, which invalidates every cookie issued so far. Verified end to end: a captured cookie works, survives nothing across a logout, and a fresh login still works. The rest: - The update card could not show its own timeout warning. It was suppressed by a `job` field read from the last snapshot — which, when the host has gone away mid-job, is exactly the case the warning exists for. Nothing ever cleared the applying state either, so the card waited forever with no way out; there is a button now. "Check now" also surfaces the host's 429 instead of looking dead. - A running install survives a reload: the job id lived only in component state, so refreshing lost sight of an install that was still running while the Install buttons stayed armed against a host that answers 409. The host keeps the list — ask it. - An all-sources-failed catalog said "no plugins available". That is a successful request carrying nothing, not an empty store; it names the sources that failed. - The Installed tab rendered "vundefined" for a plugin with no recorded version (nullable in the contract, typed required here). - The Displays "In effect" badges were computed from the local draft, so they restated the operator's unsaved edits back to them as though the host had adopted them. They read the API's `effective` now. A failed background poll no longer replaces a form someone is editing, and leaving the page with unsaved edits prompts — the old `beforeunload` guard never fired for in-app navigation, which is how you actually leave. - Enter or Space on a preset's rename/update/delete icon applied the preset instead of running the action: keydown bubbled to the card. - Dates follow the console's locale, not the browser's. The dashboard's PIN-pending tile says "Waiting"/"None" instead of "●"/"—". - The Bun entry warns when TLS is half-configured, or when PUNKTFUNK_UI_SECURE is set without it — both of which silently break login. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
de17ceb8f8 |
fix(web): a newly installed plugin shows up on its own, and the display form can be used without a mouse
Installing a plugin left the sidebar unchanged until a reload. The reason is timing, not caching: the host restarts the scripting runner AFTER the job reports done, and the plugin only registers its UI once that comes back — several seconds later, by which point the one-shot invalidation had already run and found the old list. The nav then waited out the 30 s idle poll, which in practice meant "until I reloaded". Anything that changes the installed set now switches the directory to a 2 s poll for a minute, so the entry lands about a second after the plugin actually comes up. Measured end to end in a browser: 29 s → 7 s, with the plugin registering at 6 s. The plugin entries also never animated. They are rendered outside the `motion.nav` that carries the variants and the stagger, so they inherited neither and simply appeared — most visibly in exactly the case above, where one shows up in a nav that is already on screen. They get their own animation container now, matching the main nav. (A motion-wrapped div around the link, not `motion(Link)`, which erases TanStack's typed `params`.) The accessibility pass on the display form, where the console's densest controls live: - The Custom block's numeric inputs had a `<label>` with no `htmlFor` next to an `<input>` with no `id`, which labels nothing at all — a screen reader announced them as unnamed spin buttons. Single controls are paired properly now; the button groups became real `<fieldset>`/`<legend>`, which is what they are. - Every option group signalled its active choice with fill colour alone. They carry `aria-pressed` now, so the state is available to assistive tech and not only to people who can compare two button variants. - `QueryState`'s error branch is a live region, so a query that fails announces the failure instead of silently swapping one region for another. - Motion honours `prefers-reduced-motion` instead of overriding it. - `<html lang>` follows the locale instead of claiming "en" while the app renders German. Verified: switching to de flips the attribute. - "Close menu", "Language" and "Loading" went through the message catalogue. Also: ten dead message keys removed (a whole removed Clients page and the old Settings token field), and the README no longer tells operators to set the management token under "Settings → API token" — that field is gone and the token has been server-side only for some time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9e505aba41 |
fix(web): the console stops swallowing the host's answer when it says no
The host writes genuinely useful refusals — "entry is owned by provider `x`, update it through its reconcile" — and a dozen call sites threw them away. The pattern was always one of two: a mutation whose `error` nothing rendered, or an `await mutateAsync(...)` with no catch, which additionally produced an unhandled rejection. Either way the operator clicked, nothing visible happened, and the thing they asked for silently hadn't. Fixed at each site, with the host's own message shown where there is one: - Adding or editing a library entry kept the form open and said why, instead of closing it as if it had saved and taking the typing with it. Deleting one reports the refusal rather than leaving the card sitting there. - The GPU preference, capture start/stop, recording delete and download, and the dashboard's stop-session / request-keyframe / end-game all report failure. The failed capture STOP is the one that mattered most: it is "stop & save", so a swallowed error meant minutes of recording vanished with nothing on screen. - The recordings Download had a comment claiming the detail view surfaces its errors. It only does that for the selected row, and Download is on every row. Two related fixes in the same area: - `apiFetch` no longer navigates to /login synchronously from inside whichever call noticed a 401 — very often a background poll the user never started. Tearing the page down mid-render took unsaved editing state with it, which the Displays page explicitly models. It defers a beat and coalesces, so a burst of parallel 401s schedules one navigation. - The plugin liveness probe treated the auth gate's 302 → /login → 200 HTML as a healthy plugin, and rendered the console's own login page inside the plugin's iframe. It also gave up permanently on the first failed probe, so the runner restart at the end of every install threw away whatever was open in another plugin. It rejects the redirect and keeps probing on a slower beat while down. `apiErrorMessage` moves out of the display card into src/lib/errors.ts, since half the console needs it now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4a5d4b0a71 |
feat(web): the console follows the host's events instead of asking ten times a minute
The host has published every lifecycle transition on GET /api/v1/events since the API existed — client connect/disconnect, session and stream start/end, pairing decisions, display create/release, library, store and plugin changes — and nothing consumed a byte of it. The console instead polled ten endpoints on 1-5 s timers, so a change was up to 5 s stale and two pages could disagree while you looked at them. The Library page polled not at all: install a game in Steam and it never appeared until a full reload. The console now subscribes once and invalidates exactly the queries an event affects. Events never carry data into the cache — they only say "this is stale" — so an unknown future kind costs nothing and a missed event degrades to the polling that is still there underneath, now at a slow safety-net interval. The fast ticks that remain are the ones events cannot express: the live stream numbers while streaming, and a lingering display's teardown countdown. Four things had to be true for this to work, and none of them were. Each was found by measuring, not by reading: - Nitro's `localFetch` accumulates the response and only builds it when the handler returns, so nothing streams through the deployed Bun server. Three frames sent a second apart arrived together, three seconds late, when the upstream closed — and an SSE stream never closes, so nothing would ever have arrived. /api/v1/events gets its own route that hands back a web Response wrapping the upstream stream, which passes straight through. - Hydration mounts the app shell and discards it ~15 ms later. A subscription owned by that effect opened, closed, and never came back. It is a refcounted module singleton now, with a grace period so a remount re-attaches instead of reconnecting. - `getRouter()` runs more than once in the browser, and each call built its own QueryClient. The subscription held the first, the live pages read the second, and every invalidation went to a cache nobody was reading. One client per browser session; the server still gets a fresh one per request, which it must. - `invalidateQueries` only refetches queries that currently have an observer. An event means the HOST changed, so every cached copy is wrong whether or not something is watching it. Two features fall out of the same work: - **Automation** — a page for GET/PUT /api/v1/hooks. The host has run these hooks all along and the console never showed them, so the only way to see what your machine does when a stream starts was to open the config file. Writing one means writing a shell command the host will execute, so saving re-asks for the console password, like an update or an unreviewed install. - The Host page warns when another Moonlight-compatible server (Sunshine, Apollo) is running on the same machine. The host has detected this at startup for ages and reported it in /local/summary; nothing surfaced it. It is the most common reason a host looks installed and working but no client can reach it. Verified in a real browser against a mock host: three events drive three refetches of a query with no polling timer, the conflicts card names the intruder, the hook list and its dialog render, and the console reports no errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
55e01c1460 |
fix(web): stop shipping 7 MB of sound the console cannot play
`@unom/ui/button` reaches `sound/defaults.js`, which resolves two game-UI sprite sheets with `new URL(…, import.meta.url)` at module scope — a 4.8 MB .wav and a 2.2 MB .mp3. Vite emitted both into the build, so they rode into the Windows installer and the .deb. The console never mounts UnomProviders, so no player is bundled and not one byte of it could ever be played. A build-time rewrite of those two expressions takes the asset payload from 8.2 MB to 1.5 MB; the login page and the button chunk are unchanged. Deleting the plugin is the whole revert if the console ever wants click sounds. Also: - `bun run dev` forwards the management bearer, so developing against a real host stops 401ing into a /login bounce that dev has no gate to satisfy. - `check-i18n` runs after `build`, not only inside `codegen`. It exists to stop a zero-message console shipping, and the CI job and the installer build both install with `--ignore-scripts`, so it had never once run where it mattered. - Bun's idle timeout goes from its 10 s default to 120 s. The host sends SSE keep-alives every 15 s, so anything long-lived proxied through the console was cut by us first — which the event stream is about to depend on. - The typecheck covers the Storybook config and preview. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e30d94573a |
fix(web): the logs come back after a host restart, and eight more that quietly lied
The Logs page died permanently every time the host restarted — which the console's own update flow does. The host's log ring restarts at seq 1 while the page's cursor stays where it got to, and `GET /logs?after=8000` against a fresh ring is not an error, it is an empty page forever: no error, no dropped badge, stale lines on screen, and nothing short of a full reload to get out. A restart always breaks the poll first, so a failed poll now triggers a re-read from the start of the ring, and a page whose newest entry is older than what we hold is recognised as the sequence having restarted. Follow mode also stopped following at exactly the wrong moment. The autoscroll effect was keyed on the rendered row count, which pins at the 1000-row DOM cap — so once the log got busy enough to matter, the effect never re-ran again. It is keyed on the newest rendered seq now. And pausing now actually pauses: stopping the interval left React Query's focus/reconnect refetches landing, which evicted the very lines the operator had paused on. The rest: - A plugin could white-screen the whole console by registering `icon: "constructor"`. The icon map is a plain object, so the inherited key resolved to `Object`, which is truthy — the fallback never fired and React was handed `Object` as a component, from inside the app shell. - Saving a display arrangement deleted the saved position of every device that was not connected at that moment: the host replaces the whole map, and we only ever sent the displays we could see. - Flipping DDC, PnP or dedicated-game-sessions committed whatever unsaved edits the Custom block was holding, then cleared the "unsaved" badge so there was no trace of it. Those three apply on top of the SAVED policy now. - Saving the Custom block put the streamed-screen pin back to whatever it was when the form was seeded, undoing a change made in the picker below it. - "End now" on a running game calls the host's only stop, which ends EVERY live session; on a grace row with no app id it ended every waiting game. Both say so first now, when there is more than one to lose. - Edit and Delete were offered on library entries owned by a provider plugin, which the host refuses with 409 — silently. They are attributed instead. - An install whose first poll failed never polled again, and one whose host restarted spun forever with no way to dismiss it. - Submitting a second pairing PIN showed the previous attempt's "PIN sent" before a digit was typed, and the paired list it points you at never refreshed. - The streamed-screen picker claimed an env pin during every slow load, and rows that cannot be picked now look that way instead of silently eating the click. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4575134c21 |
fix(web): one bad password from anywhere stops locking out the whole console
The console's login throttle was documented as per-IP and was not. Nitro's `localFetch` hands the app a synthetic request whose socket has no `remoteAddress`, so `getRequestIP()` returned undefined for every request and every attempt was charged to one shared "unknown" bucket. Five wrong guesses from any LAN peer locked out everyone — including the operator, and including the update-apply route, which shares that budget. The Bun entry is the only place the real peer is knowable, so it now stamps it into a header (deleting any client-supplied copy first) and `peerAddress()` reads it back. Verified on a real build bound to 0.0.0.0: seven wrong logins from 127.0.0.1 lock 127.0.0.1 out, a different peer still logs in on the first try, and a request forging the header is charged to its real address. Also on the way through: - Installing an unreviewed package and adding a catalog source now re-ask for the console password, like applying an update already did. A 7-day session cookie should not be able to run new code on the host, and `store/install` with `accept_unverified` did exactly that through the generic passthrough. The gate sits at the trust boundary — adding a source, or a raw spec — not on every install from a source the operator already chose to trust. - The ui-credential denylist is matched against the normalised path too, so `/api//v1/...` and friends can no longer walk around it. - The console serves nosniff, a no-referrer policy, and a CSP that pins frame-ancestors, object-src and base-uri. - A plugin UI's response no longer re-emits the content-encoding that `fetch` already decoded (which made compressed plugin pages fail to load), no longer sets cookies on the console's origin, and OPTIONS reaches the plugin instead of being refused 405 by us. - An unreachable host reads as 502 on these routes, matching the passthrough, instead of a bare 500. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6a4ffcb15c |
fix(client): HDR stops leaking out of the stream and blowing out the console UI
ci / web (push) Successful in 1m3s
ci / rust-arm64 (push) Successful in 1m23s
ci / docs-site (push) Successful in 1m12s
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 8s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 7s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m29s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 49s
deb / build-publish-client-arm64 (push) Successful in 1m57s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
deb / build-publish-host (push) Successful in 3m40s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m55s
deb / build-publish (push) Successful in 5m59s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m7s
docker / builders-arm64cross (push) Successful in 4s
docker / deploy-docs (push) Successful in 27s
android / android (push) Successful in 8m10s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m12s
arch / build-publish (push) Successful in 9m35s
flatpak / build-publish (push) Successful in 6m57s
ci / rust (push) Successful in 10m44s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 7m54s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 6m17s
apple / swift (push) Successful in 5m10s
apple / screenshots (push) Canceled after 19m1s
The gamepad/console UI looked right on launch, and wrong forever after the first HDR session: connect to an HDR host, disconnect, and the UI came back overblown with wrong colours. The UI is not its own renderer. It is a `pf_presenter::overlay::Overlay` composited into the SAME swapchain the stream used, and it draws plain sRGB with no HDR awareness at all — so the swapchain's colorspace decides how its pixels are read. `present` switches SDR↔HDR10 from the FRAME's colour signalling, and a UI-only present is `FrameInput::Redraw`, which carries none: the mode block is skipped entirely and nothing ever hands HDR10 back. The UI's sRGB mid-tones were then emitted as PQ code points, i.e. near-peak nits. `leave_hdr` drops back to SDR, called where the UI-only present already happens and gated on the existing `browse_idle` — Browse mode with no live connector, i.e. the UI owns the screen. That covers every route back to the UI rather than just the Ended/Failed arms, and it is guarded internally so idle iterations stay free. It also bails when minimized, which is load-bearing rather than an optimization: `recreate_swapchain` keeps the old swapchain at a zero extent, but `set_hdr_mode` would by then have rebuilt the CSC and overlay pipes against the SDR format — mismatched against live HDR10 images. `present` early-returns on a zero extent above its HDR block, so this was unreachable until a caller outside `present` existed. Deliberately not applied to the `resize_scrim` arm of the same present: that scrim is a mid-stream gap in a session that is still HDR, and flipping there would rebuild the swapchain twice per resize. Does not address the adjacent case: an overlay drawn DURING a live HDR session (the stats HUD, the resize scrim) is blown out the same way. That needs the overlay to PQ-encode when `hdr_active`; dropping to SDR is only correct here because the console UI shows exactly when no stream is live. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
43a631ea9c |
fix(clients): a host that re-keys stops locking the client out for good
ci / web (push) Successful in 1m2s
ci / docs-site (push) Successful in 1m21s
ci / rust-arm64 (push) Successful in 2m48s
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 7s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 7s
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 7s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 17s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 13s
deb / build-publish-client-arm64 (push) Successful in 2m25s
docker / deploy-docs (push) Canceled after 25s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m31s
deb / build-publish (push) Successful in 3m52s
apple / swift (push) Successful in 5m20s
arch / build-publish (push) Successful in 6m52s
docker / builders-arm64cross (push) Successful in 8s
android / android (push) Canceled after 7m17s
deb / build-publish-host (push) Successful in 5m45s
apple / screenshots (push) Canceled after 1m5s
ci / rust (push) Canceled after 7m24s
flatpak / build-publish (push) Canceled after 3m32s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m58s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 3m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 2m47s
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 11s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
Reinstall a host, wipe its ProgramData, or otherwise regenerate its identity, and the desktop clients refused it forever: "Host identity rejected — wrong fingerprint, or the host requires pairing", including immediately after a successful re-pair. There was no way out of it from the UI — the host list showed two cards for one address and forgetting the wrong one was a guess. `KnownHosts::upsert` matches on the FINGERPRINT, which is what lets a host that moved address keep its record and everything the user set on it. A host that changed identity matched nothing, so pairing appended a SECOND record for an address that already had one, and `find_by_addr` returned whichever came first in the file — the dead one, every time. Trust decisions (PIN ceremony, delegated approval, TOFU accept, headless pair — all funnelled through `persist_host`, plus the Windows shell's two direct upserts) now go through `upsert_trusted`, which retires any OTHER record for that address. Retired means DELETED, not demoted: a record whose certificate the host no longer holds cannot connect, so keeping it only reproduces the two- cards-one-address confusion this fixes. What described the box rather than the identity — its MAC, its OS chain, the bound profile, the pinned cards, when it was last used — moves onto the record that survives, so a reinstall doesn't quietly cost the user their setup. What described the dead identity does not: `paired` and `clipboard_sync` are decisions about one specific certificate and have to be made again for a new one, and the retired record's stable id stays retired (a deep link written from it falls through to the `host=` recovery the link grammar already specifies). Only trust decisions may retire a record. The wake path's address re-key and every learn-from-advert path stay on plain `upsert`: those are driven by unauthenticated mDNS, and letting an advert delete a saved host by claiming its address would trade this bug for a much worse one. A plain reconnect still fails closed on a pin mismatch — nothing here changes what the pin is checked against. Stores that already hold the duplicate recover on the next connect, not at load: which of two records is live isn't knowable at load time and guessing wrong would throw away the good one. Instead `find_by_addr` stops being positional — a real fingerprint beats a placeholder, and among real ones the newest trust decision wins, since records are only ever appended by one. The next successful pair then cleans the store up for good. Every lookup that picks a pin or a per-host decision for a connect now goes through it (the session's pin and clipboard read, the deep-link resolver, orchestrate's plan, both speed tests, the CLI's --wake and --library, which had also been ignoring the port), and an advert's learned MAC/OS lands on the record it identified rather than on a stale namesake that merely came first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6af067da2d |
fix(client): the bottom rows stop smearing — the decode pool is taller than the picture
ci / web (push) Successful in 1m49s
ci / rust-arm64 (push) Successful in 2m19s
ci / docs-site (push) Successful in 2m29s
ci / rust (push) Successful in 7m3s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 19s
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 6s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
deb / build-publish-client-arm64 (push) Successful in 2m28s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 21s
deb / build-publish (push) Successful in 4m17s
deb / build-publish-host (push) Successful in 4m51s
docker / builders-arm64cross (push) Successful in 6s
docker / deploy-docs (push) Successful in 36s
android / android (push) Successful in 8m24s
arch / build-publish (push) Successful in 8m28s
flatpak / build-publish (push) Successful in 6m8s
apple / swift (push) Successful in 5m54s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 13m5s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 3m10s
apple / screenshots (push) Canceled after 7m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 16m25s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 15m57s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 5m3s
A user's 1080p stream repeated its last row of pixels over the final few rows, so the image looked stretched at the bottom. The Vulkan-Video CSC pass sampled the decoded planes with the fullscreen triangle's normalized 0..1 UVs, but its render target is built at the CROPPED frame size. Those are not the same rectangle: FFmpeg sizes the decode pool from `avctx->coded_*`, and H.264 codes `16 * mb_height` — so a 1080-row picture decodes into a 1088-row pool. Destination row 1079 sampled source row ~1087.5, dragging the 8 alignment rows into view and squashing the picture 0.7%. Encoders fill that padding by replicating the last picture line, which is why it reads as a smeared bottom row rather than garbage. Confirmed on glass (.173, RTX, H.264 1080p, vulkan-video): Vulkan Video first frame width=1920 height=1080 pool_w=1920 pool_h=1088 `VkVideoFrame` now carries the pool extent and `record_csc` takes a `uv_scale`, written to the shader's `params.zw` — which the CSC shader already reserved for a use like this. The chroma cositing offset is unchanged and stays correct: `textureSize` reports the pool width, which is the space the scaled UV is already in. Only the Vulkan-Video path passes a scale below 1.0. D3D11VA already clamps this in its VideoProcessor blit (the same bug, seen as a green bar there because DXVA padding is uninitialized rather than replicated); dmabuf imports its planes at the crop over the real stride; PyroWave allocates its ring at exact stream dims. Apple and Android crop at the OS layer. Every other call site passes [1.0, 1.0], so the change is inert there. Also adds a one-time first-frame layout log mirroring the D3D11VA one, so the frame-vs-pool gap is visible in the field instead of having to be re-derived from FFmpeg internals. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9f72a3b6ad |
feat(host): HDR and 4:4:4 stop being mutually exclusive on Windows
windows-host / package (push) Failing after 22s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
ci / web (push) Successful in 1m7s
ci / rust-arm64 (push) Successful in 1m30s
ci / docs-site (push) Successful in 1m31s
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 11s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 19s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 15s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 13s
deb / build-publish-client-arm64 (push) Successful in 2m21s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m6s
deb / build-publish (push) Successful in 4m43s
docker / builders-arm64cross (push) Successful in 11s
docker / deploy-docs (push) Successful in 33s
deb / build-publish-host (push) Successful in 5m4s
android / android (push) Successful in 6m40s
ci / rust (push) Successful in 7m10s
arch / build-publish (push) Successful in 8m11s
apple / swift (push) Successful in 5m8s
apple / screenshots (push) Canceled after 10m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 16m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 15m50s
An HDR display cost you full chroma: the IDD-push capturer's only 10-bit
output was P010, so a session that negotiated 4:4:4 on an HDR desktop was
converted to 4:2:0 at capture time — *after* the Welcome had already told
the client 4:4:4. The client believed it (nothing on the wire contradicts
a Welcome), and the new chroma tag in the stats overlay is what finally
made the discrepancy visible.
Everything except the source was already in place, which is why this is
small: the NVENC config layer has stamped `FREXT` + `chromaFormatIDC=3` +
`pixelBitDepthMinus8=2` — HEVC Main 4:4:4 10 — with a unit test since the
4:4:4 work landed, `PixelFormat::Rgb10a2` already maps to `ABGR10` and
already counts as a full-chroma input, and the desktop client learned the
10-bit 4:4:4 Vulkan pool format in
|
||
|
|
47eb8c9f6f |
fix(windows): a web-console deploy that half-succeeded stops reporting success
windows-host / package (push) Failing after 26s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
ci / web (push) Successful in 1m7s
apple / swift (push) Successful in 1m28s
ci / rust-arm64 (push) Successful in 1m57s
ci / docs-site (push) Successful in 2m31s
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 7s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 7s
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 7s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
deb / build-publish-client-arm64 (push) Successful in 2m22s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 48s
deb / build-publish-host (push) Successful in 4m38s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m26s
deb / build-publish (push) Successful in 5m45s
docker / builders-arm64cross (push) Successful in 13s
docker / deploy-docs (push) Successful in 31s
android / android (push) Successful in 7m33s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 6m55s
arch / build-publish (push) Successful in 8m58s
ci / rust (push) Successful in 7m17s
flatpak / build-publish (push) Successful in 7m11s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m2s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m39s
release / apple (push) Successful in 16m28s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m39s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m59s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 8m53s
apple / screenshots (push) Canceled after 16m31s
Found by investigating a console that had been serving errors on .173 for
hours while every health check said HTTP 200.
Nitro's `entry.mjs` imports its sibling chunks by CONTENT HASH, so a
`.output` that mixes two builds is not degraded — it is dead: bun answers
every page with a `ResolveMessage` JSON body ("Cannot find module
../_/router-<hash>.mjs") under a 200 status. Two defects here let exactly
that ship and then hid it:
* The pre-copy `Remove-Item` used `-ErrorAction SilentlyContinue`, so a
removal blocked by a still-running bun was swallowed and `Copy-Item`
merged the new build into the old tree. (Reproduced live: an older
task-based copy of this script, run against the now supervised-child
host, tried `schtasks /end` for a task that no longer exists, never
stopped the service, and so could never unlock the files.) The removal
is now verified and refuses to copy over a tree it could not clear.
* The success probe read only the status code, so it reported a healthy
console for a server that serves nothing but an error. It now checks
that `/login` actually returns HTML, and says so loudly when the body
is a module-resolution error instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
2ce0bea830 |
feat(client): desktop phase-locked capture, a real display volume on Windows, and the console's missing rows
The cross-platform half of the gap sweep: * Phase-locked capture reaches the DESKTOP clients (it shipped on Apple/Android only, though the desktop presenter has the best latch signal of all — true on-glass stamps via VK_KHR_present_wait). The presenter's 1 Hz fold publishes a latch grid (anchor = last on-glass instant; period = min positive present spacing, capped by the display mode's refresh so an arrival-paced sub-panel-rate stream can't claim a slower grid); the session pump folds every AU's arrival stamp against it with the SHARED `phase::circular_latch` statistic and sends the ~1 Hz PhaseReport (1 ms uncertainty — reference-client parity). The cap is advertised only when present timing is real (`VulkanDecodeDevice::present_timing` gates `SessionParams::phase_lock`), and the host's applied grid offset from the 0xCF tail is logged so an on-glass run can watch the controller engage. * `Hello::display_hdr` stops being hardcoded `None`: Windows reads the panel's colour volume from DXGI (`IDXGIOutput6::GetDesc1`, the `--window-pos` output else the primary, advanced-color outputs only, gated on the HDR setting) so the host's virtual-display EDID matches the real glass. Linux keeps the EDID defaults — no portable Wayland/X11 query exists — and the comment now says exactly that. * The console settings screen (the ONLY editor in Gaming Mode) learns the rows it was missing: render scale, full chroma 4:4:4, invert scroll, capture system shortcuts, fullscreen-on-stream, auto-wake and the game-library toggle. * A spec-run session's device picks (GPU adapter, speaker, microphone) now come from the `--resolved-spec` instead of a raw Settings load — the last store read the spec path still owed (§5), and what would make those fields profileable. * `session_args()` documents why the GTK/CLI spawners pass no `--window-pos` (Wayland exposes no global coordinates to read and SDL can't apply them). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fd75e66041 |
feat(windows): the shell spawns from the shared brain, and links, tests and pairing stop losing things
Windows-shell parity gaps from the 2026-07-31 sweep, closed:
* The spawner goes through the shared brain: `ConnectPlan::for_target` →
`session_args()` + a written `--resolved-spec`, replacing the
hand-assembled argv that meant Windows sessions ALWAYS took the compat
path (re-resolving every setting from the stores — the drift
orchestrate.rs documents as a trap) and that any field added to the
spec was silently Windows-dead. Fullscreen now comes from the plan's
effective settings (profile-aware) instead of a caller argument, the
spec temp file is cleaned up at exit, and the reader finally parses
the `{"window":…}` line so the SPAWNER persists the match-window size
(§5) instead of the renderer's load-modify-save fallback.
* A deep link to an unpaired host runs the trust ceremony instead of
refusing: the Pair screen opens seeded with what the link CLAIMED —
name shown as claimed, fingerprint carried, and the launch/profile
surviving the detour (`Target` grew a `launch` field for exactly
this). GTK parity; a shared link was a dead end here before.
* The speed test learns the Ask tier: a bound host whose profile
INHERITS bitrate now offers both "Set as default" and "Set in
profile" instead of silently creating a profile override. The global
write (and the forwarded-controller handler) also rebase on the file
before their whole-struct saves — two more stale-snapshot writers.
* The Controllers card shows the detected-pad inventory (read-only,
with the Steam-Input-virtual note) — the fastest answer to "is my
controller even detected?", present on GTK all along.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
698925a036 |
fix(clients): settings saves stop reverting each other, Windows gets audio pickers, and a game link launches its game
The rest of the client-gaps handoff, plus what a parity sweep surfaced: * Every whole-file settings save rebases on the file first — the GTK dialog close and its two speed-test apply arms, the Windows per-control commit, the console screen. The file has five writers and no merge (profiles.rs documents the debt); saving a shell-lifetime snapshot silently reverted whatever another writer stored meanwhile, most visibly the spawner-persisted match-window size (item 4). * Windows honors the Speaker/Microphone picks: `audio_wasapi` grows endpoint enumeration (on its own MTA thread — UI threads are STA) and resolves PUNKTFUNK_AUDIO_SINK/SOURCE as endpoint ids, falling back to the default when the picked device is gone; the settings page gets the two rows (defaults scope, "(not detected)" like the GPU row). `speaker_device`/`mic_device` stop being Linux-only fields (item 5). * A punktfunk:// link with `launch=` opened a plain desktop session on Windows — the id was parsed, validated, planned, and dropped at the last hop. It now rides `initiate_launch` (plus a waking variant for the dial-first path). Linux always forwarded it. * The console UI offered "VAAPI" on Windows — a dead option there that also hid d3d11va, the actual Windows hardware path. The decoder list is per-OS now. * The Windows gamepad picker learns "Steam Deck" (the GTK picker had it; the host-side pad has always existed). * Doc fixes: gpu.rs named a nonexistent env var (PUNKTFUNK_ADAPTER → PUNKTFUNK_VK_ADAPTER), and session/Cargo.toml claimed video decode is Linux-only while explaining what the ARM64 leg drops. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
74863c96b3 |
feat(client): 4:4:4 can stay on hardware, and the overlay says what you actually got
Two halves of the same honesty problem (client-gaps handoff items 1+2,
done together as the handoff asked).
The presenter learns full chroma: `vkframe_plane_views` accepts the
2-plane 4:4:4 pool formats (8-bit, and the 10-bit 3PACK16 sibling) —
what NVIDIA's Vulkan Video reports for HEVC RExt decode — with the
accepted set extracted into `vkframe_plane_formats` and pinned by a
decision-table test. The CSC shader needed nothing: its 4:2:0 siting
correction already self-disables when the plane widths match. The VAAPI
leg gets the same treatment (NV24 in `drm_fourcc_for` and the dmabuf
import, full-size chroma plane), and the Vulkan decoder's sw-format
gate admits NV24/P410. 3-plane 4:4:4 stays rejected — it needs a third
CSC binding — and demotes cleanly like every other unsupported format.
Design call (the handoff's fork, argued here as requested): (B)+(C),
not (A). No capability probe gates VIDEO_CAP_444 — software decode is
the guaranteed display floor on both OSes (swscale → RGBA), the decoder
ladder demotes on its own, and a probe-gated bit would turn the switch
inert on exactly the boxes that rely on the fallback. What (A) wanted
from a prediction, the overlay now delivers as ground truth:
The Detailed tier prints the encoder's target next to the measured
rate — `19.4 Mb/s · target 20 Mb/s (auto)` — and the resolved chroma,
`4:4:4→4:2:0` when the host declined the ask (mirroring `HDR→SDR`).
The target is live: `NativeClient::current_bitrate_kbps()` mirrors
every BitrateChanged ack, so an Automatic session's ABR re-targets are
visible as they move. This is the figure whose absence let the
settings-drop bug (
|
||
|
|
5926306a4c |
feat(windows): the web console becomes a supervised child of the host service
windows-host / package (push) Failing after 22s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
ci / docs-site (push) Successful in 1m47s
ci / web (push) Successful in 1m53s
ci / rust-arm64 (push) Successful in 1m58s
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 9s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 7s
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 58s
apple / swift (push) Successful in 4m45s
deb / build-publish-client-arm64 (push) Successful in 3m28s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m27s
deb / build-publish (push) Successful in 5m26s
docker / builders-arm64cross (push) Successful in 6s
android / android (push) Successful in 6m22s
docker / deploy-docs (push) Successful in 38s
ci / rust (push) Successful in 7m10s
deb / build-publish-host (push) Successful in 5m27s
arch / build-publish (push) Successful in 8m42s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m58s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m16s
apple / screenshots (push) Successful in 20m19s
Three silent console outages in one week (0x1 / 0xFFFFFFFF / 0x41306), each a different proximate cause of the same structural defect: the console's lifecycle was owned by Task Scheduler — one best-effort start per boot/logon/install, no retry on a plain non-zero exit, no watchdog — while the product already shipped a real supervisor. The service now supervises the console as a second child slot: plain session-0 spawn (suspended → own no-breakaway kill-on-close job → resume), started only once the host has written mgmt-token + cert.pem + key.pem (the cert race dies by construction), secrets read from their files at every respawn, bun's stdout finally captured in logs\web.log, doubling backoff 0.5s→60s that never gives up. Session switches never touch it; a service stop takes it down via the job. The PunktfunkWeb task is retired: web setup slims to password + legacy task delete + firewall, the 127-line web-run.cmd batch supervisor is deleted, an [InstallDelete] entry reaps the stale copy, and service install now sets SCM crash-recovery actions (restart 1s/5s/60s) since the console rides on the service process. StopBunRuntimes stays for the scripting runner + the one migrating upgrade. Design: punktfunk-planning design/windows-web-console-lifecycle.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9c5af8d7e1 |
fix(clients): the settings you chose reach the stream again, and four more that never did
ci / web (push) Successful in 58s
ci / docs-site (push) Successful in 2m50s
ci / rust-arm64 (push) Successful in 2m55s
deb / build-publish-host (push) Successful in 4m16s
deb / build-publish-client-arm64 (push) Successful in 4m56s
apple / swift (push) Successful in 4m41s
flatpak / build-publish (push) Successful in 6m32s
deb / build-publish (push) Successful in 3m44s
android / android (push) Successful in 8m2s
ci / rust (push) Successful in 11m18s
arch / build-publish (push) Successful in 10m24s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m42s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m10s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m24s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m1s
apple / screenshots (push) Successful in 20m36s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m40s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 4m9s
Since spec mode (`f32c3aaa`, shipped in 0.22.0) the GTK shell has handed the session a `--resolved-spec` built from `Settings::default()`. A session running from a spec performs ZERO store reads by design, so every stream since has run at the defaults: `bitrate_kbps: 0` reaches the host, which reads it as its 20 Mbps fallback — the field report this starts from, "my bitrate seems to be stuck at 20 MBPS" on 0.22.3 — and with it the resolution, refresh, render scale, codec, decoder, HDR, 4:4:4, audio channels, mic, touch/mouse mode, invert scroll, stats tier, match-window and gamepad type. Profiles never applied at all: the spec's `profile` was None, and spec mode ignores `--profile`. The tell in the wild is that the GPU and audio-device pickers kept working — the session reads those three off disk into env vars before it reads the spec. The plan carried a comment explaining that `settings` "carries only what the argv needs (the fullscreen flag)". That was true when it was written and stopped being true one commit later, in another file. `ConnectPlan::for_target` answers that shape of bug: a front-end holding its own request type resolves through the same helper the session's compat path uses, so there is no hand-built `Settings` left to go stale. The GTK shell's `fullscreen_on_stream` parameter goes with it — that is a tier-P field, and a shell-read global was beating a profile that set it. The Windows shell was never affected (it spawns with no spec) but had the same fullscreen-vs-profile bug, so it moves onto the resolver too. The audit that followed found four more settings stored, rendered in two UIs, and read by nothing: - `enable_444` never became `VIDEO_CAP_444`. "Full chroma (4:4:4)" is announced in the 0.22.0 notes for Linux and Windows and did nothing on either; only Apple advertised the bit. The host already gates it on its own policy, its capturer, HEVC and a real GPU probe, and answers the resolved chroma in the Welcome before we build a decoder — the client only has to ask. There is no desktop decode probe (Apple has `Stage444Probe`), but the presenter bails cleanly on a plane format it doesn't know and demotes to software, so the downside is a slow decode, not a broken one. - `session_params` picked the HEIGHT fallback off `settings.width == 0`. - `cli::exec_session` dropped `--profile` from its forward list, so `punktfunk --connect … --profile Work` from a script or a Decky wrapper streamed with the host's binding instead. - `ResolvedSpec::write_temp` named the spec by the SPAWNER's pid alone, so a cancelled connect and the retry behind it shared one path — and the first child's exit deleted the file the second was still starting up to read. The regression test asserts the plan carries the profile-overlaid bitrate and that the spec equals it. On the old code both are 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3594bc029e |
fix(host): a host with no host.env streams a screen, not a test pattern
android / android (push) Canceled after 0s
apple / swift (push) Canceled after 37s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 0s
ci / rust (push) Canceled after 40s
ci / rust-arm64 (push) Canceled after 37s
ci / web (push) Canceled after 38s
ci / docs-site (push) Canceled after 36s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 15s
deb / build-publish (push) Canceled after 0s
deb / build-publish-host (push) Canceled after 46s
deb / build-publish-client-arm64 (push) Canceled after 41s
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 11s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
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
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 10s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 25s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m19s
docker / builders-arm64cross (push) Successful in 16s
docker / deploy-docs (push) Successful in 38s
windows-host / package (push) Successful in 17m58s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 34s
`PUNKTFUNK_VIDEO_SOURCE` had no default, and unset fell through to the synthetic
test pattern. That was invisible while the systemd unit made host.env mandatory —
you could not start the host without the file, and every copy of the file sets
`virtual`. Now that the unit treats host.env as optional (
|
||
|
|
fd3c54bd43 |
feat(client): "Capture system shortcuts" finally decides where Alt+Tab goes
ci / web (push) Successful in 1m7s
ci / docs-site (push) Successful in 2m22s
ci / rust-arm64 (push) Successful in 3m34s
android / android (push) Canceled after 4m8s
apple / swift (push) Canceled after 4m13s
apple / screenshots (push) Canceled after 0s
decky / build-publish (push) Successful in 42s
arch / build-publish (push) Canceled after 4m25s
ci / rust (push) Canceled after 4m30s
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 10s
deb / build-publish (push) Canceled after 4m35s
deb / build-publish-host (push) Canceled after 3m29s
deb / build-publish-client-arm64 (push) Canceled after 2m15s
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 10s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 13s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 6s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 4s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 10s
docker / builders-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 30s
flatpak / build-publish (push) Canceled after 0s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 6m14s
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 toggle has been stored, profileable and rendered in two settings UIs since profiles landed, and nothing read it. Windows grabbed the keyboard whenever input was captured, setting or no setting; Linux never grabbed at all, because the grab sat behind `#[cfg(windows)]` with a comment deferring the compositor story to "the shells" — which never picked it up. `Settings.inhibit_shortcuts` now reaches the presenter and gates the grab, on both platforms. SDL3 already maps `SDL_SetWindowKeyboardGrab` onto `zwp_keyboard_shortcuts_inhibit_manager_v1` on Wayland and `XGrabKeyboard` (plus `_XWAYLAND_MAY_GRAB_KEYBOARD`) on X11, so dropping the cfg is the Linux fix. Capture state still gates it, so releasing input hands the chords straight back, and the desktop mouse model never grabs. A compositor with no shortcuts-inhibit global says so once instead of failing silently — at debug under gamescope, which has no shortcuts to inhibit in the first place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
020306b5ac |
fix(packaging,host): a fresh Linux install can start, and the comments stop lying
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 2m11s
ci / rust-arm64 (push) Successful in 2m41s
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 11s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 24s
deb / build-publish-client-arm64 (push) Successful in 2m28s
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 7s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 13s
apple / swift (push) Successful in 4m48s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m36s
arch / build-publish (push) Successful in 7m20s
ci / rust (push) Successful in 7m21s
deb / build-publish-host (push) Successful in 7m4s
docker / builders-arm64cross (push) Successful in 8s
deb / build-publish (push) Successful in 5m31s
docker / deploy-docs (push) Successful in 34s
android / android (push) Successful in 8m17s
windows-host / package (push) Successful in 10m43s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 18s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m40s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 17m31s
apple / screenshots (push) Successful in 20m57s
Fallout from the documentation sweep: verifying doc claims against the code
turned up defects in the code and the shipped templates. Mostly comments that
describe behaviour we no longer have — which is how the docs went wrong in the
first place, since someone reads the comment and writes the page.
The one that mattered: a fresh deb/RPM/Arch install could not start the host at
all. The unit's `EnvironmentFile=` had no `-`, making host.env mandatory, and no
package creates it — all three ship only the templates under /usr/share and the
postinst merely prints the copy command. So `systemctl --user enable --now
punktfunk-host` died on "Failed to load environment files". Every field in
HostConfig::from_env resolves through unwrap_or/filter/None, so absent means all
defaults, exactly like a hand-run `serve`; the Nix module already wrote it as
`-${environmentFile}`. The Deck installer's own generated unit gets the same
prefix for the case where an operator later removes the file.
Shipped templates: PUNKTFUNK_SECURE_DDA is read by nothing (DDA/WGC are gone;
IDD-push is the sole Windows capture path and the secure desktop is
unconditional), so it stops being written into a fresh host.env;
PUNKTFUNK_INPUT_BACKEND offered a `uinput` value that does not exist and omitted
`kwin`, which is what a KDE session actually resolves to; PUNKTFUNK_RENDER_ADAPTER
no longer claims to pick a "Desktop-Duplication" GPU.
Comments corrected rather than deleted, since each explains a real why:
PUNKTFUNK_10BIT is default-on with explicit-off grammar, not an operator opt-in;
GNOME reaches EIS through Mutter's direct RemoteDesktop API, so it needs no portal
approval and is headless-capable; and the host does not run in session 0 — the
service is the session-0 supervisor and the host runs as SYSTEM in the interactive
console session, which is why game_term has to bind the input desktop at all.
packaging/bazzite/update-punktfunk.sh is now installed to /usr/share/punktfunk/,
so the command the docs promised exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
5807d3a8a6 |
fix(tools): the loss harness catches up with seal_streamed_chunk's slice flag
ci / rust (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (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
ci / rust-arm64 (push) Successful in 2m24s
|
||
|
|
2a6bf3c421 |
fix(vdisplay/gamescope): the takeover stops a live session's DM on every flavor — the mask never stopped SDDM's relogin
ci / web (push) Successful in 1m7s
ci / docs-site (push) Successful in 2m12s
ci / rust-arm64 (push) Successful in 2m26s
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 8s
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 9s
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 5s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 49s
apple / swift (push) Successful in 4m49s
ci / rust (push) Failing after 4m7s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 18s
android / android (push) Successful in 5m58s
deb / build-publish-client-arm64 (push) Successful in 3m31s
docker / builders-arm64cross (push) Successful in 8s
docker / deploy-docs (push) Successful in 32s
deb / build-publish-host (push) Successful in 4m14s
deb / build-publish (push) Successful in 5m44s
windows-host / package (push) Successful in 11m1s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 21s
arch / build-publish (push) Successful in 14m59s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m34s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m9s
apple / screenshots (push) Successful in 21m19s
The managed takeover masked gamescope-session-plus@*.service and trusted the mask to hold SDDM's Relogin=true loop at bay. On images whose sddm session helper execs the session script directly (/etc/sddm/wayland-session gamescope-session-plus steam — f43 bazzite-deck), the relogin never touches the unit: SDDM relogged in ~3x/s for the whole stream, each attempt a full bash --login session start failing against the managed instance. Live on the .41 VM (2026-07-31): 328 forks/s, load 6+, 1481 logind sessions in 8 minutes, the journal flooded past its own rotation — and a 5070 Ti that "could not hold 240fps". A DM that drove a LIVE gaming session is now stopped for the stream's duration on every flavor (the fragile-flavor machinery — lingering guard, pkexec dm-helper, restore via reset-failed+restart, crash-persisted state — already existed and is reused as-is; a pure dm_plan() derives the decision and is unit-tested). The flavors now differ only in the degraded mode when the stop is impossible: SDDM falls back to mask-only (the storm tax, but Steam stays protected — never attach), fragile DMs still fail the takeover into the attach degrade. The mask stays as belt-and-braces for the window before the stop and for images that do route the relogin through the unit. Verified: scripts/xcheck.sh linux clippy (-D warnings, --all-targets) clean; pf-vdisplay suite 125/125 on Ubuntu 26.04. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
957cf7781c |
feat(apple): a Pencil near the glass pins the panel at the link ceiling
ci / web (push) Successful in 2m4s
ci / docs-site (push) Successful in 2m19s
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 9s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
ci / rust-arm64 (push) Successful in 3m42s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
deb / build-publish-client-arm64 (push) Successful in 2m26s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m16s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m11s
ci / rust (push) Failing after 2m7s
apple / swift (push) Successful in 4m59s
deb / build-publish (push) Successful in 6m51s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 8s
deb / build-publish-host (push) Successful in 7m10s
docker / builders-arm64cross (push) Successful in 8s
android / android (push) Successful in 8m44s
arch / build-publish (push) Successful in 8m35s
docker / deploy-docs (push) Successful in 33s
flatpak / build-publish (push) Successful in 8m0s
windows-host / package (push) Successful in 18m16s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 21s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m27s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m29s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m10s
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 3m5s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 4m1s
release / apple (push) Successful in 25m56s
apple / screenshots (push) Canceled after 4s
UIKit delivers Pencil events at the PANEL's cadence, and the deadline link's rate hint pins the panel to the stream rate (min = preferred = stream Hz, deliberately — the anti-idle-stall floor). Net effect: a 60 fps stream on a 120 Hz iPad silently halved pencil sampling, 8.3 → 16.7 ms per event batch — exactly the workload (drawing) that feels it most. FrameRateHint gains a boost that lifts min/preferred to the range ceiling while a Pencil is in range (hover or contact): PencilStream surfaces proximity transitions from its emit() choke point, the view controller debounces release by 2 s (edge-of-canvas hover flicker must not thrash the link), and Stage2Pipeline/SessionPresenter stage it like the rate hint — applied from the link's own thread, no-op under arrival/glass pacing. Presents still pace at stream rate (extra updates vend into the newest-wins stash), so the cost is empty link wakes scoped to proximity. pf.present logs engage/release for on-glass verification. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7cf71dd218 |
feat(clients): the Apple deadline presenter reports its latch phase
Phase-locked capture had no client half on Apple — the host's v3 controller (grid-locked submits, coherence-gated engage) shipped with only the Android reporter feeding it. Now the stage-4 link thread flushes the same v2 circular arrival-phase statistic at ~1 Hz: - PhaseReporter (Stage2Pipeline): the decode callback deposits per-AU reassembly-completion stamps, the CAMetalDisplayLink update deposits the latch grid (period = window-min of update spacing), and the flush ports punktfunk_core::phase::circular_latch verbatim — a period-smeared Wi-Fi link reads coherence ≈ 0 and the host correctly never engages; a wired link opens the gate. Binds/unbinds per session like DecodeReport. - PunktfunkConnection.reportPhase wraps the existing ABI entry point. - Hello honesty: iOS/tvOS advertise CLIENT_CAP_PHASE_LOCK (macOS stays without — the stage-2 arrival presenter has no latch grid), Android now sets the bit its reporter already earned, and the ABI grows the PUNKTFUNK_CLIENT_CAP_PHASE_LOCK mirror const (header regenerated). Advisory in v1: the host arms on report receipt. - Stage-3 doc comment no longer calls itself the tvOS default (stage-4 took that over in the 2026-07 rebuild). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
86d4b36bfa |
chore(core): the Apple recv path documents its unsafe blocks
clippy -D warnings only lints this cfg(target_vendor = "apple") file on a Mac, which CI never does — the three recv/recvmsg_x blocks predate the undocumented_unsafe_blocks sweep and kept every local Apple-side clippy run red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c002ca8746 |
fix(clients): pen batches split at the wire cap and the heartbeat survives tracking mode
Three drawing-path defects on the pen clients: - The Apple in-range heartbeat was re-armed on every emit (~120 run-loop mutations/s during a stroke) and lived in .default run-loop mode only — a tracking-mode excursion >200 ms with a stationary pen crossed the host's force-release failsafe and dropped the held stroke. Now one long-lived 50 ms timer in .common mode, resending only after ≥50 ms of send silence. - Both clients TRUNCATED an over-8-sample coalesced run instead of splitting it into consecutive batches (the send_pen contract): Apple suffix(8) dropped the oldest samples, the Android JNI clamped count. An over-cap run means the UI thread hitched — exactly when dropping stroke geometry hurts most. Apple splits in emit(); the Android JNI loops send_pen over ≤8-sample chunks (Kotlin's per-emit ceiling is now 64 = a >250 ms stall). - The iOS letterbox mapper did a lock+FFI currentMode() read per coalesced sample on the main thread, contending the ABI lock the batch send takes next — now a 250 ms TTL cache (mid-stream requestMode still tracked). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d383161723 |
docs: the docs catch up with five releases of shipped work
ci / rust (push) Failing after 2m31s
ci / docs-site (push) Successful in 1m22s
ci / web (push) Successful in 1m48s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 1m1s
ci / rust-arm64 (push) Successful in 2m2s
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 9s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / builders-arm64cross (push) Successful in 20s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 36s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 36s
docker / deploy-docs (push) Canceled after 0s
~1150 feat/fix commits landed since v0.19 and the docs drifted badly. This is a full sweep of every page against the code as shipped: ~280 verified corrections, nine new pages, and one deletion. The worst of what was wrong: the quickstart's five-minute path could not work (`serve` never started the web console, so step 3 had no PIN to read); every packaged Linux host runs `serve --gamestream` while security.md told readers to leave GameStream off; HDR was documented as Windows-only; `PUNKTFUNK_SECURE_DDA` was documented as a working knob that nothing reads; `PUNKTFUNK_INPUT_BACKEND` listed a `uinput` value that does not exist and named libei for KDE instead of kwin; README linked three pages deleted on 2026-07-05; and the rpm-ostree update command pointed at a script no package installs. Completeness: about half of what shipped since v0.19 had no page at all. New: support-matrix (what works where, from 217 verified capability cells), input (mouse/touch/pen — and the in-stream chords, so the docs finally say how to get your mouse back), client-settings, profiles-and-links, game-library, clipboard, wake-on-lan, hdr, uninstall. Updating existed but had zero inbound links. status.md is gone: its facts moved into the support matrix, its shell stays as a redirect so the public URL does not 404. roadmap.md is themes now, not a feature checklist — checkboxes are what rotted. Debian is no longer claimed. The .deb's Depends resolve against Ubuntu images, nothing in CI builds or tests Debian, and Debian 12 is below the glibc 2.39 floor. The `debian` in the repo URL is the package format. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
49bc93cd20 |
feat(encode/windows): the sync NVENC session reads slices out mid-encode
ci / web (push) Successful in 1m2s
apple / swift (push) Successful in 1m19s
ci / rust-arm64 (push) Successful in 1m45s
ci / docs-site (push) Successful in 2m6s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 16s
ci / rust (push) Canceled after 2m36s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 10s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Canceled after 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
deb / build-publish-client-arm64 (push) Successful in 2m8s
android / android (push) Successful in 5m37s
deb / build-publish-host (push) Successful in 4m40s
deb / build-publish (push) Successful in 5m56s
apple / screenshots (push) Canceled after 6m40s
arch / build-publish (push) Canceled after 8m14s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 6m13s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 6m11s
windows-host / package (push) Canceled after 8m58s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
The Windows direct-NVENC session gets the Linux backend's sub-frame chunked poll: a doNotWait sampling lock cuts each completed slice out of the bitstream while the frame is still encoding, so the send loop's streamed slice wire starts shipping an AU's front before its tail exists. The default retrieve mode on Windows is synchronous, which is exactly the mode the sampler requires - the async opt-in keeps its whole-AU path and the chunked latch drops away when it is active. The session's slice count now honors the negotiation instead of pinning one: the direct-NVENC default of 4 clamped by the client's ceiling, so a single-slice client (Amlogic-class decoders that never advertised multi-slice) keeps today's wire shape and a multi-slice client gets real boundaries to cut at. The pending queue carries a submit-time IDR hint because an AU's early chunks ship before the driver reports the picture type; sub-frame write itself stays env-gated (PUNKTFUNK_NVENC_SUBFRAME=1) until the Windows on-glass A/B validates it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
780791ed81 |
feat(android): the presenter's latch margin starts at zero and earns its width
The 2.5 ms submit margin ahead of expected present assumed SurfaceFlinger latches well before the vsync. On-glass (NP3, 120 Hz) that lead is not needed: with the margin at zero every release still made its vsync (paced=0 across full sessions) - meaning the constant was 2.5 ms of pure display latency on every frame, and it was quantizing away exactly the readiness gains the slice-overlap pipeline just bought. e2e on the Linux-host pairing dropped from 22-24 ms to 14-18 ms. The margin is now adaptive instead of assumed: it starts at 0 and only widens (+500 us per 1 Hz window, capped at the old 2.5 ms) when the paced counter shows real latch misses - one-way per stream, so a margin that proved necessary is never re-gambled mid-session. A device that needs lead converges within seconds; a device that doesn't keeps the full win. debug.punktfunk.latch_margin_us (0..=8000) pins the margin for rebuild-free sweeps, and the presenter logs the resolved mode at stream start. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4240b76182 |
feat(android): slices feed the decoder as they arrive, not when the AU closes
The decode loop used to hold every access unit until its last wire packet landed. With slice-progressive delivery (frame_parts) each newly contiguous piece now goes straight into MediaCodec under BUFFER_FLAG_PARTIAL_FRAME, the closing piece drops the flag, and the decoder chews the front of the frame while its tail is still on the wire. Feeding partial input means owning its failure modes without a codec flush: a broken sequence (dropped piece, orphan, oversize) closes the dead AU with an empty non-partial buffer at its own pts, arms the re-anchor freeze so the concealed output never reaches glass, and requests a recovery keyframe - the same machinery ordinary loss already rides. Per-AU accounting keeps its units: the RFI gap detector notes an AU once, and the HUD stamps, host/network split and the phase-lock arrival sensor ride only the completing delivery. The opt-in is decoder truth and loop truth: every decoder this device would use must pass FEATURE_PartialFrame, and only the async loop may see parts - the legacy sync loop feeds whole AUs and stays that way. Smoke-checked on-glass (NP3 vs the Windows host): byte-identical degenerate path, presenter profile unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
de8430097e |
chore(clients): the bundled licence notices catch up with the root file
ci / web (push) Successful in 1m22s
ci / rust-arm64 (push) Successful in 1m31s
decky / build-publish (push) Failing after 7s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 9s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 12s
ci / docs-site (push) Successful in 1m17s
ci / rust (push) Failing after 2m14s
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 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 9s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 28s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m2s
apple / swift (push) Successful in 3m35s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m28s
deb / build-publish-client-arm64 (push) Successful in 4m4s
docker / builders-arm64cross (push) Successful in 8s
deb / build-publish-host (push) Successful in 5m29s
deb / build-publish (push) Successful in 4m2s
android / android (push) Successful in 7m13s
docker / deploy-docs (push) Successful in 39s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m31s
arch / build-publish (push) Successful in 9m20s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 2m54s
flatpak / build-publish (push) Successful in 6m29s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 3m59s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m40s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m52s
apple / screenshots (push) Successful in 20m57s
windows-host / package (push) Successful in 14m16s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 24s
release / apple (push) Successful in 25m29s
The Apple and Android apps carry their own copy of THIRD-PARTY-NOTICES.txt and show it on the acknowledgements screen. `7fc2775f` refreshed the root file but not the copies — running the python generator directly skips the sync `scripts/gen-third-party-notices.sh` does — so they had drifted to a 531-crate snapshot that predates the whole vendored-source section. That section is where the OS marks' attribution lives, so both apps have been shipping Font Awesome's CC BY 4.0 icons with no attribution visible at all. Re-synced, which also carries the Bazzite Apache-2.0 notice the previous commit adds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5520167958 |
feat(clients): the OS marks tell gaming distros apart, and Windows looks current
Three things were wrong with the host-card OS icons. The Windows mark was Font Awesome 5's, which is still the Windows 8/10 flag with the perspective skew — dated next to the flat four-pane mark Microsoft has shipped since Windows 11. No icon set has the current one (Simple Icons carries no windows/microsoft slug at all), so it is drawn here: four equal squares at the authentic 11.377 + 1.246 proportion. The Decky plugin was pulling FaWindows straight from react-icons, so it now inlines the masters like the web console does, or it would have kept the old flag regardless. Bazzite, CachyOS and Nobara collapsed onto their family's mark. The host already advertises the full chain, so this is purely missing art: all three now ship a leaf mark, because "a Bazzite box" and "a Fedora box" are different machines to the person reading the card. CachyOS and Nobara come from Simple Icons; Bazzite has no icon anywhere, so its "b" is lifted out of the project's own badge (Apache-2.0, attributed). On Android every non-square mark was stretched. A VectorPainter maps the viewport onto the ImageVector's default size with independent x and y scales, so declaring a 448x512 Tux as 24x24 dp squashed it — silently, no crash, no warning. The longest viewport edge now sets the 24 dp box and the other follows the ratio, which is what Icon()'s ContentScale.Fit expects. A unit test pins the invariant; every other client was already correct. Also: scripts/gen-os-icons.sh replaces the undocumented hand-run pipeline that turns a master into the GTK symbolic SVG, the Windows PNG and the Apple template PDF. It reproduces the committed artifacts byte-identically. Verified: web and Decky typecheck, Decky bundles, Android compiles and its tests pass, PunktfunkKit builds, osinfo's tests pass. Not verified on glass, and the GTK/Windows client crates do not build on macOS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f4f83202cb |
feat(core/client): an AU's prefix reaches the decoder while its tail is on the wire
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
ci / web (push) Successful in 1m13s
apple / swift (push) Successful in 1m21s
ci / rust (push) Failing after 2m14s
android / android (push) Canceled after 2m26s
apple / screenshots (push) Canceled after 58s
arch / build-publish (push) Canceled after 2m42s
ci / docs-site (push) Successful in 2m36s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 11s
ci / rust-arm64 (push) Canceled after 2m47s
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 8s
deb / build-publish (push) Canceled after 2m50s
deb / build-publish-host (push) Canceled after 1m38s
deb / build-publish-client-arm64 (push) Canceled after 41s
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) Canceled after 9s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 7s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 10s
docker / builders-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
flatpak / build-publish (push) Canceled after 25s
release / apple (push) Canceled after 0s
windows-host / package (push) Canceled after 3m36s
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) Canceled after 0s
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 7s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
Delivery used to be all-or-nothing: the decoder saw byte 0 only after the last packet of the AU landed, so the whole transmit time sat in front of decode. With the slice-streamed wire (previous commit) blocks now arrive addressable, and a client can opt in (connect's new frame_parts) to receive each AU's newly-contiguous prefix as Frame::part pieces - offset tiling, first/last marked, the completing push carrying only the suffix. A PARTIAL_FRAME-capable decoder then chews slices concurrently with the remaining network transfer. The reassembler walks a per-frame cursor over successfully-completed blocks (failed FEC reconstructs don't advance it), coalesces blocks that finished out of order into one part, keeps probe filler whole, and stops short of the final block so the zero-padded tail still trims at completion. Whole-frame consumers see byte-identical behavior - parts never flow without the opt-in, and never on PyroWave (its newest-wins draining assumes whole AUs). Per-AU accounting keeps its units: OWD/ABR feeds, the inter-arrival series and the clock-based jump-to-live detector only count completing deliveries, and FrameChannel::depth() counts AUs so a part-rich queue can't trip jump-to-live at a fraction of the real backlog. The consumer contract (gap or orphan part = AU lost: abandon, flush, resync on the next first) is documented on FramePart; the C ABI keeps parts off until PunktfunkFrame can express them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
440cb8d1ec |
feat(core/wire): streamed AUs cut blocks at slice boundaries, not at 5.5 MB
The streamed-AU path only ever flushed a block when a full FEC block's worth of bytes accumulated - at real bitrates a frame finished encoding before that, so every AU degenerated to one tail-heavy send and the overlap the sentinel wire was built for never happened. Now a slice boundary flushes every whole shard (floored at MIN_STREAM_BLOCK_SHARDS so per-block parity stays economical), which means blocks are variable-size and can no longer be addressed by the uniform block_index * K_max formula. USER_FLAG_SLICE_STREAM marks such frames on every packet: sentinel blocks reuse frame_bytes as their shard-aligned base offset, the final block's base derives from the totals, and the receiver pins/validates positionally (sentinels must sit strictly below the final block's base; a mixed-flag or out-of-range header is dropped before it can place a byte). The AU's own flag bit gates the wire shape, and the host sets it only toward clients advertising STREAMED_AU + MULTI_SLICE, so shipped receivers keep seeing the byte-identical legacy sentinel (PUNKTFUNK_SLICE_STREAM=0 pins legacy for A/B). Both sides bound the per-frame block count by the same floored formula, and the flush loop cuts as many blocks as a chunk completes - a slice bigger than one FEC block can't leave an oversized final block behind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
43868af1f5 |
feat(android): the client advertises multi-slice tolerance per-decoder
The P0 gating branch deliberately left Android off VIDEO_CAP_MULTI_SLICE
("embedder-set decoder truth") — this is that deferred item, and the P2
slice-pipeline prerequisite. VideoDecoders.multiSliceTolerant() probes the
pick for every advertised codec: tolerant unless any pick is an Amlogic
decoder (the 0.17.0 device-rebooting wedge) or uninspectable (a null pick =
platform default → conservatively single-slice). nativeConnect carries the
bit; the JNI ORs it into the Hello's video_caps beside the panel-truth
HDR bits. Phones (Qualcomm/Exynos) advertise it; the Amlogic TV boxes the
gate exists for never will.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
19392918ff |
feat(gamescope/hdr): HDR is attempted by default and every install path ships the patched gamescope
ci / web (push) Successful in 1m9s
ci / docs-site (push) Successful in 1m14s
ci / rust-arm64 (push) Successful in 3m28s
deb / build-publish-client-arm64 (push) Successful in 2m28s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 10s
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 11s
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 12s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 29s
android / android (push) Successful in 5m47s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m16s
docker / builders-arm64cross (push) Successful in 9s
deb / build-publish (push) Successful in 6m5s
docker / deploy-docs (push) Successful in 35s
deb / build-publish-host (push) Successful in 6m35s
ci / rust (push) Successful in 6m38s
arch / build-publish (push) Successful in 9m13s
apple / swift (push) Failing after 13m37s
apple / screenshots (push) Skipped
windows-host / package (push) Successful in 17m4s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 20s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m17s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 39s
Field report (RX 5700 / SteamOS): 10-bit HEVC "unsupported" — but the GPU was
never the blocker (RADV probed Main10 fine on that box). The session stayed SDR
because stock gamescope offers no 10-bit PQ capture formats and nothing shipped
our pipewire-hdr build outside the Bazzite sysext, while PUNKTFUNK_GAMESCOPE_HDR
additionally defaulted off everywhere. Close all three gaps:
- PUNKTFUNK_GAMESCOPE_HDR now defaults ON (explicit-off grammar, the flip the
config comment planned post-canary). Safe by construction: the +pfhdr banner
probe, managed-spawn term, and client 10-bit cap keep stock-gamescope boxes on
the exact 8-bit path. The nix module's gamescopeHdr (default true now) only
controls whether the patched binary is on PATH; it no longer sets the env.
- SteamOS: new scripts/steamdeck/build-gamescope.sh builds the HDR gamescope in
the same pf2 trixie distrobox (apt dep list validated by a full build in a
debian:trixie container), installs ~/.local/bin/punktfunk-gamescope, and
maintains PUNKTFUNK_GAMESCOPE_BIN in host.env ONLY while the binary passes its
on-glass --version/+pfhdr check — a stale absolute override would break
session spawning, not just HDR. Wired into install.sh (§3b), update.sh
(retrofit), and rebuild-check.sh (ldd-probes it like the host binary).
- Arch: build-sysext.sh gains a --gamescope fold-in (banner-verified by
execution, host-image-only), and punktfunk-host optdepends the companion.
Deliberately NO CI-published host sysext: per the 2026-07-22 packaging
verdict a prebuilt SteamOS host breaks on A/B soname bumps (and /var is
per-partition-set), so arch.yml records that instead — SteamOS hosts use the
distrobox flow above.
Also fixes a latent bug that made build-sysext.sh fail every invocation as
documented: a literal `}` inside `${1:?usage …{host,client}…}` terminates the
expansion early and corrupts $PKG (caught by the new synthetic-package test,
which also covers the fold-in, the unmarked-gamescope refusal, and the
client-image guard).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
78926be4ac |
fix(store): a plugin install lands in the plugins dir, not the nearest package.json above it
ci / docs-site (push) Successful in 2m8s
ci / web (push) Successful in 1m15s
ci / rust-arm64 (push) Successful in 1m25s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 10s
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 15s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 33s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 15s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 21s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 19s
deb / build-publish-client-arm64 (push) Successful in 4m21s
apple / swift (push) Successful in 4m52s
android / android (push) Successful in 5m22s
deb / build-publish (push) Successful in 3m57s
docker / builders-arm64cross (push) Successful in 8s
docker / deploy-docs (push) Successful in 26s
deb / build-publish-host (push) Successful in 3m55s
ci / rust (push) Successful in 9m19s
arch / build-publish (push) Successful in 9m25s
windows-host / package (push) Successful in 14m3s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 25s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m55s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m14s
apple / screenshots (push) Successful in 20m44s
Installing any plugin from the console failed on a user's Fedora 44 host with
plugin store job failed: @punktfunk/plugin-virtualhere is not present after install
while the same version installed fine on every box we tried. The difference turned
out to be a file in his home directory.
`bun add` does not install into its working directory. It walks UP to the nearest
ancestor `package.json` and installs into that tree. A fresh plugins dir has no
`package.json` — the store and the runner only write `bunfig.toml` — so any stray
one above it captures the install: the packages land in `~/node_modules`, the
dependency is written to `~/package.json`, bun prints "installed …" and exits 0,
and the plugins dir stays empty. The plugins-dir `bunfig.toml` is not read in that
case either, so the outcome splits on whether `@punktfunk` resolves in the hijacked
tree: with no scope mapping there it 404s against npmjs and exits 1 ("the plugin
runner exited with status 1"); with one, it succeeds into the wrong tree and the
job dies on the presence check instead. Only the second shape looks like a broken
store, which is why this took a reproduction to find.
Seed the plugins dir with a `package.json` so it owns bun's install root. Three
call sites rather than one: the store job, the `plugins add` CLI, and the SDK's
`ensurePluginsDir`. The runner ships as its own package and can predate the host
binary, so the host cannot delegate this — the same reason `ensure_bunfig_scope`
writes the registry mapping on this side rather than passing a runner flag.
Seeding only ever touches a tree with no `node_modules`. A dir with packages and
no `package.json` is the hand-assembled/older layout that `installed_packages`
deliberately falls back to the naming convention for, and dropping an empty
`dependencies` on it reports every plugin the operator runs as uninstalled —
already pinned by `an_emptied_dependency_list_means_nothing_is_installed`.
The residual failure stops dead-ending: if the presence check fails anyway, the
error names the capturing ancestor and what to do about it.
Reproduced and fixed on glass (Nobara 44 VM, host + runner 0.22.3): with a stray
~/package.json the store reproduces the field report byte for byte — same 150 ms,
same phase sequence, same message — and with the plugins dir seeded the same
install lands correctly with nothing leaking into the home dir.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
759aac255b |
feat(phase-lock): controller v3 — grid-locked submits, arrival sensing, antipode damping
ci / docs-site (push) Successful in 1m17s
ci / web (push) Successful in 2m14s
ci / rust-arm64 (push) Successful in 2m54s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 19s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 25s
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 10s
deb / build-publish-client-arm64 (push) Successful in 2m59s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 13s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
apple / swift (push) Successful in 4m48s
deb / build-publish-host (push) Successful in 4m51s
android / android (push) Successful in 5m31s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 32s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 31s
docker / builders-arm64cross (push) Successful in 12s
docker / deploy-docs (push) Successful in 35s
deb / build-publish (push) Successful in 6m0s
ci / rust (push) Successful in 9m53s
arch / build-publish (push) Successful in 10m32s
windows-host / package (push) Successful in 19m10s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 15s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m17s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m2s
apple / screenshots (push) Successful in 21m3s
Midday on-glass falsified v2's remaining assumptions: a per-frame additive hold SATURATES the arrival-slaved loop once hold + work >= interval — submits free-run at their own cadence and the commanded phase dissolves (measured: +-2 ms hold steps, zero client response) — and the latch statistic hovers at the target's ANTIPODE, where shortest-way errors sign-flip on noise (the 0<->2<->4 ms offset chatter). v3, one change per falsified assumption: - ACTUATOR: submits lock to an absolute grid (epoch + k x period + offset); the controller walks only the offset. A periodic grid cannot free-run — occupancy is one frame per period whatever the offset — so phase actuation is linear BY CONSTRUCTION. Disengaged = no grid sleeps = zero cost; every failure path DISENGAGES (never parks: v2's e2e-tax lesson). - SENSOR: the client reports the circular mean + coherence of the ARRIVAL lead (per-AU reassembly stamps vs the panel latch grid) — the phase the host actually controls; latch measured downstream of the decoder pipeline, which absorbed the actuation. pf.phase logs "arrival lead circ= coh=". - ANTIPODE DAMPING: errors within 1 ms of +-period/2 take half-steps until they commit to a side. Harness rewritten around plants glass validated: a GRID plant (linear by construction), the DECOUPLED plant (v2's saturation — must disengage, not orbit or park), an antipode start (must converge within one period of travel), incoherence (never engages), a regime change (re-engages after backoff), and the actuator's own periodicity/offset-linearity. Also fixes the harness's SIM_TARGET (a mis-derived max asserted 3.5 ms where the controller's actual target is 2.5 — the controller was right, the ruler was wrong; the Python cross-check inherited the same constant, a lesson in replicating the CODE, not the author's belief about it). Gates: v3 simulations 6/6; host bin suite 345/1 (the documented environmental qemu UDP test); clippy --all-targets -D warnings clean; cargo ndk arm64 check clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4457356ee4 |
test(phase-lock): a closed-loop simulation harness — both on-glass failures become CI regressions
ci / web (push) Successful in 1m7s
ci / docs-site (push) Successful in 1m14s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 23s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 27s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 12s
ci / rust-arm64 (push) Successful in 2m48s
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 10s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 22s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 25s
deb / build-publish-client-arm64 (push) Successful in 2m48s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m1s
android / android (push) Successful in 5m31s
deb / build-publish-host (push) Successful in 6m5s
docker / builders-arm64cross (push) Successful in 18s
apple / swift (push) Successful in 4m41s
docker / deploy-docs (push) Successful in 46s
ci / rust (push) Successful in 6m45s
deb / build-publish (push) Successful in 5m9s
arch / build-publish (push) Successful in 9m4s
flatpak / build-publish (push) Successful in 6m54s
windows-host / package (push) Successful in 11m3s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 25s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m10s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m35s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m13s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m27s
release / apple (push) Successful in 25m17s
apple / screenshots (push) Successful in 21m0s
The controller iterated twice on the user's gaming PC before this existed; never again. punktfunk_core::phase::circular_latch is extracted from the Android presenter (shared: iOS reports next, and the harness generates its synthetic reports through the IDENTICAL statistic the clients ship), with unit tests including the wrap-straddling cluster an arithmetic mean gets maximally wrong. The stream.rs harness models the plant — latch = (base − hold + noise) mod P, deterministic LCG noise — and drives PhaseController::adjust through five regimes: - tight jitter locks into the deadband within ~5 adjusts and stops moving; - a wrap-side start takes the SHORTEST way (travel < P/2 asserted); - an incoherent phase never steps (zero hold, ever); - a v1 pinned-median report trips the travel budget and DECAYS TO ZERO — the 2026-07-31 orbit and the parked-hold e2e tax, both as asserts; - a post-decay regime tightening re-locks. Gates: host bin suite 344 passed / 1 failed = the documented environmental qemu UDP-loopback test (clean main fails identically in this container); core phase tests 4/4; clippy --all-targets -D warnings clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fe552a50bb |
fix(android): the phase reporter actually sends the v2 circular statistic
ci / rust-arm64 (push) Successful in 2m45s
ci / web (push) Successful in 1m15s
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 11s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 12s
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 11s
ci / docs-site (push) Successful in 1m29s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 18s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 37s
docker / builders-arm64cross (push) Successful in 18s
android / android (push) Successful in 6m17s
ci / rust (push) Successful in 9m44s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 12m57s
docker / deploy-docs (push) Canceled after 0s
The v2 client edit missed its anchor against rustfmt's reshaping and the reporter kept sending the v1 median — caught by the arm64 build the moment the tuple signature landed. Sends (circular mean, coherence) now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1d31e4c565 |
feat(phase-lock): controller v2 — circular phase, coherence gate, decay-to-zero
ci / web (push) Successful in 1m9s
ci / docs-site (push) Successful in 1m34s
ci / rust-arm64 (push) Successful in 1m39s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 7s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 7s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 7s
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 5s
android / android (push) Failing after 2m21s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 17s
ci / rust (push) Canceled after 2m52s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 34s
docker / builders-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
apple / swift (push) Successful in 4m47s
deb / build-publish-client-arm64 (push) Successful in 3m59s
deb / build-publish-host (push) Successful in 4m29s
deb / build-publish (push) Successful in 5m59s
flatpak / build-publish (push) Successful in 4m58s
arch / build-publish (push) Successful in 10m18s
windows-host / package (push) Successful in 11m6s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 33s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m8s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m9s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m41s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m12s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m46s
apple / screenshots (push) Canceled after 0s
release / apple (push) Canceled after 19m16s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 21m16s
Round-1 on-glass falsified two v1 assumptions in one morning (NP3 ↔ .173): - A median lead is immovable under period-spanning jitter (uniform mod P), so v1 orbited the period at 2 ms/s forever — and the dead-signal freeze hotfix never armed, because ±1.5 ms of window sampling noise kept resetting its 'error responded' check. Thresholds can't referee noise. - A HELD hold is not free: it delays sampling after the capture stamp, taxing e2e by up to a period (user-measured: ~+4 ms during the orbit). The correct failure response is DECAY TO ZERO, never freeze-in-place. v2, all three layers: - Wire: PhaseReport grows a length-discriminated coherence tail (27-byte form; the u16::MAX sentinel encodes as the byte-identical 25-byte v1 — strict-prefix discipline, both forms pinned by tests). - Client: the reporter sends the CIRCULAR vector-mean latch phase mod the panel period + its coherence (‰); pf.present logs circ=/coh=. - Host: steps only while coherent (floor 300‰; a v1 report bypasses the gate), along the signed SHORTEST way around the period, under a cumulative 1.25-period travel budget that is noise-immune by construction (it integrates applied steps, not reported errors); incoherence or budget exhaustion decays the hold to zero and re-arms once flat. Deadband convergence resets the budget — tight regimes lock exactly as designed. report_phase + the C ABI mirror gain the coherence parameter (canary-only ABI, hours old, sole caller in-tree). Gates: docker amd64 clippy --all-targets -D warnings (host+core, nvenc) clean; core suite 244/244; cargo ndk arm64 check clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a5896f0883 |
feat(android): the phase-lock ACK is logged — the closed loop reads out in logcat
ci / web (push) Successful in 1m6s
ci / docs-site (push) Successful in 2m8s
deb / build-publish (push) Successful in 4m17s
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 12s
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 11s
deb / build-publish-host (push) Successful in 4m25s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 12s
ci / rust-arm64 (push) Successful in 5m46s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
deb / build-publish-client-arm64 (push) Successful in 3m40s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 31s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 30s
docker / builders-arm64cross (push) Successful in 11s
docker / deploy-docs (push) Successful in 50s
arch / build-publish (push) Successful in 7m59s
android / android (push) Successful in 8m20s
ci / rust (push) Successful in 9m33s
apple / swift (push) Successful in 5m3s
windows-host / package (push) Successful in 12m57s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 30s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m30s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m59s
apple / screenshots (push) Successful in 21m18s
The host's applied hold rides the 0xCF tail but nothing client-side showed it; 'adb logcat -s pf.phase' now logs transitions. This line is what exposed the dead-median controller orbit on the first on-glass run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
74ca42568a |
fix(host/phase-lock): a dead error signal freezes the hold instead of orbiting the period
First on-glass closed-loop run (NP3 ↔ .173, canary 0.23.10498) proved the transport and both endpoints work — and exposed the controller chasing a statistic that cannot respond: under period-spanning arrival jitter the client's latch DISTRIBUTION is ~uniform mod the refresh, so shifting its mean (the hold — provably reaching the client: the paced-drop bursts locked to each wrap) cannot move its MEDIAN. The controller stepped +2 ms/s in one direction forever, wrapping the period every ~4 s, and every wrap coalesced ~30 frames at the client — a periodic hiccup that must not ship. The controller now remembers its last error: three consecutive steps that fail to move it ≥1.5 ms freeze the hold (kept, not zeroed) and log; a later report whose error moved re-arms stepping — a tight-jitter regime still locks exactly as designed. Controller v2 (a circular phase statistic with a coherence gate, per the design doc's follow-ups) replaces the median outright; this freeze stays as its safety net. Gate: docker amd64 clippy --all-targets -D warnings (host, nvenc) clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
21787ca6cd |
feat(decky): the plugin updates the actual client, not only the flatpak one
audit / bun-audit (plugin-kit) (push) Successful in 17s
audit / bun-audit (sdk) (push) Successful in 20s
audit / bun-audit (web) (push) Successful in 27s
audit / pnpm-audit (push) Successful in 18s
audit / cargo-audit (push) Successful in 49s
audit / docs-site-audit (push) Successful in 42s
apple / swift (push) Successful in 1m18s
ci / web (push) Successful in 1m1s
ci / docs-site (push) Successful in 2m10s
audit / license-gate (push) Successful in 5m17s
ci / rust-arm64 (push) Successful in 5m33s
decky / build-publish (push) Successful in 35s
deb / build-publish (push) Successful in 5m10s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 7s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 5s
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 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
deb / build-publish-client-arm64 (push) Successful in 2m16s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 48s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m10s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 26s
android / android (push) Successful in 9m7s
docker / builders-arm64cross (push) Successful in 8s
arch / build-publish (push) Successful in 9m19s
docker / deploy-docs (push) Successful in 31s
deb / build-publish-host (push) Successful in 7m31s
windows-host / package (push) Failing after 14m54s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
flatpak / build-publish (push) Successful in 7m57s
ci / rust (push) Successful in 15m50s
release / apple (push) Successful in 18m41s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 7m8s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m10s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m52s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 2m49s
apple / screenshots (push) Canceled after 9m43s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 3m55s
The QAM already offered a client update — for the flatpak, by comparing OSTree commits. A native client (.deb, .rpm, pacman, sysext, nix, a source build), which the plugin has been able to LAUNCH since it learned to drive one, went stale in silence: no version, no check, no mention. check_update now branches on the client the launcher will actually run. The flatpak keeps its commit comparison, which is the exact one — a flatpak built from main between releases carries the release's crate version, so a version-based check would call it current when it isn't. Everything else asks the client itself, which verifies the signed manifest. update_client routes the same way: flatpak update --user, or `punktfunk-client --apply-update`. Where nothing on the box can install it, the plugin says so instead of offering a button. The QAM banner becomes the command, the About tab names the install kind so "why is there no Update button?" has a visible answer, and a check that FAILED reads as a failure — never as "up to date". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b164b4c03e |
feat(packaging): a client-only box can update its client without the host
The root helper, its unit, the group-scoped polkit rule and the install-kind
marker all shipped with the HOST package. A Steam Deck or a handheld has no host
package, so the client half of one-tap updates had nothing to stand on.
The client packages now ship their own copies — deliberately under their own
paths (`pf-update-client`, `punktfunk-client-update.service`,
`49-punktfunk-client-update.rules`, `/usr/share/punktfunk-client/install-kind`)
rather than sharing the host's. Two packages owning one path is a hard conflict
in dpkg, rpm and pacman alike, and the client marker needs its own DIRECTORY
too because the host RPM claims `%{_datadir}/punktfunk/*` with a glob — a
sibling file there would break `dnf install punktfunk punktfunk-client`.
`pf-update` grows the matching `apply-client` verb. The verb comes from a
root-owned unit's fixed ExecStart, never from the caller, so the zero-parameter
invariant holds; both verbs sweep every installed punktfunk package, and what
the verb selects is which marker is read and which binary the run-the-binary
gate executes afterwards. A client sysext is refused with its reason: the signed
feed carries the host image, and installing that over a client-only box is not
an update.
The opt-in group is created by both packages' scriptlets, idempotently, so
whichever lands first wins.
Also fixes the host's own helper install: it read `$R/target/release/pf-update`,
which is the source checkout, while build() exports CARGO_TARGET_DIR="$srcdir/
target" and every other binary in the file installs from `$T`. Both now read
`$T`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|