Skip to main content

Dashboard Push (optional)

Replaces dashboard polling with a SignalR push. Job-finalised and message-enqueued events fan out to every connected dashboard within one coalesce window, carrying the current DashboardStatistics DTO as the payload. Connected clients no longer trigger N × GET /api/status refetches — a single server-side stats query feeds every client per event.

Opt-in; without opt.AddDashboardPush() the dashboard runs entirely on REST refetch and a 30 s safety-net poll.

Setup

builder.Services.AddWarp<AppDbContext>(opt =>
{
opt.UsePostgreSql(); // or opt.UseSqlServer()

// Multi-server fanout — needed when more than one server processes jobs.
opt.UseDatabasePush();

opt.AddDashboardPush(o =>
{
o.CoalesceWindow = TimeSpan.FromMilliseconds(100); // default
});
});

The hub is mounted at ${RoutePrefix}/api/hub (default /warp/api/hub). The frontend reads ${RoutePrefix}/api/addons once at boot and falls back to 30 s polling when push: false (i.e. AddDashboardPush() was not called). Same discovery endpoint reports concurrency / rate-limit / saga addon availability for nav visibility.

What gets broadcast

EventTriggerPayload
JobFinalizedA job reaches a terminal state (Completed / Failed / Deleted / Cancelled).DashboardStatistics snapshot.
MessageEnqueuedA new IMessage row is written and the router signals downstream consumers.DashboardStatistics snapshot.

Both events arrive as SignalR invocations with the DTO as the first arg. Clients update their navbar counts and chart series directly from the payload; per-view data (filtered job lists, job detail, logs) stays on event-driven REST refetch.

Coalescing

Each signal sets a latched flag and releases the broadcaster's wake semaphore. The loop wakes, waits CoalesceWindow (default 100 ms), then emits at most one broadcast per event kind per window. A 50-job batch finalising in a 20 ms burst collapses to one JobFinalized broadcast, not 50.

CoalesceWindow = TimeSpan.Zero disables coalescing — every signal becomes its own broadcast. Useful only for tests that need deterministic counts.

Multi-server fanout requires DB push

The broadcaster subscribes to in-process ServerTaskSignals<TContext>. In a single-server deployment that's enough — every signal fires on the same process that processed the job. In a multi-server deployment, signals raised by server A's workers don't reach server B's broadcaster unless UseDatabasePush() is also on; without it, clients connected to server A only see events from server A's workers until the 30 s safety-net poll closes the gap.

The broadcaster is the third consumer of ServerTaskSignals<TContext>, after Orchestrator and MessageRouter.

Auth piggybacks on WarpUIMiddleware

Both the SignalR negotiate (POST /warp/api/hub/negotiate) and the WebSocket-upgrade HTTP request pass through /api/, so WarpUIMiddleware's existing 401-on-unauthenticated behaviour catches both. An auth-protected dashboard needs no extra wiring — built-in cookie login and custom IWarpAuthorizationFilter both work.

Connection states

The navbar shows a fixed-width status pill that cycles through:

  • Live — connected to the hub.
  • Connecting — initial handshake or first reconnect attempt.
  • Reconnecting — transient drop, exponential-backoff retries.
  • Polling — hub unavailable (404 probe, repeated reconnect failures); 30 s safety-net poll is driving the UI.

OpenTelemetry

Two instruments on WarpTelemetry.Meter = "Warp":

  • warp.dashboard.connections.active — UpDownCounter, current SignalR connection count.
  • warp.dashboard.events.broadcast — Counter, broadcast emissions tagged by event kind.

The broadcaster also logs at Warning on broadcast failure and stats-fetch failure (the stats fetch is best-effort — on failure the event is still broadcast without a payload so clients fall through to their REST refetch path).

Out of scope (v1)

  • Per-user / per-job groups (Clients.User / Clients.Group) — hub is broadcast-only.
  • Client-initiated hub commands — hub is one-way (server → client).
  • Redis backplane — DB push is the cross-server fanout.
  • Per-view push payloads — job lists, job detail, and logs stay on event-driven REST refetch.
  • Removing the 30 s safety-net poll — kept as a backstop for missed events / dropped reconnects.
  • User-facing polling toggle — handled by addon presence vs. absence, not a runtime switch.