cacfe04a93e27ba18053d0bc16799726f86c0e39
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7798401f06 |
perf(apple): cache posters on disk and pool the mgmt connections
ci / rust-arm64 (pull_request) Successful in 1m36s
ci / bun-nix (pull_request) Successful in 24s
ci / web (pull_request) Successful in 1m16s
apple / swift (pull_request) Successful in 1m38s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m12s
ci / rust (pull_request) Successful in 13m3s
Moving the management API onto Network.framework left one request per connection, so a library grid paid a TLS handshake per poster where the pooled URLSession had shared one. And the Apple client -- unlike Windows -- never cached art at all, so it re-fetched every poster on every visit. ArtCache: a size- and age-bounded blob cache in the CACHES directory (every byte is re-derivable from the host, so the system is welcome to evict it). Keyed by the SHA-256 of the absolute URL, so host-proxy paths and store CDN URLs share one cache without colliding. Reads touch the entry, so eviction is by last USE, not last write. Empty bodies and data: URLs are refused -- neither is worth a file. Defaults: 128 MB, 30 days. Connection pooling: MgmtConnectionPool keeps up to four keep-alive connections per host and makes further callers wait rather than opening more, which is the part that matters -- a grid can ask for dozens of posters at once. A connection the host dropped since we last used it is indistinguishable from a live one until we write, so a REUSED connection that fails is retried once on a fresh one; a fresh failure is a real failure. Keep-alive means a response can no longer be delimited by the peer hanging up, so HTTPResponseParser.messageLength finds the end from the framing itself -- Content-Length or the chunked terminal chunk plus trailers. Getting that wrong would truncate a response or bleed one into the next, silently, so it carries the bulk of the new tests. A connection with bytes left over after a response is dropped rather than reused: we never pipeline, so anything trailing means we are out of sync. LibraryView closes the loader's pooled connections on disappear instead of leaving sockets open on a screen the user has left. 16 new tests: message framing (both encodings, partial reads, back-to-back responses, close detection) and the cache (binary round trip, key separation, refusals, expiry, LRU eviction). |
||
|
|
244cafe005 |
refactor(apple): move the mgmt API off URLSession so ATS can stay on
The previous commit bought the library back on VPN/remote hosts by declaring NSAllowsArbitraryLoads, which works but is blunt: it drops ATS for ALL of the app's URLSession traffic, and the only other traffic is third-party cover-art CDN fetches -- the one surface we never wanted to open. It cost the TLS-version floor, forward secrecy, and the cleartext-HTTP block on URLs the host supplies at runtime (custom entries and scanner plugins carry arbitrary ones). So take the host out of the URL loading system instead. MgmtTransport speaks HTTPS over Network.framework, which ATS does not govern, and states the trust rule we actually mean in a verify block: the leaf must hash to the fingerprint pinned during PIN pairing. That is the same rule punktfunk-core has always applied on the QUIC stream plane -- which is exactly why streaming kept working over Tailscale while the library did not. With that, the ATS dict is gone and ATS is fully enforced again. Cover-art CDN fetches keep ordinary URLSession with full system trust evaluation and no client certificate. LibraryTLSDelegate is deleted; nothing pins through URLSession now. Also here: - HTTPResponse: just enough HTTP/1.1 to read one GET -- status, headers, Content-Length and chunked framing (hyper streams the art proxy chunked). A body shorter than Content-Length throws instead of returning partial JSON, which would otherwise read as "this host has no games". - LibraryError.pinMismatch, so a re-keyed host says "pair again" rather than sending someone to debug their network. - 403 joins 401 as "unauthorized": both are the host declining the certificate. - baseURL brackets IPv6 literals; the old string interpolation did not. - 11 tests covering the framings hyper emits and the failure modes that would otherwise be silent. Known trade-off: no connection reuse yet, so each poster costs its own handshake where the pooled URLSession shared one. Fine on a LAN, worth revisiting for large libraries over a high-latency link. |
||
|
|
bae8742e48 |
fix(apple): the library's unreachable message pointed at the wrong layer
It led with `--mgmt-bind 127.0.0.1`, a rare cause, and never mentioned the one fact that actually explains the symptom: the library rides the management API on a different port (47990) than the QUIC stream plane (9777), so it can fail while streaming to the same host works. Field triage of exactly that case spent hours on the stream path before anyone questioned the port. Leads with that now, and names browser-testing the port as the fast split between "unreachable" and anything client-side. |
||
|
|
883c317872 |
feat(clients/library): a launcher tile looks like one, on every client
The host has been able to describe a launcher entry since M2 — `role: "launcher"`,
the `steam_ui` and `launcher_ui` kinds — and the web console has grouped them into
their own rail since M4. No other client ever looked. `pf-client-core` decoded
`role` into an `is_launcher()` helper with zero call sites, and the shared console
model dropped the field entirely on its way to the renderer.
So a launcher tile arrived everywhere else as an ordinary game with no cover art:
indistinguishable from a title whose poster failed to load, sorted into the middle
of the alphabet, and captioned "Play".
One contract, implemented in each client's own idiom:
* launchers never interleave with titles — they lead, and each group keeps the
host's title order
* grid surfaces get a labelled section; a coverflow keeps its single carousel and
names the group the cursor is in, changing as it crosses the boundary. A second
focus rail would mean a new up/down nav model in three renderers for two or
three tiles
* an art-less launcher gets an accent face naming its launcher, not a title
monogram on the neutral one — "opens Steam", not "a cover that didn't load"
* anything that is not `"launcher"` is a game, and a host that omits the field
renders exactly as before (design D4's intended degradation)
* launching is unchanged: the client sends an id, the host resolves the recipe
The grouping is enforced once per client stack rather than per screen. In the
console UI it is an invariant of `LibraryShared::set_games`, so the cursor
arithmetic, the art pump and every future consumer inherit it; on Apple and Android
it is applied where the library is fetched/parsed.
Fixed in passing: the Apple and Android store badges were hard-coded
`isCustom ? "Custom" : "Steam"`, so every Lutris, GOG, Heroic, Epic and Xbox title
was labelled "Steam". Both now carry the same store table the Rust clients use.
The CLI's `--library` gains a fourth column (`game`/`launcher`), appended rather
than folded into an existing one so anything reading the first three is untouched.
Gates: punktfunk-host 436 passed / 0 failed and pf-console-ui 49 passed / 0 failed
on .21 (three new tests), workspace clippy -D warnings and cargo fmt --check clean
there; `swift build` of the full PunktfunkClient and `:app:compileDebugKotlin` clean
on macOS; `cargo check` + `clippy -D warnings` for the Windows client on .173.
Still unproven on hardware: no launcher tile has been clicked on a real host — that
needs the plugins published, which needs this branch's base merged first.
|
||
|
|
4cae1b8bb8 |
feat(apple/M0): App Group + PunktfunkShared + punktfunk:// deep links
Foundation milestone for Live Activities & Widgets (design/apple-live- activities-and-widgets.md). No user-visible change beyond the URL scheme. - New dependency-free PunktfunkShared SwiftPM target (+ library product) so a future widget extension can link it WITHOUT PunktfunkKit (Rust staticlib + presentation layer). Moves StoredHost (model + JSON codec), DefaultsKeys, and punktfunkDefaultMgmtPort there; adds AppGroup.suiteName and the punktfunk:// DeepLink builder/parser. PunktfunkKit @_exported-imports it (no call-site churn for consumers; intra-Kit files import it explicitly since imports are file-scoped). - HostStore reads/writes the shared App-Group suite (group.io.unom.punktfunk) with a one-time migration from UserDefaults.standard (old value left in place for staged rollout); reloads the "PunktfunkHosts" widget timeline on change. - App Group entitlement on iOS/tvOS + macOS. - CFBundleURLTypes scheme `punktfunk`; ContentView.onOpenURL routes connect/<uuid>[?launch=<GameEntry.id>] into the existing connect() path (unknown host / already-streaming guards; never tears down a live session). - Round-trip tests: StoredHost JSON codec (+ legacy missing-optional decode), DeepLink grammar. `swift build` + `swift test` green (142 tests, 0 failures). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
34fe896ade |
feat(apple): gamepad UI v2 — controller settings + add host, aurora, macOS
Sources reorganized (client: Home/Session/Settings/Stores/Support/Trust; kit: Audio/Connection/Gamepad/Input/Support/Video/Views) with the big files split along the same seams. The gamepad mode is couch-complete, and now on macOS too (the living-room Mac case), not just iOS/iPadOS: - GamepadSettingsView: a console-style, fully controller-navigable settings screen (X from the launcher) — up/down moves focus, left/right steps values (clamped, boundary thud), A cycles/toggles, B closes; the focused row shows a one-line description. Backed by GamepadMenuList, the vertical sibling of GamepadCarousel, and SettingsOptions — the option lists hoisted out of SettingsView statics and shared by the touch, tvOS and gamepad settings. - GamepadAddHostView + GamepadKeyboard: register a host end to end with a pad — field rows open an on-screen controller keyboard (dpad grid, A types, X backspaces, B done); the launcher carousel ends in an Add Host tile, so the dead-end "add one with touch first" empty state is gone. - Launcher polish: contextual hint bar with the pad's real button glyphs, controller name + battery chip, one shared console chrome. - GamepadScreenBackground: an animated aurora (TimelineView-driven drifting blobs in the brand's violet family, breathing radii, slow hue shift, legibility scrim; freezes under Reduce Motion). Pure SwiftUI on purpose — a .metal library only bundles reliably in one of the two build systems (SPM vs the xcodeproj's synced folders) these sources compile under. - macOS port: settings/add-host/library present as sized sheets (a macOS sheet takes its content's IDEAL size, and the GeometryReader-driven screens collapsed to nothing), NSScreen-based mode lists, scroll indicators .never (the "always show scroll bars" setting overrides .hidden), tray scrims so scrolled rows dim under the pinned title/hints, extra title clearance, and a PUNKTFUNK_FORCE_GAMEPAD_UI=1 dev hook — launcher/settings/add-host/keyboard/ library render-verified live on a real Mac + LAN hosts. - GamepadMenuInput: X button support, and (re)start now snapshots held buttons so a controller handoff press never fires twice (the B that closed the keyboard no longer also cancels the screen underneath). - Cleanups: one "Connection failed" alert in ContentView instead of one per home screen; HostDiscovery.advertises/unsaved shared by both home screens. - host: can_encode_444 stub for the non-Linux/Windows host build (the macOS synthetic-source loopback used by the Swift tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |