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:
- how dashboards stay current on their own
- what happens in environments where WebSockets can't connect (internal networks, proxies, mixed content)
- how polling saves battery and server load (it pauses when the tab is backgrounded)
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]
- (A) Polling — the front-end calls
/{screen}/liveevery 30 seconds viasetInterval. The response carries a cheap change token,rev; charts/cards are re-rendered only when it changes. When the tab is backgrounded (document.hidden), the call is skipped. - (B) Reverb push — the server broadcasts an event immediately (
ShouldBroadcastNow— no queue), and subscribed browsers receive it over the WebSocket. Today this is used solely for remote-emulate progress ticks.
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.
- Event:
App\Events\InfrRemoteEmulateTick(ShouldBroadcastNow) - Channel: a public channel
infr.emulate.{runId}—runIdis an unguessable random token (a capability). Being public, it needs no session-based/broadcasting/authwiring. - Event name:
.tick· payload is lightweight (host summary + progress counts only) - Fallback: the front-end subscribes and, at the same time, runs a 2-second fallback poll (
/asset/remote/emulate-status/{runId}) so progress is never lost even if the WS is absent or an early tick is missed.
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 channelinfr.emulate.{runId}, androutes/channels.phponly registers the framework-defaultApp.Models.User.{id}private channel. When private/presence channels are added, always authorize byuser_leveland scope (e.g.infr.threatsrequiresuser_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
- Mixed content —
echo.jsdecidesforceTLSfrom the runtime page protocol (wsswhen the page ishttps:). It deliberately does not force it from a build env — that prevents an https-served bundle from attemptingws://and getting blocked by the browser. If the front-end is https, Reverb must also serve TLS (wss) or a proxy must terminate it. - CSP / CORS —
echo.jssetsenabledTransports: ['ws','wss']+disableStats: trueto block pusher-js's sockjs CDN fallback and stats calls (which would violate a self-hosted CSP).
Q. Does the dashboard hammer the server/DB?
- The poll interval is 30 seconds, and it stops when the tab is backgrounded (
document.hidden). - The
revchange token (e.g.MAX(seq)) skips heavy re-renders when there is no new data. - Live builder services are mostly cached for 20–30 seconds.
Q. Can I schedule the realtime daemons?
Never. Do not spawn infinite-loop daemons like
reverb:start,queue:work, orpulse:checkfrom the scheduler (Schedule::command(...)). Processes that never exit pile up on every tick and exhaust the shared MySQL connection pool (measured 2026-08-06 withpulse: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
- Remote emulate — the one live job that actually uses Reverb push
- Diagnostics — the screen where remote-emulate progress ticks flow
- Dev · Observability (Pulse) — daemon-run discipline · where Realtime exceptions land
- Infrastructure · Servers — the canonical 30s-poll +
MAX(seq)rev-token example - Service Overview — the live-board polling pattern
- RBAC levels — access gates for channels and polling endpoints
