DUNGEONS-34: implement Webka local streaming app

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
2026-09-06 16:23:09 +05:00
parent 5540b9a1f4
commit 7c737c1011
36 changed files with 21596 additions and 0 deletions

37
docs/API.md Normal file
View File

@@ -0,0 +1,37 @@
# API and transport
All API paths are same-origin. Mutations and WebSocket upgrades require an `Origin` matching `WEBKA_ORIGIN` exactly. JSON requests use `Content-Type: application/json`. Authentication is the signed `webka_session` cookie. Error responses are `{ "error": "translationKey" }`; framework validation errors are shown as a translated generic error by the frontend.
| Method | Path | Access | Result |
|---|---|---|---|
| GET | `/api/bootstrap` | Public | `{ needs_setup }` |
| POST | `/api/bootstrap` | Setup token, first run only | `{ username, password, setup_token }` → 201 |
| POST | `/api/login` | Public, throttled | `{ username, password }` → signed cookie |
| POST | `/api/logout` | Signed in | Deletes current session and closes its transport |
| GET | `/api/session` | Signed in | `{ id, username, role, session_id }`; refreshes activity |
| GET/POST | `/api/accounts` | Admin | List accounts / create viewer `{ username, password }` |
| DELETE | `/api/accounts/:id` | Admin | Delete viewer and cascade sessions; admin protected |
| POST | `/api/accounts/:id/password` | Admin | `{ password }`; revokes all target sessions |
| GET | `/api/sessions` | Admin | Active sessions and last activity |
| DELETE | `/api/sessions/:id` | Admin | Force revoke session |
| GET | `/api/status` | Signed in | Live state, uptime origin, count, share URL; identities visible only to admin |
| GET upgrade | `/api/signal?mode=host\|viewer` | Signed in; host mode admin only | WebSocket JSON signaling |
One active host and one transport per login session. Up to 32 sockets, 64 KiB per WebSocket message, 120 signaling messages per second per socket; API bodies capped at 8 KiB. Roles are always enforced server-side. Login/setup permit ten attempts per IP per minute. Argon2 work is limited to two concurrent blocking jobs.
Signaling is not a media relay. The browser uses `RTCPeerConnection({ iceServers: [] })`, video-only tracks, trickle ICE and peer-to-peer encrypted WebRTC. The server chooses the sender identity from the authenticated connection; it never accepts a claimed `peer` field from a client.
Server events:
- `welcome { peer, live }`
- `viewer-joined { peer }`, `viewer-left { peer }` (host only)
- `host-online`, `host-offline` (viewers)
- `error { error }` (capacity/session/host conflict)
Routed messages: `{ type, target, data }` become `{ type, peer, data }`.
- Host → its viewers: `offer` (SDP), `ice` (ICE candidate).
- Viewer → current host: `answer`, `ice`, `renegotiate`.
- Viewer-to-viewer and forged role messages are dropped.
The server revalidates the session before each inbound signal and every two seconds. Logout, account deletion, password reset and session revocation remove affected connections and notify the host/viewers to close media peers. The frontend clears peers when signaling disconnects, retries with 1–10 second exponential backoff, and rechecks authentication before reconnecting. There is no recording or server-side video storage.

41
docs/MANUAL_TEST_PLAN.md Normal file
View File

@@ -0,0 +1,41 @@
# Real-device acceptance test
These are required manual checks, not a claim that physical devices have been tested. Use a trusted HTTPS origin, two separate browser profiles/devices on one LAN, and the Rust service with a fresh database.
## Setup and access
1. Open `/login` in a clean profile. Confirm all labels render in English. Switch to Russian and Spanish, refresh, and confirm the choice persists and the login card fits.
2. Try a wrong setup token and a weak password. Confirm rejection. Create the owner using the local setup token and a 12+ character password containing letters and digits. Try setup again: it must be closed.
3. Add two viewer accounts. Try a duplicate username with different casing: rejected. Sign in as a viewer; direct navigation and direct API calls to account management must be rejected.
4. Try incorrect credentials 11 times within one minute from the same IP: the login endpoint returns 429. Wait a minute; valid login works.
## Capture and source switching
1. On `/host`, no camera should activate before user interaction. Enable preview; accept permission. A local video appears, but another viewer remains waiting until **Go live**.
2. Deny permission in another profile. Confirm an actionable translated error and no false Live status. Open a LAN HTTP URL: confirm the secure-context error rather than a broken capture attempt.
3. Grant camera access and inspect front/back controls on a dual-camera phone. Select back camera, then front. Confirm the correct lens, front preview mirroring only, and no duplicate media tracks. With one camera, unavailable back camera stays disabled.
4. Start broadcasting. Join in a separate viewer session via the QR code. Confirm moving video and the host's viewer count. Compare host and viewer clocks/frame motion for LAN latency.
5. While live, switch front → back → screen → front. Select a screen/window in the browser prompt. Confirm the existing viewer updates without signing in again. On platforms unable to open both lenses concurrently, note the error; stop preview and retry the other lens.
6. Cancel the screen picker: the previous source must continue. Click the browser’s **Stop sharing** control: hosting stops, camera indicator turns off, and viewer shows waiting.
7. Switch bitrate between low/adaptive/high. Inspect browser WebRTC stats: max bitrate changes (actual resolution depends on capture and congestion).
8. Stop hosting, navigate to settings, close the tab, and log out separately. Each must stop the host camera and close outgoing peer streams.
## Viewer lifecycle and session revocation
1. Join before the host is live; automatic playback should start once the host starts. Check fullscreen, language switching while fullscreen, quality/resolution, and disconnect.
2. Join from two viewers and revoke one session. Within two seconds its signaling closes; the host removes that peer and the revoked viewer returns to login. Other viewers continue normally.
3. Reset a viewer password; all its sessions stop and old credentials fail. Delete that viewer account and confirm subsequent login is denied.
4. Reset the administrator password while streaming. All its sessions are logged out and viewers stop receiving media.
5. Interrupt Wi-Fi briefly. Confirm reconnect state and bounded exponential retry. Restore connectivity; playback should recover. Reload/stop/start the host and confirm waiting viewers recover.
6. Attempt a second host tab with the same or another administrator session: it must show a conflict, not replace the active broadcast.
7. Leave a session hidden/inactive for 30 minutes and reopen it; it must return to login. Absolute expiry after 12 hours applies even with heartbeats (also covered by automated backend tests).
## Visual, accessibility, and LAN checks
- Check all five routes at 390px and 1440px widths, portrait/landscape, and 200% text enlargement. No clipped dialogs or unreachable controls; the account table may scroll horizontally.
- Check keyboard-only navigation, skip link, focus indicators, native dialog focus containment/Escape/return focus, language dropdown, and form error announcements.
- Check dark/light modes and all four accents. Enable OS reduced motion: highlights and morph/ripple animations stop.
- Scroll account rows to observe compact navigation. Check notches and fullscreen bottom safe areas.
- Scan the QR on a second device; its URL must match the configured HTTPS origin. Test `webka.local` with mDNS on/off and the documented local DNS/IP fallback.
- Inspect browser network traffic: assets, API and WebSocket stay at the configured origin; media connects only between LAN peers. There should be no public STUN/TURN, external image/font, telemetry, or cloud requests.
- Confirm behavior on desktop Chromium/Firefox/Safari, Android Chrome, and iOS Safari where available. Record unsupported screen share, background suspension, camera switching, and client-isolation limits.