InfraGuard

Live updates (Reverb + polling)

The two ways screens refresh themselves — instant Reverb WebSocket pushes and lightweight 30-second polling with a cheap rev change-token — and why the console never breaks when WebSockets are off.

Overview — what it does

InfraGuard dashboards update themselves without a page reload. That auto-refresh runs on two mechanisms.

Mechanism How Latency Used by
Reverb WebSocket Server → browser instant push (event-driven) milliseconds live jobs that stream progress as ticks
Lightweight polling Browser GETs /…/live on an interval up to 30s most dashboards (Health, Servers, Traffic, Service, Security, Alerts …)

One design rule sums it up:

Live is an enhancement layered on top of persisted data. Every screen first renders data stored in the DB, and realtime updates ride on top of that. So even if Reverb (the WebSocket server) is off, blocked by a firewall, or restarting, the screen never breaks. When push is unavailable, polling fills in (graceful degradation).

In practice, nearly every screen runs on polling. The Reverb WebSocket is installed and wired, and today it powers exactly one feature — the security-diagnostics remote emulate (Remote emulate · Diagnostics), where it streams per-host progress ticks. All other live screens refresh with 30-second polling plus a cheap rev change-detection token.


When to use

This is a conceptual reference. It is not a screen with a live route — it is for operators and engineers who want to understand:


Layout

Two-path architecture

[persisted data]  server_status · infr_db_metric · blacklist_info · …
      │
      ▼ (initial page load: server injects data-* JSON into the view)
[browser screen]  ← renders instantly (complete without live)
      ▲                          ▲
      │ (A) polling               │ (B) Reverb push
      │  GET /…/live (≤30s)       │  WS event 'tick'
      │  re-paint only if rev↑    │  ShouldBroadcastNow
   [Laravel controller]        [Reverb server]

Reverb topology

Item Value Source
Client libraries laravel-echo + pusher-js dynamic import (excluded from the main bundle)

WebSocket connection details are set up at install time. Operators simply open the screen — there is nothing to configure.


How to use

Operators — nothing to do

Live is automatic. Leave a screen open and polling fetches the latest values every 30 seconds; if the WebSocket is on, event-driven screens update instantly. To save power, polling pauses while the tab is in the background and resumes when you return to it.

Turning the Reverb WebSocket on (optional)

WebSocket push is configured at install time by your deployment contact — it is not an operator toggle in the console. If you need real-time push, contact your administrator or the vendor.

WebSocket push being off is not an error. It is an intentional off switch — live screens keep working on polling alone.

What polling actually does

// Representative pattern (e.g. service-overview)
poll_ms: 30000,
this._poll = setInterval(() => {
    if (this.live && !document.hidden) this.poll_tick();   // skip when tab backgrounded
}, this.poll_ms);
// poll_tick → GET /service/live → re-render only when the response's rev changed

The server-side rev is the cheapest possible change signal. For example, the server-metrics screen uses MAX(seq) from the legacy server_status table as its rev — the value only rises when the collector inserts a new snapshot.

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

The one live job that uses Reverb push — remote emulate

Diagnostics remote emulate broadcasts a progress tick each time a host finishes.


Data sources

File Role
resources/js/echo.js The app's single Echo builder (SOT) — dynamic import · config gate → null · runtime forceTLS
app/Support/Realtime.php Broadcast safety net (up() fsockopen probe · fire() exception absorber)
routes/channels.php Channel auth callbacks (currently only the default App.Models.User.{id})
app/Events/InfrRemoteEmulateTick.php The one broadcast event actually in use
each screen's …/live controller + …Service::rev() live payload for polling + change token

Realtime — two-layer defense (the WS can die without killing the original work)

// (1) up() — 150ms fsockopen probe (memoized per request). An event's broadcastWhen() gates on this,
//     so when Reverb is down a ShouldBroadcastNow event does NOT inline-throw on connect-timeout.
public static function up(): bool  // probes from configuration (no hardcoding)

// (2) fire() — protects the caller even when Reverb is up but rejects (413, or 5xx while restarting).
//     Failures are report()ed to Pulse Exceptions; the original work (a saved scan result) still succeeds.
public static function fire(object $event): void

An event's broadcastWhen() returns Realtime::up() — if Reverb is off, it skips the send entirely.


Access

Channel authorization

Channel type Auth
Public channel (infr.emulate.{runId}) none — the runId capability token is the access grant (unguessable)
private / presence channels (design roadmap) routes/channels.php callback checks session user_level + role scope

The realtime channel table in CLAUDE.md (private-infr.server.{id}, private-infr.threats, presence-infr.ops, …) is a design roadmap. The broadcast implemented today is the single public channel infr.emulate.{runId}, and routes/channels.php only registers the framework-default App.Models.User.{id} private channel. When private/presence channels are added, always authorize by user_level and scope (e.g. infr.threats requires user_level>=2 && scope∋security).

Polling endpoint access

The /{screen}/live polling endpoints inherit the read-level gate of their screen's controller (demo=1, staff=5, admin=10; some areas gated by the infr.level:N middleware). For example, remote emulate's status fallback poll (emulate-status) requires write access (level≥2). See RBAC levels for the full policy.


Troubleshooting & FAQ

Q. WebSockets won't connect, but the screen keeps updating?

That's expected. Live screens are fully functional on polling without a WS. WebSockets only cut latency for event-driven jobs like remote emulate.

Q. I enabled Reverb but the browser won't attach

Q. Does the dashboard hammer the server/DB?

Q. Can I schedule the realtime daemons?

Never. Do not spawn infinite-loop daemons like reverb:start, queue:work, or pulse:check from the scheduler (Schedule::command(...)). Processes that never exit pile up on every tick and exhaust the shared MySQL connection pool (measured 2026-08-06 with pulse:check: 169 accumulated over 16 hours → [1040] Too many connections, affecting the production DB). Run daemons only from the launcher's single persistent pane (dev) or supervisor/systemd (prod). See Dev · Observability for daemon placement.

Q. How do I restart Reverb after a code change?

In production, zero-downtime php artisan reverb:restart (or restart it under supervisor). In development, restart the launcher pane. If the WS briefly rejects during restart, Realtime::fire() absorbs the exception and polling covers the gap.


Related

The LIVE indicator in the header — the screen is refreshing itself
The LIVE indicator in the header — the screen is refreshing itself