# Web コンソールと Web Push

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

ブラウザーコンソールは `https://remote.ivjn.us/console/` で提供されます。メール認証付き登録、アカウント・パスワードまたは運用トークンによるログイン、ワークスペース単位のデバイス管理、ストリーミングコマンド、xterm PTY、Android の画面取得・タップ・ナビゲーション・文字入力、メトリクス、監査記録、デバイス作成・削除、任意の通知に対応します。読み取り専用アカウントはコマンド実行や Android 画面の閲覧ができません。オフラインのデバイスも表示されます。

`ubus` 機能を通知するデバイスには **ubus** タブが表示され、オブジェクト、メソッド、任意の JSON オブジェクト引数を入力できます（空欄は `null`）。呼び出しには暗号化セッション、オンラインで有効なデバイス、操作権限が必要です。待機を中止しても実行済みの処理は取り消せません。デバイスの切り替えや権限の喪失で保留中の呼び出しと結果を消去します。

登録すると独立したワークスペースを作成します。ログイン前に 8 桁のコードでメールアドレスを認証する必要があります。コンソールはコードの再送と、再読み込み後の認証画面への復帰に対応します。Exchange には SMTP 設定が必要です（`SMTP_HOST`、`SMTP_PORT`、`SMTP_FROM`、および `.env.example` の認証・セキュリティ設定）。メールサービスが使えない場合、認証を省略せず登録エラーを返します。

認証情報はページのメモリ内にのみ保持します。再読み込み後は再ログインが必要です。ログアウト時はセッションを閉じ、このブラウザーの Push 購読の削除を試み、パスワードログインのセッションを失効させます。主運用トークンの有効期間は変わりません。通知をクリックすると、開いているコンソールにフォーカスし、認証情報を再読み込みしません。

## ビルドと配信

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

`dist/` を Web サイトの `console/` ディレクトリにコピーします。共有フォントとマークのため、既存の `website/assets/` を `/assets/` で公開してください。`index.html`、`sw.js`、マニフェストは再検証または no-cache で配信します。ハッシュ付きアセットは immutable でキャッシュできます。`/api/*` や `/ws/*` はキャッシュしないでください。

コンソールは同一オリジンの `/api/*` と `/ws/browser` を使います。Host を保持し、WebSocket アップグレードを許可して Exchange にリバースプロキシしてください。`/api` や `/ws` のプレフィックスを削除しないでください。Vite 開発サーバーは API/WS を `127.0.0.1:18082` にプロキシします。Rust Exchange は既定で同一オリジンのブラウザーアクセスを許可します。`REMOTE_CORS_ORIGINS` に正確なオリジンを設定し、明示的な `*` の場合だけ全オリジンを許可します。Worker は `CORS_ORIGINS` と独自の既定値を使用します。Bearer 認証と一回限りの WebSocket チケットは引き続き必須で、クロスオリジン Cookie は使いません。

ブラウザー認証は Bearer 認証付きの `POST /api/ws-ticket` に続いて、`GET /ws/browser` と最初の JSON フレーム `{"ticket":"..."}` を使用します。チケットは 30 秒で期限切れになり、1 回限り使用できます。使用時にはオペレーターの有効状態と認証情報の有効性を再検証します。長期認証情報を WebSocket URL に含めることはありません。ネイティブクライアントは引き続き `/ws/operator` を使用します。

すべてのデバイス操作はネイティブの密封 WebSocket セッションを使用します。HTTP は認証、デバイスのメタデータ、鍵と認可のディレクトリ、中継セッション管理を提供します。[Agent 操作用 HTTP API](/docs/ja/http-api.md)を参照してください。

## Web Push の有効化

Exchange の環境に両方の変数を設定します（両方とも未設定なら Push は無効）。

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

`python scripts/generate-vapid-key.py` で秘密鍵を生成します。Git 外のアクセス制限された環境ファイルに保存し、再起動後も同じ鍵を使い、安全にバックアップしてください。公開鍵は自動導出されます。鍵を変更するとブラウザーで再購読が必要です。

`WEB_PUSH_ALLOWED_ORIGINS` の既定値は、信頼された正確なオリジン `https://fcm.googleapis.com,https://updates.push.services.mozilla.com,https://web.push.apple.com` です。他のブラウザーベンダーを追加する場合は、HTTPS の Push オリジンを先に確認してください。クライアントが送るエンドポイントでこの許可リストを拡張することはできません。リダイレクトは追跡しません。

認証が必要なルート:

| メソッドとパス | 用途 |
| --- | --- |
| GET `/api/webpush/config` | 有効状態と公開アプリケーションサーバー鍵 |
| GET/POST `/api/webpush/subscriptions` | 自分のメタデータ一覧、または購読・upsert |
| DELETE `/api/webpush/subscriptions/{id}` | 自分の購読を削除 |
| POST `/api/webpush/test` | 呼び出し元の購読へ表示テストをキュー投入 |
| POST `/api/webpush/messages` | 自分に送信、または管理者が同一ワークスペースの別アカウントへ送信 |
| POST `/api/webpush/devices/{id}/messages` | 認証したデバイスを管理する購読済み管理者へ送信 |

購読にはブラウザーの `PushSubscription.toJSON()`、`application_server_key`、`triggers` を使用します。`triggers` は `online` オブジェクトと `offline` オブジェクトをそれぞれ最大 1 つ含みます。各オブジェクトには `kind`、`tick_count`（既定 3、範囲 1～100）、任意の `device_ids` があります。`offline` は `tick_count` 回連続で応答がないと通知し、`online` は `tick_count` 回連続で成功すると通知します。`device_ids` は 1 つのデバイス ID（文字列）、デバイス ID のリスト、または省略 / `null`（ワークスペース内の全デバイス）です。空の `triggers` 配列は自動オンライン/オフライン通知なしでブラウザーを購読します。`missed_count` と `success_count` は受け付けません。メッセージは任意の `operator_id` と `notification` を使用し、後者には `title`、`body`、任意の同一オリジン相対 `url` と `tag` を指定します。`/webpush/` は最小限の参照クライアントです。本番コンソールは `/console/` スコープの独自 worker を使います。

マイグレーション `0005_web_push.sql` は購読と永続 outbox を追加します。`0008_online_offline_triggers.sql` はデバイス通知を `triggers` に移行し、`missed_count` / `success_count` を削除します。`202` はキュー投入済みを意味し、配信済み・表示済みではありません。ジョブは 15 分で期限切れになります。ネットワークエラー、408、429、5xx は最大 5 回試行し、404/410 は該当リビジョンの購読を削除します。90 秒のリースにより worker 障害後に未処理作業を復旧できます。配信は少なくとも 1 回で、ブラウザーでの表示はベンダーの配信、権限、OS のポリシーに依存します。永続キューへの投入前のデバイスイベントは、高負荷時にはベストエフォートです。投入済みイベントは再起動後も残ります。上限はアカウントあたり 20 購読、購読あたり 32 件の待機メッセージ、アカウントあたり毎分 30 回の変更操作です。

iOS/iPadOS では通知を有効にする前にコンソールをホーム画面に追加する必要がある場合があります。ブラウザーの Push サービスへの到達性は Exchange 接続とは別です。

## 検証

`cargo test --workspace`、`cargo clippy --workspace --all-targets -- -D warnings`、コンソールの npm テストとビルドを実行してください。PostgreSQL/HTTPS 統合テストでは `psycopg[binary]` をインストールし、`WEB_PUSH_TEST_ADMIN_URL` に CREATE DATABASE 権限のあるテスト用 PostgreSQL 接続を設定してから、`python scripts/test-webpush.py` を実行します。使い捨てデータベースを作成・削除するため、意図しない権限を持つデータベースロールを指定しないでください。

## 各クライアントから送信

Web Console のブラウザー通知にメッセージフォームがあります。モバイル Console の設定には Web Push の送信、テスト、Web Console を開く操作があります。デスクトップ Agent のローカル設定、Android Agent の設定、ブラウザー拡張の設定ページからデバイス認証情報で送信できます。ヘッドレス/OpenWrt では次のコマンドを使います。受信は Web Console/PWA で通知を有効にします。ネイティブ FCM/APNs 受信機能は追加しません。

```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` はオペレーターの Bearer 認証を使います。`operator_id` を省略すると自分に送信します。他の受信者には管理者権限が必要で、同じワークスペースに属する必要があります。上記の JSON は `remote-cli webpush send --json-file message.json` でも使えます。状態トリガーを空配列にすると、自動オンライン/オフライン通知を停止し、汎用メッセージは受信できます。

POST `/api/webpush/devices/{id}/messages` はデバイスの Bearer トークンを使い、通知オブジェクト（`title`、`body`、任意の `tag`）を直接受け取ります。そのデバイスを管理する有効な管理者の購読だけが対象です。サーバーが `device_id`、`event=device.message`、`occurred_at`、`/console/` を設定し、送信者はアカウントや他のデバイスを指定できません。デバイス送信はワークスペースごとに毎分 30 回、1 回最大 256 購読です。アカウントの購読数とキュー制限も適用されます。対象がなければ `202 {"queued":0}` を返します。`queued` は購読数であり配達数ではありません。
