Clearing a host's trust store meant clicking the row trash icon once per device and confirming each time — tedious with a handful of clients, and easy to leave half-done.
The Paired devices card header now carries an Unpair all action behind a single confirmation.
One per pairing plane, because the two planes own separate trust stores with separate persistence and separate revocation duties. Each empties its store in one persisted write — doing it as N deletes would rewrite (and atomically rename) the store once per client, and a failure partway would leave the operator with a half-emptied store and no way to tell which half.
They are collection deletes, so they carry the single delete's revocation guarantees across the whole set: a live session owned by any removed certificate is ended, and on the Moonlight side the ENet control port closes because no pairing is left to hold it open.
200 with a count rather than the single delete's 204/404: "unpair everything" is idempotent, an already-empty store satisfies it, and the count still tells the operator whether that meant three devices or none.
Authorization
Both gates match on (method, path), so the roster's plugin-readable GET /clients does not carry over to emptying it. Both new routes are admin-token only, like every other pairing-administration route, with explicit rows in the route-classification table (which fails the build until a new route is classified).
Console
Only the planes that actually hold a row are called — the native endpoint answers 503 on a host built without that plane, which would otherwise report a failure for devices that were never there. The two calls go out together under Promise.allSettled, so one failing neither cancels the other nor throws past the handler.
Verification
Run on the Ubuntu box (punktfunk-host does not build on macOS):
cargo test -p punktfunk-host --bins — 492 passed, 0 failed
api/openapi.json regenerated with cargo run -p punktfunk-host -- openapi; the openapi_document_is_complete_and_checked_in test passes
New host tests: native_unpair_all_empties_the_trust_store, native_unpair_all_without_a_native_host_is_unavailable, plus a bulk section extending paired_clients_list_and_unpair that asserts the live session of the second client is torn down, the store is persisted empty, and a repeat call is a zero-count success.
Web: bun run build, bun run lint, bun run test (9 pass), biome check, i18n at 654 messages for en + de. The button was rendered through the real Pages/Pairing story in both dark and light themes.
⚠️ One consequence worth naming: the console now requires a host that has these endpoints. punktfunk-web and punktfunk-host ship as separate packages with no version handshake, so a canary box running console N against host N-1 will get a 404 here and show the failure toast.
Clearing a host's trust store meant clicking the row trash icon once per device and confirming each time — tedious with a handful of clients, and easy to leave half-done.
The **Paired devices** card header now carries an **Unpair all** action behind a single confirmation.
## Two endpoints, not a loop
```
DELETE /api/v1/clients -> {"unpaired": N}
DELETE /api/v1/native/clients -> {"unpaired": N}
```
One per pairing plane, because the two planes own separate trust stores with separate persistence and separate revocation duties. Each empties its store in **one** persisted write — doing it as N deletes would rewrite (and atomically rename) the store once per client, and a failure partway would leave the operator with a half-emptied store and no way to tell which half.
They are collection deletes, so they carry the single delete's revocation guarantees across the whole set: a live session owned by any removed certificate is ended, and on the Moonlight side the ENet control port closes because no pairing is left to hold it open.
`200` with a count rather than the single delete's `204`/`404`: "unpair everything" is idempotent, an already-empty store satisfies it, and the count still tells the operator whether that meant three devices or none.
## Authorization
Both gates match on `(method, path)`, so the roster's plugin-readable `GET /clients` does **not** carry over to emptying it. Both new routes are admin-token only, like every other pairing-administration route, with explicit rows in the route-classification table (which fails the build until a new route is classified).
## Console
Only the planes that actually hold a row are called — the native endpoint answers `503` on a host built without that plane, which would otherwise report a failure for devices that were never there. The two calls go out together under `Promise.allSettled`, so one failing neither cancels the other nor throws past the handler.
## Verification
Run on the Ubuntu box (`punktfunk-host` does not build on macOS):
- `cargo test -p punktfunk-host --bins` — **492 passed, 0 failed**
- `cargo clippy -p punktfunk-host --all-targets -- -D warnings` — clean (non-vacuous; the log shows `Compiling punktfunk-host`)
- `cargo fmt -p punktfunk-host --check` — clean
- `api/openapi.json` regenerated with `cargo run -p punktfunk-host -- openapi`; the `openapi_document_is_complete_and_checked_in` test passes
New host tests: `native_unpair_all_empties_the_trust_store`, `native_unpair_all_without_a_native_host_is_unavailable`, plus a bulk section extending `paired_clients_list_and_unpair` that asserts the live session of the *second* client is torn down, the store is persisted empty, and a repeat call is a zero-count success.
Web: `bun run build`, `bun run lint`, `bun run test` (9 pass), `biome check`, i18n at 654 messages for **en + de**. The button was rendered through the real `Pages/Pairing` story in both dark and light themes.
> ⚠️ One consequence worth naming: the console now requires a host that has these endpoints. `punktfunk-web` and `punktfunk-host` ship as separate packages with no version handshake, so a canary box running console `N` against host `N-1` will get a `404` here and show the failure toast.
Clearing a host's trust store meant clicking the row trash icon once per
device and confirming each time — tedious with a handful of clients, and
easy to leave half-done.
The "Paired devices" card header now carries an "Unpair all" action behind
a single confirmation. It is backed by two new endpoints rather than a loop
over the per-fingerprint deletes:
DELETE /api/v1/clients -> {"unpaired": N}
DELETE /api/v1/native/clients -> {"unpaired": N}
one per pairing plane, because the two planes own separate trust stores
with separate persistence and separate revocation duties. Each empties its
store in ONE persisted write. Doing it as N deletes would rewrite (and
atomically rename) the store once per client, and a failure partway would
leave the operator with a half-emptied store and no way to tell which half.
They are collection deletes, so they carry the single delete's revocation
guarantees across the whole set: a live session owned by any removed
certificate is ended, and on the Moonlight side the ENet control port
closes, because no pairing is left to hold it open.
200 with a count rather than the single delete's 204/404: "unpair
everything" is idempotent, an already-empty store satisfies it, and the
count still tells the operator whether that meant three devices or none.
Both gates match on (method, path), so the roster's plugin-readable GET
does not carry over to emptying it — both new routes are admin-token only,
like every other pairing-administration route, with explicit rows in the
route-classification table.
The console calls only the planes that actually have a row: the native
endpoint answers 503 on a host built without that plane, which would
otherwise report a failure for devices that were never there.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Clearing a host's trust store meant clicking the row trash icon once per device and confirming each time — tedious with a handful of clients, and easy to leave half-done.
The Paired devices card header now carries an Unpair all action behind a single confirmation.
Two endpoints, not a loop
One per pairing plane, because the two planes own separate trust stores with separate persistence and separate revocation duties. Each empties its store in one persisted write — doing it as N deletes would rewrite (and atomically rename) the store once per client, and a failure partway would leave the operator with a half-emptied store and no way to tell which half.
They are collection deletes, so they carry the single delete's revocation guarantees across the whole set: a live session owned by any removed certificate is ended, and on the Moonlight side the ENet control port closes because no pairing is left to hold it open.
200with a count rather than the single delete's204/404: "unpair everything" is idempotent, an already-empty store satisfies it, and the count still tells the operator whether that meant three devices or none.Authorization
Both gates match on
(method, path), so the roster's plugin-readableGET /clientsdoes not carry over to emptying it. Both new routes are admin-token only, like every other pairing-administration route, with explicit rows in the route-classification table (which fails the build until a new route is classified).Console
Only the planes that actually hold a row are called — the native endpoint answers
503on a host built without that plane, which would otherwise report a failure for devices that were never there. The two calls go out together underPromise.allSettled, so one failing neither cancels the other nor throws past the handler.Verification
Run on the Ubuntu box (
punktfunk-hostdoes not build on macOS):cargo test -p punktfunk-host --bins— 492 passed, 0 failedcargo clippy -p punktfunk-host --all-targets -- -D warnings— clean (non-vacuous; the log showsCompiling punktfunk-host)cargo fmt -p punktfunk-host --check— cleanapi/openapi.jsonregenerated withcargo run -p punktfunk-host -- openapi; theopenapi_document_is_complete_and_checked_intest passesNew host tests:
native_unpair_all_empties_the_trust_store,native_unpair_all_without_a_native_host_is_unavailable, plus a bulk section extendingpaired_clients_list_and_unpairthat asserts the live session of the second client is torn down, the store is persisted empty, and a repeat call is a zero-count success.Web:
bun run build,bun run lint,bun run test(9 pass),biome check, i18n at 654 messages for en + de. The button was rendered through the realPages/Pairingstory in both dark and light themes.Clearing a host's trust store meant clicking the row trash icon once per device and confirming each time — tedious with a handful of clients, and easy to leave half-done. The "Paired devices" card header now carries an "Unpair all" action behind a single confirmation. It is backed by two new endpoints rather than a loop over the per-fingerprint deletes: DELETE /api/v1/clients -> {"unpaired": N} DELETE /api/v1/native/clients -> {"unpaired": N} one per pairing plane, because the two planes own separate trust stores with separate persistence and separate revocation duties. Each empties its store in ONE persisted write. Doing it as N deletes would rewrite (and atomically rename) the store once per client, and a failure partway would leave the operator with a half-emptied store and no way to tell which half. They are collection deletes, so they carry the single delete's revocation guarantees across the whole set: a live session owned by any removed certificate is ended, and on the Moonlight side the ENet control port closes, because no pairing is left to hold it open. 200 with a count rather than the single delete's 204/404: "unpair everything" is idempotent, an already-empty store satisfies it, and the count still tells the operator whether that meant three devices or none. Both gates match on (method, path), so the roster's plugin-readable GET does not carry over to emptying it — both new routes are admin-token only, like every other pairing-administration route, with explicit rows in the route-classification table. The console calls only the planes that actually have a row: the native endpoint answers 503 on a host built without that plane, which would otherwise report a failure for devices that were never there.