Realtime Events (SignalR)
Live updates travel over one SignalR hub at /hub/realtime. Authentication
uses the same JWT as the REST API, passed as ?access_token=<jwt> (or an
Authorization header).
Subscription groups
Section titled “Subscription groups”High-frequency streams are opt-in per group, joined only while a consuming screen is mounted and re-joined automatically on reconnect:
| Group | Carries | Consumed by |
|---|---|---|
sensors | Live sensor reading batches | Realtime Monitor |
predictions | New prediction batches | Dashboard charts |
logs | Full log stream (all levels) | LogViewer page only |
alerts | Warning/Error log entries only | Notification bell, app-wide |
The alerts group exists so warnings surface everywhere without streaming the
full logs firehose to every dashboard.
Events (server → client)
Section titled “Events (server → client)”The hub publishes twelve events, listed here in full. Field names arrive camel-cased: the JSON hub protocol applies ASP.NET Core’s camelCase policy to the server-side payload records. Timestamps are Unix epoch milliseconds unless noted.
| Event | Delivered to | Payload |
|---|---|---|
Connected | The connecting client only | connectionId, timestamp |
SensorReadingBatch | sensors group | ticks — the sensor ticks coalesced since the last flush (shape below) |
NewPredictionBatch | predictions group | predictions — the prediction records coalesced since the last flush (shape below) |
LogEntry | logs group | id, timestamp (ISO-8601 string), source, level, message, logger (nullable), correlationId (nullable; the same id on every line of one tick, from the input read to each output write), inputDatasourceId, inputDatasourceName, outputDatasourceId, outputDatasourceName (all nullable; set when the line belongs to an inference tick — names are as of the time of the line, ids stay stable across renames) |
AlertLogEntry | alerts group | Same shape as LogEntry, carrying warning and error entries only |
InferenceStateChanged | All clients | state ∈ idle · ready · running, activeInputDatasourceId (nullable), loadedModelId (nullable), loadedModelVersion (nullable), timestamp — emitted on every lifecycle transition |
InferenceFaulted | All clients | reason, datasourceId (nullable), timestamp, severity (defaults to error), code (nullable) — also raises the blocking fault banner |
HealthMetricsUpdate | All clients | status, timestamp, gpu (nullable object), cpu, memory, storage (nullable object), uptimeSeconds (inference engine), backendUptimeSeconds, engineRestartCount, lastEngineRestartAt (nullable) (nested shapes below) |
ModelActivated | All clients | modelId, version, inputShape, outputShape, fileSizeMb |
ModelUploadProgress | All clients | modelId, version, phase, isError, errorMessage (nullable) |
ModelUploadFailed | All clients | modelId, version, reason |
OutputWriteFailed | All clients | datasourceId, reason, code (nullable), severity (warning), timestamp, count — coalesced per sink: the first failure alerts immediately, then at most one follow-up per 30 s window with count repeats folded in |
An event name is the method name on the server’s hub client interface, so the names above are the literal wire names a client subscribes to.
Nested payload shapes
Section titled “Nested payload shapes”Each entry of SensorReadingBatch.ticks is one tick — every channel value
stamped at a single producer time:
| Field | Meaning |
|---|---|
timestamp | Producer time for the whole tick |
values | One value per channel; values[i] is channel index i |
HealthMetricsUpdate nests these objects:
| Object | Fields |
|---|---|
gpu | utilization, memoryUsedMb, memoryTotalMb, memoryUsagePercent, temperatureCelsius — the whole object is null on a host with no GPU, and before the first successful poll |
gpu.memoryKind | shared (Jetson: the GPU uses system RAM, so the GPU memory fields repeat the host reading), dedicated (discrete VRAM), or null when unknown |
cpu | usagePercent, temperatureCelsius (null when the host exposes no CPU thermal sensor) |
memory | usagePercent, usedMb, totalMb — the host’s RAM |
memory swap | swapUsedMb, swapTotalMb, swapUsagePercent — null when the host has no swap or it cannot be read |
memory.inference | usedMb, limitMb, usagePercent, source (nullable) — the inference container against the limit it is stopped at (RAM plus swap); the whole object is null when the container has no limit or it cannot be read |
storage | totalGb, usedGb, freeGb, usagePercent, databaseMb — each nullable; the whole object is null when the disk cannot be read |
The memoryKind, swap and inference fields are additive: the inference
engine reports them, so against an older engine they arrive as null and the
dashboard shows them as not reported. storage is measured by the backend
itself.
Each entry of NewPredictionBatch.predictions is one prediction record:
| Field | Meaning |
|---|---|
requestId | Identifier of the inference request that produced the record |
predictions | Model output, one value per output channel |
confidenceScores | One confidence value per output channel |
modelId | Model that produced the prediction |
inferenceTimeMs | Model execution time |
timestamp, windowStartTimestamp, windowEndTimestamp, emittedAt | See the next section |
Prediction timestamp semantics
Section titled “Prediction timestamp semantics”A prediction describes a past window of input. All timestamps are Unix epoch milliseconds from one clock — the backend stamps each sample once at read time, and that value is echoed through the pipeline, never regenerated:
| Field | Meaning |
|---|---|
windowStartTimestamp | Oldest input sample in the window |
windowEndTimestamp | Newest input sample — the “as-of” time the prediction is valid for |
timestamp | Mirrors windowEndTimestamp (compatibility) |
emittedAt | Backend wall-clock at emission — emittedAt − windowEndTimestamp ≈ end-to-end latency |
On the dashboard the prediction trace visibly trails the sensor trace by the real pipeline latency. That gap is an intended operational signal, not a rendering defect.
Delivery guarantees
Section titled “Delivery guarantees”Slow consumers never throttle inference: dashboard broadcasts are
fire-and-forget, and events to a client that can’t keep up are dropped and
counted (/api/inference/backpressure). Notification-worthy events are
deduplicated and rate-limited to keep the bell useful — see
Notifications.