One item from #294 — luxus's suggestion on the issue:
maybe a simple toast on incoming pairing requests?
The problem
A device asking to pair appeared only on the Pairing page. Anywhere else in the console, nothing said it had happened, so a request sat until someone thought to go and look. Worst on a phone, which is what #294 is about: that page is several taps away.
Why it costs almost nothing
Both halves already existed and were not joined up:
The host publishes the knock as a pairing.pending SSE frame (events.rs, EventKind::PairingPending { device: DeviceRef }).
The console already subscribes to that exact kind in api/events.ts and already uses toast from @unom/ui/toast across eight sections.
So this hangs a toast off a frame that was arriving anyway. It names the device from the event's own payload and offers one button through to Pairing. recordActivity now returns what it parsed instead of discarding it, so the payload is read without parsing the same frame twice.
A related bug fixed in passing
keysFor("pairing.pending") invalidated the status card and the pairing status — not the pending list itself. The list an operator sits and waits on refreshed only on its own 10 s timer, despite an event saying it had just changed. Both pairing.pending and pairing.denied now invalidate getListPendingDevicesQueryKey().
The one compromise
The toast's action does window.location.assign("/pairing") rather than a router push. api/events.ts is a module-level singleton, not a component, and router.tsx keeps liveRouter private and typed as { latestLocation: { href: string } } — no navigate to reach. It costs a page load, on a button someone pressed on purpose. Commented as such.
Strings
Three keys added to both catalogues (en, de), inserted in place beside the existing pairing block rather than re-sorted, so the diff is 3 lines per file:
Key
en
de
pairing_knock_title
A device wants to pair
Ein Gerät möchte sich koppeln
pairing_knock_desc
{name} is waiting for approval.
{name} wartet auf Freigabe.
pairing_knock_action
Review
Ansehen
bun run codegen reports 759 compiled messages for en and de.
Verification
Check
Result
bunx biome check on the changed files
clean
bun test server/
28 pass, 0 fail
tsc --noEmit
1 error, not from this branch — see below
The single tsc error is src/router.tsx(72,29): TS2554: Expected 0-1 arguments, but got 2, in a file this branch does not touch. Cause: the linked node_modules carries @unom/ui0.8.16 where package.json requires ^0.10.0, and 0.8.16's reloadOnStaleChunk takes one argument. Zero errors in the changed files.
⚠ bun run build was not run locally — it is the first step of CI's web job, and it fails for the same stale-dependency reason. This PR's CI run is the first real pass.
Not in scope
The rest of #294 — navigation, margins, section reorganisation. #294 stays open.
One item from #294 — luxus's suggestion on the issue:
> maybe a simple toast on incoming pairing requests?
## The problem
A device asking to pair appeared only on the Pairing page. Anywhere else in the console, nothing said it had happened, so a request sat until someone thought to go and look. Worst on a phone, which is what #294 is about: that page is several taps away.
## Why it costs almost nothing
Both halves already existed and were not joined up:
- The host publishes the knock as a `pairing.pending` SSE frame (`events.rs`, `EventKind::PairingPending { device: DeviceRef }`).
- The console already subscribes to that exact kind in `api/events.ts` and already uses `toast` from `@unom/ui/toast` across eight sections.
So this hangs a toast off a frame that was arriving anyway. It names the device from the event's own payload and offers one button through to Pairing. `recordActivity` now returns what it parsed instead of discarding it, so the payload is read without parsing the same frame twice.
## A related bug fixed in passing
`keysFor("pairing.pending")` invalidated the status card and the pairing status — **not the pending list itself**. The list an operator sits and waits on refreshed only on its own 10 s timer, despite an event saying it had just changed. Both `pairing.pending` and `pairing.denied` now invalidate `getListPendingDevicesQueryKey()`.
## The one compromise
The toast's action does `window.location.assign("/pairing")` rather than a router push. `api/events.ts` is a module-level singleton, not a component, and `router.tsx` keeps `liveRouter` private and typed as `{ latestLocation: { href: string } }` — no `navigate` to reach. It costs a page load, on a button someone pressed on purpose. Commented as such.
## Strings
Three keys added to **both** catalogues (`en`, `de`), inserted in place beside the existing pairing block rather than re-sorted, so the diff is 3 lines per file:
| Key | en | de |
| --- | --- | --- |
| `pairing_knock_title` | A device wants to pair | Ein Gerät möchte sich koppeln |
| `pairing_knock_desc` | {name} is waiting for approval. | {name} wartet auf Freigabe. |
| `pairing_knock_action` | Review | Ansehen |
`bun run codegen` reports 759 compiled messages for en and de.
## Verification
| Check | Result |
| --- | --- |
| `bunx biome check` on the changed files | clean |
| `bun test server/` | 28 pass, 0 fail |
| `tsc --noEmit` | 1 error, **not from this branch** — see below |
The single `tsc` error is `src/router.tsx(72,29): TS2554: Expected 0-1 arguments, but got 2`, in a file this branch does not touch. Cause: the linked `node_modules` carries `@unom/ui` **0.8.16** where `package.json` requires **^0.10.0**, and 0.8.16's `reloadOnStaleChunk` takes one argument. Zero errors in the changed files.
⚠ **`bun run build` was not run locally** — it is the first step of CI's `web` job, and it fails for the same stale-dependency reason. This PR's CI run is the first real pass.
## Not in scope
The rest of #294 — navigation, margins, section reorganisation. #294 stays open.
Refs #294
A device asking to pair showed up only on the Pairing page. Anywhere
else in the console nothing said it had happened, so a request waited
until someone thought to go and look — worst on a phone, where that page
is several taps away.
The host already publishes the knock as a `pairing.pending` event and
the console already subscribes, so this hangs a toast off the frame
that was arriving anyway. It names the device from the event's own
payload and offers one button through to the Pairing page.
The same event now also invalidates the pending list itself, which it
did not: it refreshed the status card and the pairing status, and left
the list an operator actually sits and waits on to its own 10 s timer.
The action navigates rather than routing: this module is not a
component and has no router to reach. It costs a page load, on a button
someone pressed on purpose.
Refs #294
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.
One item from #294 — luxus's suggestion on the issue:
The problem
A device asking to pair appeared only on the Pairing page. Anywhere else in the console, nothing said it had happened, so a request sat until someone thought to go and look. Worst on a phone, which is what #294 is about: that page is several taps away.
Why it costs almost nothing
Both halves already existed and were not joined up:
pairing.pendingSSE frame (events.rs,EventKind::PairingPending { device: DeviceRef }).api/events.tsand already usestoastfrom@unom/ui/toastacross eight sections.So this hangs a toast off a frame that was arriving anyway. It names the device from the event's own payload and offers one button through to Pairing.
recordActivitynow returns what it parsed instead of discarding it, so the payload is read without parsing the same frame twice.A related bug fixed in passing
keysFor("pairing.pending")invalidated the status card and the pairing status — not the pending list itself. The list an operator sits and waits on refreshed only on its own 10 s timer, despite an event saying it had just changed. Bothpairing.pendingandpairing.deniednow invalidategetListPendingDevicesQueryKey().The one compromise
The toast's action does
window.location.assign("/pairing")rather than a router push.api/events.tsis a module-level singleton, not a component, androuter.tsxkeepsliveRouterprivate and typed as{ latestLocation: { href: string } }— nonavigateto reach. It costs a page load, on a button someone pressed on purpose. Commented as such.Strings
Three keys added to both catalogues (
en,de), inserted in place beside the existing pairing block rather than re-sorted, so the diff is 3 lines per file:pairing_knock_titlepairing_knock_descpairing_knock_actionbun run codegenreports 759 compiled messages for en and de.Verification
bunx biome checkon the changed filesbun test server/tsc --noEmitThe single
tscerror issrc/router.tsx(72,29): TS2554: Expected 0-1 arguments, but got 2, in a file this branch does not touch. Cause: the linkednode_modulescarries@unom/ui0.8.16 wherepackage.jsonrequires ^0.10.0, and 0.8.16'sreloadOnStaleChunktakes one argument. Zero errors in the changed files.⚠
bun run buildwas not run locally — it is the first step of CI'swebjob, and it fails for the same stale-dependency reason. This PR's CI run is the first real pass.Not in scope
The rest of #294 — navigation, margins, section reorganisation. #294 stays open.
Refs #294