InfraGuard

ライブ更新(Reverb + ポーリング)

画面が自動的に更新される2つの仕組み — Reverb WebSocket による即時プッシュと、軽量な rev 変更トークンを使う30秒ポーリング — そして WebSocket が無効でもコンソールが決して壊れない理由。

概要 — 何をするものか

InfraGuard のダッシュボードは、ページを再読み込みしなくても自動的に更新されます。この自動更新は2つの仕組みの上で動いています。

仕組み 方式 レイテンシ 利用箇所
Reverb WebSocket サーバー → ブラウザへの即時プッシュ(イベント駆動) ミリ秒 進捗を tick としてストリーミングするライブジョブ
軽量ポーリング ブラウザが一定間隔で /…/live を GET 最大30秒 ほとんどのダッシュボード(ヘルス、サーバー、トラフィック、サービス、セキュリティ、警報 …)

設計上の原則は次の一文に集約されます。

ライブ更新は、永続化されたデータの上に重ねる拡張機能です。 すべての画面はまず DB に保存されたデータを描画し、リアルタイム更新はその上に乗ります。したがって Reverb(WebSocket サーバー)が停止していても、ファイアウォールでブロックされていても、再起動中でも、画面は決して壊れません。 プッシュが利用できないときはポーリングが肩代わりします(グレースフルデグラデーション)。

実運用では、ほぼすべての画面がポーリングで動いています。 Reverb WebSocket は導入・配線済みで、現時点で担っている機能はただ1つ — セキュリティ診断のリモートエミュレート(リモートエミュレート · セキュリティ診断)で、ホストごとの進捗 tick をストリーミングします。それ以外のライブ画面はすべて、30秒ポーリングと軽量な rev 変更検知トークンで更新されます。


こんなときに使う

これは概念のリファレンスです。ライブルートを持つ画面ではなく、次のことを理解したい運用者・エンジニアのための項目です。


画面構成

2経路アーキテクチャ

[永続化データ]  server_status · infr_db_metric · blacklist_info · …
      │
      ▼ (初回ページ読み込み: サーバーが data-* JSON をビューに埋め込む)
[ブラウザ画面]  ← 即座に描画(ライブなしでも完結)
      ▲                          ▲
      │ (A) ポーリング            │ (B) Reverb プッシュ
      │  GET /…/live (≤30s)     │  WS イベント 'tick'
      │  rev↑ のときだけ再描画     │  ShouldBroadcastNow
   [Laravel コントローラー]     [Reverb サーバー]

Reverb のトポロジー

項目 値 出典
クライアントライブラリ laravel-echo + pusher-js 動的インポート(メインバンドルからは除外)

WebSocket の接続設定は導入時に構成されます。運用者は画面を開くだけで、設定する項目はありません。


使い方

運用者 — 何もする必要はありません

ライブ更新は自動です。画面を開いたままにしておけば、ポーリングが30秒ごとに最新値を取得します。WebSocket が有効なら、イベント駆動の画面は即座に更新されます。電力を節約するため、タブが背面にある間はポーリングが一時停止し、戻ると再開します。

Reverb WebSocket を有効にする(任意)

WebSocket プッシュは導入担当者が導入時に設定するものであり、コンソール上の運用者向けトグルではありません。リアルタイムプッシュが必要な場合は、管理者またはベンダーにご連絡ください。

WebSocket プッシュが無効であることはエラーではありません。これは意図的なオフスイッチであり、ライブ画面はポーリングだけで問題なく動作し続けます。

ポーリングが実際に行っていること

// 代表的なパターン(例: service-overview)
poll_ms: 30000,
this._poll = setInterval(() => {
    if (this.live && !document.hidden) this.poll_tick();   // タブが背面にあるときはスキップ
}, this.poll_ms);
// poll_tick → GET /service/live → レスポンスの rev が変わったときだけ再描画

サーバー側の rev は可能な限り安価な変更シグナルです。たとえばサーバーメトリクス画面では、レガシーの server_status テーブルの MAX(seq) を rev として使います。この値はコレクターが新しいスナップショットを挿入したときにのみ増加します。

// App\Services\Metric\ServerMetricService::rev()
return (int) DB::connection('legacy')->table('server_status')->max('seq');

Reverb プッシュを使う唯一のライブジョブ — リモートエミュレート

セキュリティ診断のリモートエミュレートは、ホストが1台終わるたびに進捗 tick をブロードキャストします。


データソース

ファイル 役割
resources/js/echo.js アプリ唯一の Echo ビルダー(SOT) — 動的インポート · 設定ゲート → null · ランタイムでの forceTLS
app/Support/Realtime.php ブロードキャストの安全網(up() の fsockopen プローブ · fire() の例外吸収)
routes/channels.php チャネル認証コールバック(現在はフレームワーク既定の App.Models.User.{id} のみ)
app/Events/InfrRemoteEmulateTick.php 実際に使われている唯一のブロードキャストイベント
各画面の …/live コントローラー + …Service::rev() ポーリング用のライブペイロード + 変更トークン

Realtime — 二層の防御(WS が死んでも元の処理は道連れにならない)

// (1) up() — 150ms の fsockopen プローブ(リクエストごとにメモ化)。イベントの broadcastWhen() がこれで判定するため、
//     Reverb が停止しているときに ShouldBroadcastNow イベントが接続タイムアウトでインライン例外を投げることはない。
public static function up(): bool  // 設定から探索する(ハードコーディングなし)

// (2) fire() — Reverb が起動していても拒否する場合(413、あるいは再起動中の 5xx)に呼び出し元を保護する。
//     失敗は report() で Pulse Exceptions に送られ、元の処理(保存済みのスキャン結果)はそのまま成功する。
public static function fire(object $event): void

イベントの broadcastWhen() は Realtime::up() を返します — Reverb が停止していれば、送信そのものをスキップします。


アクセス権限

チャネルの認可

チャネル種別 認証
パブリックチャネル(infr.emulate.{runId}) なし — 推測不能な runId ケーパビリティトークンそのものがアクセス権になります
private / presence チャネル(設計ロードマップ) routes/channels.php のコールバックがセッションの user_level とロールスコープを検査します

CLAUDE.md にあるリアルタイムチャネル表(private-infr.server.{id}、private-infr.threats、presence-infr.ops …)は設計ロードマップです。現時点で実装されているブロードキャストは単一のパブリックチャネル infr.emulate.{runId} だけで、routes/channels.php にはフレームワーク既定のプライベートチャネル App.Models.User.{id} のみが登録されています。private / presence チャネルを追加する際は、必ず user_level とスコープで認可してください(例: infr.threats は user_level>=2 && scope∋security を要求)。

ポーリングエンドポイントのアクセス権限

/{screen}/live ポーリングエンドポイントは、その画面のコントローラーの読み取りレベルゲートを継承します(demo=1、staff=5、admin=10。一部の領域は infr.level:N ミドルウェアで制御されます)。たとえばリモートエミュレートのステータスフォールバックポーリング(emulate-status)は書き込み権限(レベル2以上)を必要とします。ポリシー全体は RBAC レベル を参照してください。


トラブルシューティングと FAQ

Q. WebSocket が接続できないのに、画面は更新され続けます

想定どおりの動作です。ライブ画面は WS がなくてもポーリングだけで完全に機能します。WebSocket は、リモートエミュレートのようなイベント駆動ジョブのレイテンシを縮めるだけのものです。

Q. Reverb を有効にしましたが、ブラウザが接続しません

Q. ダッシュボードはサーバーや DB に負荷をかけませんか

Q. リアルタイム系のデーモンをスケジューラーに登録できますか

絶対にしないでください。 reverb:start、queue:work、pulse:check のような無限ループ型のデーモンをスケジューラー(Schedule::command(...))から起動してはいけません。終了しないプロセスが tick ごとに積み上がり、共有 MySQL のコネクションプールを枯渇させます(2026-08-06 に pulse:check で実測: 16時間で169個が蓄積 → [1040] Too many connections が発生し、本番 DB にも影響しました)。デーモンは、開発時はランチャーの常駐ペイン1つから、本番では supervisor / systemd からのみ実行してください。デーモンの配置については 開発 · オブザーバビリティ を参照してください。

Q. コード変更後に Reverb を再起動するには

本番では無停止の php artisan reverb:restart を使う(または supervisor 配下で再起動する)方法があります。開発時はランチャーのペインを再起動します。再起動中に WS が一時的に接続を拒否しても、Realtime::fire() が例外を吸収し、その間はポーリングが穴を埋めます。


関連項目

헤더의 LIVE 표시 — 화면이 스스로 갱신되는 중
헤더의 LIVE 표시 — 화면이 스스로 갱신되는 중