Wire-compatible with 0.21.x and 0.22.x — nothing about streaming changed, and everything already paired keeps working. This release repairs the Windows installer and gives every host something new: the web console now tells you when a newer Punktfunk is out. The apps on your phone, tablet, Mac and TV are unchanged.

**If you stream to a Windows PC, update that PC.** The 0.22.1 and 0.22.2 installers were built without the web console in them at all — not broken, absent. If the tray menu on your PC says "Open web console (not responding)" and the page never loads no matter what you try, that is this, and reinstalling those versions could never have fixed it.

## New

- **The console tells you when a newer host is out.** The Host page has an **Updates** card: the version you're running, the channel you follow (stable or canary), and — once something newer exists — release notes and the exact way to update *your* kind of install. The check is a small signed file the host verifies against keys built into it, so nothing between you and the project can feed it a fake update, and a feed that ever tried to roll you backwards is refused outright. It contacts only the Punktfunk forge, and `PUNKTFUNK_UPDATE_CHECK=0` turns it off entirely. Scripts can react too: the host emits `update.available` on its event stream. Actually *applying* an update from the card arrives in the next release.

## Fixed

- **The web console is in the installer again.** On 0.22.1 and 0.22.2 the Windows installer shipped with the console missing entirely: the files it runs from were never included, so the PC had nothing to start and nothing to open. The host itself was fine the whole time — streaming, pairing and controllers all worked — but the settings page you reach in a browser simply was not there. The tray reported it as "not responding", which was true and also the only clue you got, and because every reinstall produced the same installer, uninstalling and reinstalling made no difference. Updating to 0.22.3 restores it; you don't need to touch your settings, your password, or your paired devices.

- **Updating no longer stops on a "DeleteFile failed; code 5" error.** Partway through an update, the installer could stop on a dialog complaining that it could not replace `bun\bun.exe`, offering only Try again, Skip, or Cancel — and Try again usually failed the same way. Windows refuses to replace a program while it is running, and the pieces of Punktfunk that were still running weren't the ones the installer knew how to stop. It now shuts all of them down, waits for them to actually exit rather than assuming, and if something still holds the file it finishes the install and puts the new copy in place on your next restart instead of leaving you stuck at a dialog.

- **Updating no longer switches your plugins off.** If you had enabled the script and plugin runner, every update quietly disabled it again, so plugins stopped running until you noticed and re-enabled them by hand. Updates now leave it exactly as you had it — on if it was on, and still off by default for everyone who has never turned it on.

## Improved

- **You can save or share the host's log from the console.** The Logs page has two new buttons: one downloads what you're looking at as a timestamped `.log` file, the other hands it to your phone or tablet's share sheet, or copies it to the clipboard on a desktop. Handy when someone asks you for a log — the file carries full dates and times, so it still makes sense once it leaves your browser. Whatever level filter and search you have applied is what you get, and it saves everything that matches rather than just the part on screen.

- **Televisions that struggle with the startup speed test can be told to ease off.** Two seconds into a session Punktfunk briefly bursts traffic to measure how much room your network really has. On some TVs that burst is enough to disturb the very link it is measuring — one LG set showed no video for fourteen seconds when it went badly. The burst's size can now be capped, so a device that already limits its own speed test can keep ours in line with it. Nothing changes unless a device asks for it.

## Under the hood (for developers)

- The console's absence was one CI variable in the wrong place. `windows-host.yml` exported `WEB_OUTPUT_DIR` on the last line of "Build + smoke-boot web console (bun)", and `be2fabcf` had just put that step behind `if: steps.webconsole.outputs.cache-hit != 'true'`. So the first build after the cache was populated — and every build after it with `web/**` and `sdk/**` unchanged — skipped the step, left the variable unset, and `pack-host-installer.ps1` reads an unset `WEB_OUTPUT_DIR` as "omit the console", announcing it in a single `Write-Host` before packing happily. Confirmed from the job logs rather than inferred: on both the v0.22.1 (run 14235) and v0.22.2 (run 14272) tag builds, step 12 is `skipped` and the job is green. `BUN_EXE` and `SCRIPTING_BUNDLE` are exported from unconditional steps, which is why those installers still carried bun and the plugin runner but no `{app}\web`.
- Downstream, `web setup` bailed at `web launcher missing: {app}\web\web-run.cmd` before registering anything, so there was no `PunktfunkWeb` task, no listener on 47992, and no firewall rule — and Inno ignores `[Run]` exit codes, so the failure left no trace in the install at all. `WEB_OUTPUT_DIR` now comes from its own unconditional step that throws when `web\.output\server\index.mjs` is absent, and a new pre-pack step asserts all five payloads (console, bun, plugin runner, FFmpeg DLLs, VB-CABLE) so a missing input fails the build rather than silently redefining the installer. The shape is borrowed from the packer's existing VB-CABLE check. FFmpeg was the most dangerous of the silent ones: an `amf-qsv` host link-imports avcodec, so an installer without those DLLs ships a host that cannot start at all.
- The same cache-hit build is why `bun.exe` locked. It ships under `WithWeb` **or** `WithScripting`, but the installer's pre-copy stop was `#ifdef WithWeb` — so a console-less installer shipped bun while the only code that stopped bun was compiled out. `StopBunRuntimes` replaces `StopWebConsole` behind the same `WithWeb || WithScripting` gate the payload uses, and covers what the old routine structurally could not: neither bun runs under the host service (both are Task Scheduler tasks — `PunktfunkWeb` as SYSTEM, `PunktfunkScripting` as LocalService), and the runner listens on no port, so a task-name-plus-port sweep could never see it. It now disables both tasks before stopping them — they carry restart-on-failure at 10× and 999× a minute, and the web task also has a logon trigger, so a force-kill alone invited a respawn into the middle of a copy that takes over a minute at `lzma2/max` — kills any bun whose image lives under the install dir (by path, so an unrelated bun survives), and polls until they are gone, since `Stop-ScheduledTask` returns on request and `Stop-Process` is `TerminateProcess`. `bun.exe` also gained `restartreplace`, so a survivor defers to reboot instead of dead-ending the install.
- Disabling a task is not free: unlike a stopped one it does not return at the next boot, so an aborted install would have taken the console down permanently. Two restores cover it — a last-in-`[Run]` entry for the normal flow and `DeinitializeSetup`, which Inno calls even on user cancel — both re-enabling only what was enabled before the copy. That is also the plugin-runner fix: the scripting `[Run]` entry re-registers its task and then unconditionally disables it, correct on a first install and wrong on every upgrade since.
- `PUNKTFUNK_ABR_PROBE_KBPS` sets the startup capacity probe's burst target for embedders. Unset, zero or unparseable keeps the 2 Gbps default, so existing sessions are unchanged; `PUNKTFUNK_ABR_PROBE=0` remains a poor substitute because it pins the climb ceiling at the negotiated ~20 Mbps.
- The console's log export serializes the filters' full result rather than the rendered tail — the 1000-row cap is a DOM budget and has no business truncating a file destined for a bug report — and stamps each line with a local ISO 8601 timestamp plus numeric offset. Web Share level 2 is probed at runtime after mount (SSR has no `navigator`, and guessing there would mismatch on hydration), falling back to the clipboard, and the button is omitted only where neither exists.
- The update check's truth is a per-channel Ed25519-signed manifest (`…/generic/punktfunk-update/{stable,canary}/manifest.json` + `.sig`), verified against keys pinned in the host binary via the plugin-index machinery, with a persisted monotonic serial as the anti-rollback floor and the channel name bound into the signed document. Stable manifests publish from `announce.yml` — the existing manual fleet-green gate doubles as the gate for the fleet learning about a release; canary rides `windows-host.yml`. TLS and the registry are transport, never trust.
- `/api/v1/update/*` is admin-lane only: whole-prefix denied to the plugin token and absent from the paired-cert allowlist. This release exposes the check surface only — there is no apply route in it.
- No wire, ABI or driver-protocol changes: wire protocol 2, C ABI 13, Windows virtual-gamepad channel 3, virtual-display driver protocol 6 — identical to 0.22.0, 0.22.1 and 0.22.2.
