# Web console and Web Push

> HTML: https://remote.ivjn.us/docs/en/web-console/

The browser console is served at `https://remote.ivjn.us/console/`. It supports registration with email verification, account/password or operator-token login, workspace-scoped devices, streaming commands, an xterm PTY, Android screenshots/taps/navigation/text input, metrics, audit records, device creation/deletion, and opt-in notifications. Readonly accounts cannot execute commands or view Android screens. Offline devices remain visible.

Devices advertising `ubus` expose an **ubus** tab with object, method and optional JSON-object parameters (blank sends `null`). Calls use sealed sessions and require an online, enabled device with operation access. Cancel stops waiting; it cannot undo a method already executed on the device. Device switches and permission loss clear pending calls and results.

Registration creates a separate workspace. The email must be verified using its eight-digit code before login; the console supports resending codes and returning to verification after a reload. Exchange must have SMTP configured (`SMTP_HOST`, `SMTP_PORT`, `SMTP_FROM`, and authentication/security settings in `.env.example`). Without an available mail service, registration reports an error instead of bypassing verification.

Credentials stay in page memory. Refreshing requires login again. Logging out closes sessions, attempts to remove this browser's push subscription, and revokes password-login sessions. Primary operator tokens retain their existing lifetime. Notification clicks focus an open console without reloading its credentials.

## Build and serve

```sh
cd website/console
npm ci
npm test
npm run build
```

Copy `dist/` to the website's `console/` directory. Keep the existing `website/assets/` available at `/assets/` for shared fonts and the mark. Serve `index.html`, `sw.js`, and the manifest with revalidation/no-cache. Hashed assets may be cached immutably. Do not cache `/api/*` or `/ws/*`.

The console uses same-origin `/api/*` and `/ws/browser`. Reverse proxy these to Exchange, preserving Host and allowing WebSocket upgrades. Do not strip the `/api` or `/ws` prefix. The Vite development server proxies API/WS to `127.0.0.1:18082`. Rust Exchange permits same-origin browser access by default. Configure exact origins with `REMOTE_CORS_ORIGINS`; only an explicit `*` permits all origins. Worker origins use `CORS_ORIGINS` and its own defaults. Bearer authentication and one-use WebSocket tickets remain required; cross-origin cookies are not enabled.

Browser authentication uses `POST /api/ws-ticket` with Bearer authorization, followed by `GET /ws/browser` and a first JSON frame `{"ticket":"..."}`. Tickets expire in 30 seconds, are single-use, and recheck the operator's enabled state and credential validity when consumed. Long-lived credentials are never put in WebSocket URLs. Native clients keep using `/ws/operator`.

All device operations use native sealed WebSocket sessions. HTTP provides authentication, device metadata, key and grant directories, and relay session administration; see [HTTP API for Agent operations](/docs/en/http-api.md).

## Enable Web Push

Set both variables in the Exchange environment (missing both disables push):

```text
WEB_PUSH_VAPID_PRIVATE_KEY=<base64url-unpadded-32-byte-P256-private-key>
WEB_PUSH_VAPID_SUBJECT=https://remote.ivjn.us
```

Generate a private key with `python scripts/generate-vapid-key.py`. Store it outside Git in a restricted environment file, keep it stable across restarts, and back it up securely. The public key is derived automatically. Changing the key requires browsers to subscribe again.

`WEB_PUSH_ALLOWED_ORIGINS` defaults to the exact trusted origins `https://fcm.googleapis.com,https://updates.push.services.mozilla.com,https://web.push.apple.com`. Add another browser vendor only after verifying its HTTPS push origin. Client-supplied endpoints cannot expand this allowlist. Redirects are never followed.

Authenticated routes:

| Method and path | Purpose |
| --- | --- |
| GET `/api/webpush/config` | Enabled state and public application-server key |
| GET/POST `/api/webpush/subscriptions` | List owned metadata or subscribe/upsert |
| DELETE `/api/webpush/subscriptions/{id}` | Remove an owned subscription |
| POST `/api/webpush/test` | Queue a visible test to the caller's subscriptions |
| POST `/api/webpush/messages` | Send to yourself, or as admin to another account in the same workspace |
| POST `/api/webpush/devices/{id}/messages` | Notify subscribed administrators who manage the authenticated device |

Subscriptions use the browser's `PushSubscription.toJSON()`, `application_server_key`, and `triggers`. `triggers` holds at most one `online` object and one `offline` object. Each object has `kind`, `tick_count` (default 3, range 1–100), and optional `device_ids`. `offline` notifies after `tick_count` consecutive misses; `online` notifies after `tick_count` consecutive successes. `device_ids` is one device id (a string), a list of device ids, or omitted/`null` for every device in the workspace. An empty `triggers` array subscribes the browser without automatic presence notifications. `missed_count` and `success_count` are not accepted. Messages use optional `operator_id` and `notification` with `title`, `body`, optional same-origin relative `url` and `tag`. `/webpush/` is a minimal reference client; the production console uses its own worker scoped to `/console/`.

Migration `0005_web_push.sql` adds subscriptions and the durable outbox. `0008_online_offline_triggers.sql` moves device notifications onto `triggers` and drops `missed_count` / `success_count`. `202` means queued, not delivered or displayed. Jobs expire after 15 minutes. Network errors, 408, 429 and 5xx retry up to five attempts; 404/410 remove the matching subscription revision. A 90-second lease allows pending work to recover after worker failure. Delivery is at least once and browser display depends on vendor delivery, permission and operating-system policy. Device events before durable enqueue are best-effort under overload; events already queued survive restarts. Limits: 20 subscriptions/account, 32 queued messages/subscription and 30 mutations/minute/account.

iOS/iPadOS may require adding the console to the home screen before enabling notifications. Browser Push service reachability is separate from Exchange connectivity.

## Validation

Run `cargo test --workspace`, `cargo clippy --workspace --all-targets -- -D warnings`, and the console's npm tests/build. For the PostgreSQL/HTTPS integration test, install `psycopg[binary]`, set `WEB_PUSH_TEST_ADMIN_URL` to a test PostgreSQL connection with CREATE DATABASE rights, then run `python scripts/test-webpush.py`. It creates and removes a disposable database; never point at a database role with unintended privileges.

## Send from every client

Web Console has a message form in Browser notifications. Mobile Console Settings has Web Push send/test controls and an Open Web Console link. Desktop Agent local settings, Android Agent Settings and the browser extension configuration page can send using device credentials. Headless/OpenWrt agents use the command below. Receive notifications by enabling them in Web Console/PWA; this does not add native FCM/APNs receivers.

```sh
remote-cli webpush status
remote-cli webpush subscriptions
remote-cli webpush test
remote-cli webpush send --title "Backup ready" --body "Completed"
remote-cli webpush send --operator-id ACCOUNT_UUID --title "Notice" --body "Hello"
remote-agent --config /path/to/config.json webpush --title "Device ready" --body "Completed"
```

```json
{"notification":{"title":"Backup ready","body":"Completed","url":"/console/","device_id":"edge-1"}}
```

POST `/api/webpush/messages` uses operator Bearer authentication. Omit `operator_id` to send to yourself; another recipient requires admin permission and must be in the same workspace. The JSON body above also works with `remote-cli webpush send --json-file message.json`. Empty presence triggers disable automatic online/offline alerts while retaining generic message delivery.

POST `/api/webpush/devices/{id}/messages` uses that device’s Bearer token and accepts a notification object directly (`title`, `body`, optional `tag`). Only active subscriptions of enabled administrators who manage that device receive it. The server sets `device_id`, `event=device.message`, `occurred_at` and `/console/`; callers cannot choose an account or impersonate another device. Device sends share a 30 requests/minute/workspace budget and a maximum of 256 subscriptions per send. Account subscription and queue limits still apply. No active recipient returns `202 {"queued":0}`; `queued` counts subscriptions, not deliveries.
