CachyOS ships ufw enabled by default (firewalld is not installed) — verified live
on the .21 box — but the docs and shipped firewall openers claimed "CachyOS enables
firewalld by default". Correct that everywhere and ship a ufw application profile
(the one-liner analogue of the firewalld service files):
- packaging/linux/punktfunk.ufw (new): [punktfunk-native], [punktfunk-gamestream],
[punktfunk-web] profiles, installed to /etc/ufw/applications.d/punktfunk by the
Arch (CachyOS) and .deb host packages. `sudo ufw allow punktfunk-native`.
- packaging/linux/punktfunk-web.xml (new): firewalld service for the optional web
console (TCP 47992), installed by the host package on arch/deb/rpm. Neither the
native nor gamestream opener covered 47992, so a firewalld/ufw host that enabled
punktfunk-web could not reach the console over the LAN.
- Fix the "CachyOS enables firewalld" claim in arch.md, arch/README.md,
debian/README.md, both firewalld service .xml comments, and the pacman scriptlet;
firewalld now attributed to the spins that use it (EndeavourOS, Fedora/RHEL).
- Docs present both one-liners (ufw + firewalld) whichever firewall you run, plus a
console-opener step; postinst/scriptlet hints detect ufw as well as firewalld.
The native data plane stays hole-punched (ephemeral UDP, no fixed port) — its
openers correctly open only 9777/udp + mDNS; the stale "open a UDP range" note is
replaced with the accurate outbound-UDP explanation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the Arch firewalld service definitions into the RPM spec and the Debian
host package so every Linux packager installs them, and move the two XML files
to the shared packaging/linux/ home (alongside the .desktop files both the
PKGBUILD and deb scripts already source there) so there's one source of truth
instead of three drifting copies.
- rpm: install punktfunk-{gamestream,native}.xml to /usr/lib/firewalld/services/,
list them in %files host, and print the firewalld enable command in %post
(gated on firewall-cmd). Fedora/RHEL run firewalld by default, so this is where
it matters most; Bazzite inherits it via the sysext built from the package /usr.
- deb: install both XMLs in build-deb.sh and add the same firewalld-gated hint to
the postinst. Debian/Ubuntu ship no active firewall, so it's a no-op unless the
admin runs firewalld.
- PKGBUILD + arch README updated to the packaging/linux/ path.
- Firewall docs (bazzite README now leads with --add-service; debian README gains
a firewalld block) point at the shipped services; XML comments made
distro-neutral. Never auto-enabled — packages don't touch the admin's firewall.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pacman-repo setup step used a bash heredoc (`<<'EOF'`), which fish — the
default shell on CachyOS — cannot parse ("expected a string, but found a
redirection"). Replace it with a cross-shell `printf | sudo tee -a` form in both
the Arch guide and packaging/arch/README.md; `$repo`/`$arch` stay literal for
pacman and the output is byte-identical to the old heredoc.
Firewall: stock Arch ships none (ports already open), but CachyOS enables
firewalld by default and an Arch package must never touch the running firewall.
Ship firewalld service definitions the host package installs to
/usr/lib/firewalld/services/ (punktfunk-gamestream, punktfunk-native), not
auto-enabled; the install scriptlet prints the enable command only when
firewall-cmd is present. Document it in the Arch guide (new section) and README.
The mgmt API (loopback) and web console ports are deliberately not opened.
Also fix the "GTK4 couch/Deck client" mislabel — it's the native
GTK4/libadwaita Linux client (desktop/laptop/Deck are targets; the
controller-optimized launcher is one view, not its identity) — across the Arch
PKGBUILD/README, Arch guide, and the Debian README.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New crates/punktfunk-tray — a small per-user companion showing the host service
state at a glance (running / stopped / starting / degraded / failed + the live
session in the tooltip) with one-click actions: open web console, approve a
pending pairing request, start/stop/restart, open logs. No more digging through
logs to learn whether the service came back after a reboot or an update.
Status is service-manager-FIRST (SCM / systemd user unit — a port squatter can
never fake Running), then the new loopback-only unauthenticated
GET /api/v1/local/summary (counts/booleans only; the mgmt token and cert.pem
are SYSTEM/Admins-DACL'd on Windows, so a non-elevated tray cannot bearer-auth).
Windows: windows_subsystem binary (a console exe in the Run key would flash a
terminal at sign-in), Shell_NotifyIcon + hidden window, per-session single
instance, TaskbarCreated re-add, --quit for the uninstaller; service actions
elevate per click via ShellExecuteW "runas" onto the new
`punktfunk-host service restart` (stop → wait Stopped → start).
Linux: ksni/StatusNotifierItem over zbus, systemctl --user actions (no polkit),
/etc/xdg/autostart entry whose --autostart self-gates to actual host users.
Icons: scripts/gen-tray-icons.py (pure stdlib) renders the brand lens + status
dot into committed .ico/hicolor assets; deb/rpm/arch ship binary+autostart+icons.
Live-validated: Linux on the headless KDE session (SNI registration, state
transitions, menu-driven start, dbusmenu layout); Windows on the RTX box
(session-1 launch with no NIM_ADD failure, single instance, --quit, restart
round-trip, summary loopback-200/LAN-401).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port 3000 collides with half the dev-server ecosystem; 47992 sits next
to the mgmt API (47990) in the punktfunk port family. Updates the run
scripts, systemd/scheduled-task units, Dockerfile, Windows firewall
rule + installer, packaging, and every doc that referenced :3000.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a client-selectable **preferred codec** and wires the core + ABI + probe + Linux client to
negotiate and decode it. (Windows/Apple/Android follow in 2b.)
**Core:**
- `Hello.preferred_codec` (a single CODEC_* bit, 0 = auto) — a soft hint appended after
`video_codecs`. `resolve_codec(client, host, preferred)` now honors the preference when the host
can also emit it, else falls back to precedence (HEVC > AV1 > H.264). Roundtrip + preference tests.
- `NativeClient::connect` takes `video_codecs` + `preferred_codec`; `NativeClient.codec` exposes the
resolved `Welcome.codec`.
- ABI: `punktfunk_connect_ex7` (adds the two codec params; `ex6` delegates to it advertising
HEVC-only) + `punktfunk_connection_codec` getter + `PUNKTFUNK_CODEC_{H264,HEVC,AV1}` constants
(drift-guarded against the wire values). Header regenerated.
**Host:** passes `hello.preferred_codec` into `resolve_codec`.
**probe:** `--codec h264|hevc|av1|auto` sets the preference (still advertises it can decode all
three); the dump extension already follows the resolved codec.
**Linux client:** advertises the codecs FFmpeg can actually decode (`decodable_codecs()`), threads
the user's `codec` setting as the preference, and builds the decoder — both the software and VAAPI
paths, plus the mid-session VAAPI→software demotion — from the negotiated `Welcome.codec` instead of
hardcoding HEVC. New "Video codec" dropdown in Preferences (Automatic/HEVC/H.264/AV1).
Live-validated on the dev box: probe `--codec hevc` against a software (H.264-only) host resolves to
H.264 (graceful soft-preference fallback), no failure. clippy + core (57) + host (133) tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Web console
- Pairing/Library/Stats refactored into self-contained subsections that each own
their own queries + mutations; a shared slot-based layout (view.tsx) is filled by
the live page (containers) and Storybook (pure cards + fixtures) so the layout can't
drift.
- All paired devices in one list on Pairing with a protocol column (punktfunk/1 +
Moonlight), routing each unpair to the right endpoint; the redundant Clients page is
removed.
- Library: overview grid split from the add/edit form into separate files.
- Login screen links out to the docs.
Docs
- "Console login password" section on every host page (apt/RPM/Bazzite/SteamOS/Windows)
plus a new "Forgot your Password?" troubleshooting page, linked from the login screen.
- Console served as HTTP/1.1 over TLS (drop the unusable HTTP/3 advertising) across the
Bun entry, launchers, systemd units, and packaging.
Tooling
- Biome now respects .gitignore (stops linting generated code), config migrated to
2.5.1; all lint issues fixed cleanly.
Also includes this branch's in-progress host, Apple client, packaging, and CI changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MIT OR Apache-2.0 SOURCE license is clean (audit found no copied copyleft); the
gaps were all binary-distribution (Layer-2). This makes the shipped artifacts honest:
- Windows host + client: bundled FFmpeg BtbN gpl-shared -> lgpl-shared (AMF/QSV/decode
unaffected; the GPL-only x264/x265 were never used), and ship the FFmpeg LGPL notice
+ license text in the installer + MSIX (licenses/).
- THIRD-PARTY-NOTICES.txt generated + bundled into installer/MSIX/deb/rpm. Offline
generator (scripts/gen-third-party-notices.{py,sh}) + cargo-about config (about.toml/
.hbs) with a permissive-only accepted-license allow-list as a copyleft regression gate.
- Reword the win32u GPU-preference hook comments to reflect independent reimplementation
(no Apollo/Sunshine GPL-3.0 source copied).
- README dual-license + inbound=outbound contributor clause + non-affiliation trademark
disclaimer; new CONTRIBUTING.md.
- LICENSE files into the standalone driver + vk-layer workspaces; deb copyright holder
aligned to "unom and the punktfunk contributors".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Streaming the KDE *Desktop* (KWin) session failed on a real interactive
Plasma session with "KWin does not expose zkde_screencast_unstable_v1":
KWin treats the screencast/virtual-output and fake_input globals as
restricted and advertises them only to a client whose installed .desktop
lists them under X-KDE-Wayland-Interfaces (matched by /proc/<pid>/exe ->
Exec, and cached per-executable on first connect). The host shipped no
.desktop, so it was permanently denied; it only ever worked on the
headless dev box via KWIN_WAYLAND_NO_PERMISSION_CHECKS=1.
Ship packaging/linux/io.unom.Punktfunk.Host.desktop (least-privilege:
only the host, only zkde_screencast_unstable_v1 + org_kde_kwin_fake_input)
and install it from the RPM/.deb/Arch host packaging so it is present
before the host first connects. Drop the blunt session-wide
NO_PERMISSION_CHECKS hack from kde-desktop-setup.sh (it now only seeds the
RemoteDesktop input grant) and fix the now-misleading kwin.rs docs/errors.
Validated live on a Bazzite Kinoite box (KWin 6.6.4): probe-compositor +
spike --source kwin-virtual succeed against a KWin running WITHOUT the
permission bypass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The file moves (docs/ → design/, docs/api/openapi.json → api/openapi.json) landed
in 8897286, but the matching reference updates did not — so mgmt.rs's drift-test
`include_str!("../../../docs/api/openapi.json")` pointed at a path that no longer
exists and the host failed to build. This restores it and updates every reference:
- mgmt.rs include_str! → ../../../api/openapi.json (fixes the build)
- web/orval.config.ts codegen target, web/Dockerfile, .dockerignore
- deb/rpm/Arch packaging install paths
- CLAUDE.md, the .gitea CI workflows, code doc-comments, design-doc cross-links
docs-site route URLs (/docs/...) untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A push to main publishes canary builds to canary channels (fast iteration,
unchanged); a single vX.Y.Z tag releases every platform at one version to the
stable channels and attaches all artifacts (.deb/.rpm/.msix/.apk/.aab/.dmg +
flatpak/decky/host-installer) to one Gitea Release. Collapses the
host-v*/win-v*/host-win-v* tag namespaces into v* — the channel split makes the
version-shadow bug structurally impossible (canary and stable are separate repos,
never a shared version line).
- scripts/ci/gitea-release.{sh,ps1}: one idempotent release helper
(create-or-fetch + delete-before-upload), replacing 3 copy-pasted inline blocks
and fixing their latent 409-on-reupload bug; prerelease flag auto-derived from
the tag (an -rc tag won't shadow "Latest")
- channels: apt canary/stable distributions; rpm *-canary/base groups; flatpak
canary/stable OSTree branches + a 2nd .Canary.flatpakref; generic-registry
canary/ vs latest/ aliases; Play internal/alpha; Apple TestFlight vs notarized DMG
- android versionName threaded through gradle (versionCode stays run_number);
Apple canary = TestFlight-only (no DMG/tvOS); canary base bumped to 0.3.0
- docs: new docs-site channels.md (subscribe table + cut-a-release runbook +
box migration), refreshed ci.md workflow table + packaging READMEs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follows the security audit (#5/#9): the GameStream-compat plane carries inherent on-path weaknesses
that can't be fixed on the wire without breaking stock Moonlight — its pairing runs over plain HTTP
(#9, MITM-able during the pairing window) and its legacy control encryption can reuse GCM nonces (#5,
a passive eavesdropper can recover/forge input). The native punktfunk/1 plane (SPAKE2 PIN pairing +
per-direction AEAD nonces) has neither. So flip the default to secure-by-default:
- `serve` → native punktfunk/1 plane + management API ONLY (no GameStream surface).
- `serve --gamestream` → ALSO the GameStream/Moonlight-compat planes (nvhttp pairing, RTSP, ENet
control, _nvstream mDNS). Opt-in, logged with a trusted-LAN caveat. `--moonlight` is an alias.
- The native plane is now ALWAYS on in `serve` (`--native` is a kept-for-compat no-op); the unified
GameStream+native host is `serve --gamestream`.
`gamestream::serve` gates the GameStream spawns (nvhttp/rtsp/control/mdns) on the flag; the native
plane + mgmt + native-pairing handle always run.
To avoid silently regressing validated Moonlight deployments, the explicit deployment configs PRESERVE
Moonlight via `--gamestream` (each documents dropping it for a secure native-only host): the Linux
systemd unit, the Steam Deck installer, and the Windows service default (DEFAULT_HOST_CMD). The bare
`serve` default (new/manual use) is secure.
Docs swept to match (host-cli, moonlight, quickstart, install, packaging READMEs, CLAUDE.md, README,
…): Moonlight setup now instructs `--gamestream`; native/console refs use bare `serve`. OpenAPI
regenerated (a stale "run `serve --native`" string). fmt + clippy clean; 94 host tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Polish for AMD/Intel support:
- GameStream serverinfo advertises only codecs the GPU can ACTUALLY encode on
the VAAPI backend (probed once by opening a tiny encoder per codec). AV1
encode is narrow (Intel Arc/Xe2+, AMD RDNA3+/RDNA4) and an old iGPU may lack
HEVC, so a Moonlight client never negotiates a codec the encoder can't open.
NVENC/Windows keep the Moonlight-validated static mask. Validated on a Radeon
780M: h264/h265/av1 all probe true -> mask unchanged (65793).
- Packaging: Recommends mesa-va-drivers + intel-media-va-driver (deb) /
mesa-va-drivers + intel-media-driver (rpm) so the auto-selected VAAPI backend
works out of the box on AMD/Intel; NVIDIA boxes can --no-install-recommends.
(Fedora note: stock mesa-va-drivers disables HEVC/AV1 -- needs the freeworld
variant from RPM Fusion.)
- De-NVIDIA-fy the user-facing encoder log/context strings ("open NVENC" ->
"open video encoder") now that VAAPI is a first-class backend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two bodies of work in one commit (the rename moved files the fixes also touched).
Naming/structure cleanup (pre-launch):
- Host modules m3.rs->punktfunk1.rs, m0.rs->spike.rs; CLI m3-host->punktfunk1-host,
m0->spike; bare `punktfunk-host` now prints help. Types M3Options/M3Source->
Punktfunk1Options/Punktfunk1Source.
- Clients consolidated out of crates/ into clients/: punktfunk-client-rs->
clients/probe (crate punktfunk-probe), client-linux->clients/linux,
client-windows->clients/windows, punktfunk-android->clients/android/native
(crate punktfunk-client-android; kept [lib] name=punktfunk_android so the JNI
contract is unchanged). crates/ now holds only core + host.
- Milestone codes M0-M4 purged from code/CLI/CLAUDE.md/README/docs/docs-site,
kept only in docs/implementation-plan.md. docs/m2-plan.md->
docs/gamestream-host-plan.md. CI/gradle/flatpak paths updated.
Client loss-recovery (video froze and never recovered after a brief drop):
- Export punktfunk_connection_frames_dropped through the C ABI (the core already
tracked it for the client keyframe-recovery loop; it was never reachable from
the ABI clients). Regenerated punktfunk_core.h.
- Apple (StreamPump + Stage2Pipeline) and Android (decode.rs) now poll
frames_dropped and request a keyframe when it climbs -- the same loss-driven
recovery Linux/Windows already had. Under infinite GOP the decoder silently
conceals reference-missing frames, so the decode-error trigger rarely fires.
Apple rumble robustness (worked then went spotty -- DualSense + Xbox):
- Add CHHapticEngine stopped/reset handlers (rebuild on app background / audio
interruption / server reset) and drop the permanent `broken` latch on a
transient drive failure; latch only when the controller truly has no haptics.
- Surface swallowed SDL set_rumble errors on Linux/Windows + diagnostic logging.
Verified: cargo build/clippy/fmt --workspace, C-ABI harness, header drift.
Not runnable on this box (verify in CI): Gitea workflows, gradle/Android,
flatpak, Swift/decky.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the host docs match the real distribution path and the actual CLI. Reviewed by a
multi-agent pass (6 editors against one verified fact sheet + an accuracy reviewer); its
findings (a wrong client-Recommends claim, a native-concurrency overstatement) folded in.
- Install front door: new README "Install (host)" method-picker + docs-site/install.md
(+ nav), routing each distro to its package registry; source build demoted to a fallback.
- Registry-first install: ubuntu-gnome/ubuntu-kde now lead with the apt registry (not a
cargo build); bazzite leads with the Gitea RPM registry (was COPR/source). Source builds
moved to an appendix.
- CLI accuracy: serve --native arms pairing from the web console (NOT --allow-pairing, which
with --require-pairing/--max-concurrent is m3-host-only); --open disables mandatory pairing.
host-cli/configuration/pairing/quickstart/troubleshooting corrected; mgmt API documented as
always HTTPS+token. Native host serves one session at a time (extras queue) — not multi.
- Firewall: real ports documented (native UDP 9777 + the ephemeral data port caveat +
GameStream ports) for Debian + Arch (ufw + nftables), not just Bazzite.
- Sync/accuracy: punktfunk-client (GTK4) presented as a shipping client (not "roadmap"),
punktfunk-client-rs as the headless tool; host Recommends punktfunk-web only (not the
client); COPR chroots f43/44; bootc header says Gitea registry not COPR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- spec: narrow ExclusiveArch to x86_64 — no aarch64 build is produced/published (NVENC is
desktop-NVIDIA), so claiming aarch64 advertised an arch we never ship.
- build-deb.sh: ship punktfunk-kde-session.service (ExecStart repointed to the packaged
run-headless-kde.sh) + host.env.kde, matching the RPM/Arch — the deb README's "mirrors the
Fedora RPM" claim now holds.
- audit.yml: weekly + Cargo.lock-change `cargo audit` over the network-facing crypto dep tree
(RustSec advisories); ignore unfixables via .cargo/audit.toml.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The stale code a default install/upgrade got was a TAG LEAK: deb.yml/rpm.yml shared
`tags: ['v*']` with the Apple-client release.yml, so the v0.1.0/v0.1.1 tags cut to ship
the macOS app ALSO published host packages versioned 0.1.1 — which outranks every rolling
0.0.1~ciN / 0.0.1-0.ciN build in both registries (dpkg/rpm version compares confirm), so
`apt install`/`rpm-ostree install` silently fetched ~99-commits-stale code while the READMEs
claimed auto-tracking. Two fixes:
- Decouple host publishing from Apple `v*` tags: deb.yml/rpm.yml now trigger on `host-v*`
only, so a client tag can never poison the host channel again.
- Bump the rolling base 0.0.1 -> 0.2.0 (deb `0.2.0~ciN`, rpm `0.2.0-0.ciN`): sits ABOVE the
stray 0.1.1 yet BELOW a future 0.2.0 tag, and still climbs monotonically by run number — so
`apt upgrade`/`rpm-ostree upgrade` genuinely move forward. Spec default + build scripts +
PKGBUILD pkgver bumped to match.
Build provenance (so a stale/shadowed host is detectable): build.rs stamps PUNKTFUNK_BUILD_VERSION
(set by CI = the full package version, e.g. 0.2.0~ci120.g802e98d; falls back to the crate version
for a plain `cargo build`) into the binary via rustc-env. Surfaced in `punktfunk-host --version`,
the startup log, and the mgmt /health + /host `version` field (was a hardcoded CARGO_PKG_VERSION).
Deliberately env-driven, not git-derived — the RPM builds from a git-archive tarball with no .git.
Version computed BEFORE the build in deb.yml; the spec %build exports it from %{version}-%{release}
(and gains --locked for reproducibility parity with the .deb path). Validated: plain build reports
0.0.1, env-stamped build reports 0.2.0~ci999.gdeadbee.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every user needs the console for pairing, so ship it via apt, auto-wired to the
host — no manual bun/env setup. New punktfunk-web .deb (Architecture: all,
Depends: nodejs >= 20 — runs the node-server build under apt-native node, no
bundled bun):
- packaging/debian/build-web-deb.sh: stages web/.output (server + public) + a
/usr/bin/punktfunk-web-server wrapper (node) + the systemd --user units + the
web.env template + docs. Refuses a bun bundle (Bun.serve) as a wrong-preset guard.
- scripts/punktfunk-web.service: --user unit on :3000, EnvironmentFile sources the
host's ~/.config/punktfunk/mgmt-token (the shared bearer) + the generated
web-password; sets PUNKTFUNK_MGMT_URL=https://127.0.0.1:47990 +
NODE_TLS_REJECT_UNAUTHORIZED=0 (loopback self-signed cert). Restart=on-failure
rides out the host-writes-token-first ordering.
- scripts/punktfunk-web-init.service + web-init.sh: --user one-shot that generates
the login password (a .deb postinst runs as root → wrong $HOME) and surfaces it
to the journal.
- build-deb.sh: punktfunk-host now Recommends punktfunk-web (apt pulls it by
default; headless boxes opt out with --no-install-recommends).
- deb.yml: build the web console + smoke-boot it under node (gate the .deb on a
real /login 200) + build-web-deb.sh; the publish loop globs it automatically.
- web/{.env.example,web.env.example}: document the auto-wiring vs a manual deploy.
End state: `apt install punktfunk-host` pulls punktfunk-web; enable both --user
services; the console logs in (password from the journal) and proxies the host's
HTTPS mgmt API with the shared token — zero hand-edited env. Local .deb build +
node smoke-boot verified.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The client asks the kernel for a 32 MB SO_RCVBUF, but the kernel silently clamps
it to net.core.rmem_max — whose default is far too small. A too-small recv buffer
is the dominant client-side wall above ~1 Gbps. Measured live (Fedora host -> two
clients, real 2.5G LAN, GSO off): a client capped at 4 MB rmem_max dropped 31.6%
of a 2 Gbps stream at the receiver, while a 32 MB client delivered the same
2 Gbps at 0.0% loss. The host already shipped this tuning; the client packages
didn't (the RPM's %post even referenced the host-only file), so a client-only
install streamed lossy at high bitrate.
Add scripts/99-punktfunk-client-net.conf (rmem/wmem = 32 MB, distinct filename so
host+client can coexist) and ship+apply it from both the .deb (build-client-deb.sh)
and the RPM client subpackage (install, %files client, %post client).
For reference the full ladder (punktfunk speed-test): 0% loss to 1.5 Gbps on a
4 MB client; 31.6% at 2 Gbps on 4 MB vs 0% at 2 Gbps on 32 MB. iperf3 put the raw
link at ~2.35 Gbps TCP / ~2.4 Gbps UDP, so the stack now tracks the wire given a
big enough recv buffer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audio: a headless host has no speakers, and on a LAN with AirPlay devices PipeWire picks a random
HomePod as default — so desktop audio (which the host captures from the default sink's monitor)
went to a HomePod over AirPlay instead of to the client, and there was no "Punktfunk" output to
select. Ship a `punktfunk-sink.conf` (a `support.null-audio-sink` adapter — NOT the non-existent
module-null-sink, which makes pipewire refuse to start) with high priority.session so it's the
default; run-headless-kde.sh installs it and restarts pipewire once on first install. The host then
captures its monitor and streams it. (Disable AirPlay sinks out of band: `dnf remove
pipewire-config-raop`.)
Input: the host's libei portal D-Bus connection goes stale when the compositor session restarts the
portal under it, and the in-process reopen loop can't recover it (EIS setup keeps timing out) — only
a full restart does. Add PartOf=punktfunk-kde-session.service so the host restarts with the session.
Both verified live on the Fedora 44 KDE box.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On a headless KDE appliance, libei input injection silently failed: the EIS socket comes from the
xdg RemoteDesktop portal, which never came up, and even up it would pop an unanswerable "Allow
remote control?" dialog. Three fixes in run-headless-kde.sh, all idempotent + safe on the dev box:
- Reach graphical-session.target: xdg-desktop-portal is ordered behind it and its start job fails
without it, but a headless linger session never gets there and Fedora's target has
RefuseManualStart=yes — drop that in once, then start the target.
- Start the portal with `start` (the old `try-restart` is a no-op when inactive — the first-boot
case), so it actually comes up.
- Pre-seed the RemoteDesktop grant: vendor the `kde-authorized` permission-store GVariant DB and
copy it to ~/.local/share/flatpak/db/ (never clobbering an existing one), so the portal grants
RemoteDesktop without a dialog. Shipped by the RPM + .deb.
Diagnosed + fixed live on the Fedora 44 KDE box: libei devices RESUME and emit (MouseMove/keys).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
scripts/punktfunk-host.service is dev-oriented — its ExecStart references the
source tree (%h/punktfunk/target/release/punktfunk-host). When the deb/rpm ship
it to /usr/lib/systemd/user, a fresh install with no hand-rolled unit would try
to run a binary that isn't there. Rewrite the ExecStart to the installed
/usr/bin/punktfunk-host during packaging (sed in build-deb.sh + the spec); the
source unit stays as-is for from-source dev. Hosts with a custom ~/.config unit
(which shadows the packaged one) are unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hook the Linux client into the existing packaging CI:
- deb.yml builds both binaries and publishes punktfunk-host AND
punktfunk-client to the Gitea apt registry; new
packaging/debian/build-client-deb.sh mirrors the host script
(shlibdeps auto-Depends — GTK4/libadwaita/SDL3/FFmpeg/PipeWire
sonames; no NVIDIA filter, the client links no CUDA). Built and
inspected locally on Ubuntu 26.04.
- punktfunk.spec gains a "client" subpackage (binary + desktop entry +
udev rule); rpm.yml's publish loop picks it up unchanged.
- New shared assets: packaging/linux/io.unom.Punktfunk.desktop and
scripts/70-punktfunk-client.rules — DualSense hidraw uaccess (USB +
Bluetooth, steam-devices style) so SDL's HIDAPI driver gets
touchpad/motion/lightbar/triggers instead of degrading to evdev.
- Builder images learn the client link deps (rust-ci already had
them; fedora-rpm adds gtk4/libadwaita/SDL3-devel) with idempotent
install steps in deb.yml/rpm.yml since jobs run against the
previous push's image.
Workspace check CI (build/clippy/test) already covers the crate since
a601022.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The host requests a 32 MB SO_SNDBUF, but the kernel clamps it to net.core.wmem_max
(~416 KB on a stock box) — so high-bitrate frames overflow the socket buffer and
the host drops a large fraction of packets on send (measured 28.5% loss / 54k
dropped at 1 Gbps to a clean LAN client on a fresh Bazzite box). scripts/99-punktfunk-net.conf
fixes it (32 MB caps) but the packages never installed it. Ship it to
/usr/lib/sysctl.d/ (auto-applied at boot by systemd-sysctl) and apply it in the
deb/rpm postinst. This is the dominant cause of the sub-Gbps ceiling on an
untuned host.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires up the half-built Debian packaging: build-deb.sh existed but nothing
invoked or published it. Adds a `deb` workflow that builds the release host in
the Ubuntu 26.04 rust-ci image, packages it (dpkg-shlibdeps-resolved Depends,
NVIDIA driver filtered out), and uploads to Gitea's public Debian registry on
every main push (rolling 0.0.1~ciN.<sha>) and v* tag (clean X.Y.Z). Ubuntu hosts
then track it with `apt update && apt upgrade`.
Also: box-setup docs (packaging/debian/README.md), a pointer from the packaging
README, ignore dist/, and drop backticks from the package Description (the
unquoted control heredoc ran them as a command substitution).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Everything the macOS app does that stage 1 lacked, before any new
feature work (user directive):
- Input capture is now a deliberate, reversible STATE (Moonlight-
style): engaged on stream start and click-into-video (the engaging
click is suppressed), released by Ctrl+Alt+Shift+Q (toggles) or
focus loss; held keys/buttons are flushed host-side on release;
cursor hiding + shortcut inhibition follow the state; HUD hint when
released. Per-session window handlers disconnect with the page.
- Gamepads: app-lifetime SDL service (GamepadManager parity) — pad
list + "Forwarded controller" pin in Settings (auto = most recent),
"Automatic" pad TYPE resolves from the physical pad at connect;
DualSense touchpad contacts + ~250 Hz motion samples on the 0xCC
plane (Swift GamepadWire scale constants); feedback grows adaptive-
trigger replay and player LEDs via raw DS5 effects packets (the
wire's 11-byte blocks drop into SDL_SendGamepadEffect verbatim);
held pad state zeroed on pad switch/detach. sdl3 "hidapi" feature.
- Microphone uplink: PipeWire capture -> Opus 20 ms -> 0xCB datagrams
(validated live: host received 711 mic packets), Settings toggle.
- Speed test per saved host (Swift's "Test Network Speed…"): 2 s
probe burst, goodput/loss + recommended ~70 % bitrate, one-tap apply.
- Settings: host compositor preference (sent in the Hello), native-
display resolution/refresh resolved from the window's monitor at
connect (new default), bitrate ceiling to 3 Gbit/s.
- Hosts page: saved/trusted hosts section for direct pinned reconnect
(mDNS not required), rebuilt on every page return.
Deliberately not ported: audio device pickers (PipeWire routing owns
this on Linux), resize-to-request_mode (not wired in Swift either),
pointer-lock relative mouse (stage-2 presenter, needs raw Wayland).
DualSense fidelity needs a physical pad to live-verify.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>