Skip to content

Reference — Data Types

A console has no southbound register map, so this page is the browser↔console WebSocket protocol — the hard contract between the gateway and the UI. Every type here is defined in the shared package @edgecommons/edge-console-protocol; the types are the shared wire contract — the UI imports the TypeScript package, and the Rust gateway emits and parses matching JSON. For the console↔bus UNS side, see messaging-interface.md.

  • One WebSocket per browser app, at /ws on the gateway origin (ws:// or, behind a TLS terminator, wss://). The UI derives the URL from the page origin, overridable with VITE_CONSOLE_WS_URL.
  • Every frame in both directions is a JSON object carrying a protocolVersion integer.
  • PROTOCOL_VERSION = 7. The gateway validates every inbound frame through one pure parse_client_frame() — nothing lenient is accepted (unlike the config parsers). A version skew is a clean rejection, not a misparse.
  1. Client’s first frame must be hello ({ type, protocolVersion, resumeSeq? }).
  2. Server replies welcome (the connection’s resolved RBAC role), then — if available — settings (the console’s own policy), then a snapshot (or, on a valid resume, only the missed delta batch).
  3. Server then streams delta batches and, for any families the client subscribed, their frames, plus a periodic heartbeat.

An unsupported protocolVersion yields an error with code unsupported-protocol-version (the tab should reload — never a retry loop); a malformed frame yields error code malformed; either closes the connection.

type Fields Purpose
hello protocolVersion, resumeSeq? Mandatory first frame. resumeSeq = last applied delta seq (resume attempt).
get-config key Request a component’s latest retained cfg; also registers interest (later cfg pushed).
refresh-config device Fire the per-device republish-cfg broadcast. Fire-and-forget.
get-descriptor key Request a component’s descriptor manifest (custom panels + command verbs). Answered by one descriptor or descriptor-unavailable.
refresh-descriptor key Re-fetch the component’s descriptor manifest. Answered the same way.
subscribe-events limit? Ask for the rolling evt backlog (newest-first, optionally capped) then live event pushes.
unsubscribe-events Stop event pushes. Idempotent.
subscribe-metrics / unsubscribe-metrics Metric snapshot then metric pushes / stop.
subscribe-logs key, limit?, levels?, sinceId? Ask for one component’s retained log tail (newest-first), optionally capped/filtered, then live log pushes.
unsubscribe-logs key Stop log pushes for that component. Idempotent.
subscribe-signals / unsubscribe-signals mode? Data-plane signal snapshot then signal pushes / stop. mode is "full" (default) or "summary" — summary series omit points.
get-signal-points series: {key, instance, signal}[] Fetch the points rings for 1–200 named series (the summary-mode backfill). Answered by one signal-points frame.
subscribe-attributes / unsubscribe-attributes Runtime-attribute snapshot then attribute pushes / stop.
subscribe-alarms / unsubscribe-alarms Alarm snapshot then live alarms replace-frames / stop.
ack-alarm alarmId Toggle console-side acknowledgement of an alarm.
invoke-command requestId, key, verb, args? Invoke a UNS command verb on a component (the write path).

Interest for every family is per-connection — a view re-subscribes after a reconnect; the fresh snapshot/backlog self-heals the client store (no client-side resubscribe bookkeeping).

type Payload When
welcome role Right after a valid hello.
settings settings: ConsoleSettings After welcome (server-initiated).
snapshot snapshot: FleetSnapshot On connect without a resumable resumeSeq, or as the resume fallback.
delta deltas: FleetDelta[] Change batches, strictly increasing seq.
heartbeat at, busMsgsPerSec?, busRecentRates?, self? Periodic keep-alive + the console’s own bus rate/sparkline/self vitals.
config / config-unavailable key, cfg, receivedAt, sourceTimestamp? Reply to get-config + later pushes / no cfg held.
descriptor key, manifest, receivedAt Reply to get-descriptor/refresh-descriptor: the normalized component manifest (schema, component, commands, panels).
descriptor-unavailable key, code, reason The component returned no usable manifest, or its describe failed / was denied.
events / event events: ConsoleEvent[] / event: ConsoleEvent Backlog (newest-first) / one live arrival.
metrics / metric series: MetricSeriesSnapshot[] / updates: MetricSeriesUpdate[] Snapshot / live sample batches.
logs / log / logs-unavailable key, records, dropped? / key, records, dropped? / key, code, reason Component log tail snapshot / live record batch / unavailable notice.
signals / signal series: SignalSeriesSnapshot[] / updates: SignalSeriesUpdate[] Data-plane snapshot (full or summary per the subscribe mode) / live samples.
signal-points series: {key, instance, signal, points}[] The reply to get-signal-points: found series only, in request order.
attributes / attribute components: RuntimeAttributes[] / updates: RuntimeAttributes[] Runtime-attribute snapshot / live updates.
alarms snapshot: AlarmSnapshot The reply to subscribe-alarms and every later change (one replace-frame).
command-result requestId, key, verb, ok, result?, error?, elapsedMs The single answer to an invoke-command. Never closes the connection.
error code: WsErrorCode, message A rejected frame; the connection closes after.
  • Resume: offer resumeSeq. If a bounded recent-delta ring (default 1000) proves contiguous coverage, you get only the missed delta batch; otherwise a fresh snapshot. A resumeSeq ahead of the server, or an evicted range, always re-snapshots.
  • Backpressure: a client whose transport stays backpressured across several delta pushes is dropped-and-resnapshotted, never queued — it cannot stall other clients.
  • Value bodies do not ride deltas. A value-updated delta is a change notification; cached value bodies refresh via snapshots and the dedicated body families (config/events/metrics/signals).
interface ComponentKey { device: string; component: string; }

A component is one entity per (device, component) — the UNS instance token is not part of its identity. Its canonical string form is componentKeyId(key) = "${device}/${component}".

"FRESH" | "WARN" | "STALE" | "OFFLINE" | "STOPPED" | "UNREACHABLE" — see explanation → miss-detection for the transition rules. CadenceSource = "default" | "cfg" records where the expected interval came from.

Field Type Notes
instance string Source instance — a connection id for per-instance data/evt. Component-scoped messages (state/cfg) carry no instance on the wire; the console labels the absent instance main in its model.
cls ConsumerClass state|cfg|evt|metric|data|log.
channel string? /-joined channel tokens; absent for the leaf classes (state, cfg).
body unknown The envelope body (already lib-redacted for cfg).
tags object? Envelope tags, verbatim. _-prefixed keys are system-reserved (never business context).
receivedAt number Console receipt time (ms epoch) — the authoritative LKV timestamp.
sourceTimestamp string? The publisher’s header.timestamp claim (display only — never drives staleness).

Receipt times (receivedAt everywhere, and the point at in metric/signal series) are stamped on the gateway’s own monotonic timeline — non-decreasing per gateway, even when the host wall clock steps backward.

interface InstanceStatus { instance: string; connected: boolean; state?: string; detail?: string; }

A multi-connection component (OPC UA servers, Modbus slaves, file-replicator source dirs) reports each configured instance’s status in its state.instances[], rather than minting a UNS instance per connection.

state carries the instance’s condition in the shared vocabulary, from the same state model that answers the component’s sb/status:

state Meaning Console rendering
CONNECTING Establishing the southbound session. connecting badge, blue.
ONLINE Connected and polling/subscribed. online badge, green.
BACKOFF Down, retrying on the reconnect backoff. backoff badge, red.
PAUSED Deliberately stopped by an operator. paused badge, gray, marked expected quiet.

PAUSED is expected quiet: the console excludes a paused instance from the Health tab’s connection ratio and reports it separately, so a deliberate pause never reads as a connection fault. A component that reports no state, or a token outside the table, is rendered from connected alone.

interface FleetSnapshot { seq: number; takenAt: number; devices: DeviceSnapshot[]; }
interface DeviceSnapshot { device: string; unreachable: boolean; unreachableSince?: number; components: ComponentSnapshot[]; }

ComponentSnapshot fields:

Field Type Notes
key ComponentKey
path string identity.path (full hierarchy join) — the tree/grouping key.
hier {level,value}[] The full hierarchy, for N-level rollups.
liveness Liveness Effective (device UNREACHABLE overlays the ladder).
status string? Last reported state.status (RUNNING/STOPPED).
uptimeSecs number? Last reported uptime (restart = a decrease).
instances InstanceStatus[]? Per-instance status, when the state carried it.
lastStateAt number? Receipt time of the last state keepalive.
expectedIntervalSecs number The interval driving miss-detection.
cadenceSource CadenceSource default or cfg.
restarts number Observed uptime resets.
values CachedValue[] Every cached last-known value.
droppedChannels number Distinct channels dropped by the per-component cap.

Every delta carries a monotonic seq and a model-clock at. The variants:

type Extra fields Meaning
device-discovered device First sight of a device.
component-discovered key, path, hier First sight of a component (carries hier for dynamic grouping without a snapshot).
instances-changed key, instances The full new per-instance status set (replace wholesale).
value-updated key, instance, cls, channel? A cached value changed (notification only — no body).
liveness-changed key, from, to A ladder transition.
component-restarted key, previousUptimeSecs, uptimeSecs An uptime reset.
device-reachability-changed device, unreachable, componentCount Whole-device UNREACHABLE contain/release (componentCount = the “+N suppressed” rollup).

Body families (the values the liveness stream deliberately omits)

Section titled “Body families (the values the liveness stream deliberately omits)”

The evt envelope body plus the console’s attribution. Key fields: id (monotonic, arrival order), key, instance, severity? (verbatim token), type, channel?, body, tags?, receivedAt, sourceTimestamp?. The evt/{severity}/{type} channel is split leniently by splitEventChannel(), and raw severity tokens are classified into critical | error | warning | info | debug by classifyEventSeverity() (unknown ⇒ rendered neutrally — the class is open, never rejected).

Metrics — MetricSeriesSnapshot / MetricSeriesUpdate

Section titled “Metrics — MetricSeriesSnapshot / MetricSeriesUpdate”

One series per (component, instance, metric, measure): latest, receivedAt, and a bounded ascending points: {at, value}[] (default DEFAULT_METRIC_SERIES_POINTS = 60). Bodies fold leniently — the library’s EMF shape (top-level numeric measures; _aws skipped) and bare numbers (measure "value") alike.

Logs — ConsoleLogRecord / ConsoleLogSnapshot

Section titled “Logs — ConsoleLogRecord / ConsoleLogSnapshot”

One retained record per structured UNS log/{level} envelope. The console only accepts attributable EdgeCommons envelopes with body.schema === "edgecommons.log.v1"; malformed or over-retained records are dropped and counted.

type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
interface ConsoleLogRecord {
id: number;
key: ComponentKey;
instance: string;
level: LogLevel;
logger: string;
message: string;
receivedAt: number;
sourceTimestamp?: string;
sequence?: number;
thread?: string;
fields?: Record<string, unknown>;
error?: { type?: string; message?: string; stack?: string };
truncated?: boolean;
channel?: string;
tags?: Record<string, unknown>;
}

subscribe-logs scopes to one component key. A logs frame returns the retained component tail newest-first; later log frames carry one or more fresh records. logs-unavailable is returned when the gateway has no log source wired or policy forbids the subscription.

Signals (data plane) — SignalSeriesSnapshot / SignalSeriesUpdate

Section titled “Signals (data plane) — SignalSeriesSnapshot / SignalSeriesUpdate”

One series per (component, instance, signal):

interface SignalSeriesSnapshot {
key: ComponentKey;
instance: string;
signal: string; // the data channel
latest: unknown; // the newest sample's value, verbatim
quality?: string; // the newest sample's normalized quality (GOOD | BAD | UNCERTAIN)
receivedAt: number;
sourceTimestamp?: string; // folded display fallback: sourceTs ?? serverTs ?? envelope timestamp
sourceTs?: string; // the newest sample's measured/device timestamp, verbatim
serverTs?: string; // the newest sample's protocol-server refresh timestamp, verbatim
name?: string; // signal.name — the human label
signalId?: string; // signal.id — the canonical stable id
address?: unknown; // signal.address — protocol-native, opaque
adapter?: string; // device.adapter
endpoint?: string; // device.endpoint
qualityRaw?: string; // the newest sample's native status code
publishedTs?: string; // the latest publish's envelope header timestamp, verbatim
points?: { at: number; value: unknown; quality?: string; sourceTs?: string; serverTs?: string }[];
}

points is a bounded ring (60). Each point’s at is the console receipt time — one consistent time base for trend rendering. sourceTs (measured/device time) and serverTs (protocol-server refresh time) ride each point verbatim when the publisher provided them — a publisher that only stamps serverTs yields points with serverTs and no sourceTs. The series-level sourceTs/serverTs describe the newest sample and clear when it carries neither. publishedTs is the adapter’s publish-time envelope timestamp — with the verbatim pair it lets a client compute publish lag (publishedTs − (sourceTs ?? serverTs)) entirely in the adapter’s clock domain; a series with neither sample timestamp has no computable lag. sourceTimestamp is the folded display fallback.

subscribe-signals accepts mode: "full" | "summary" (default full). A full snapshot carries every series’ points ring; a summary snapshot serves the same series objects with the points key omitted. A summary-mode client backfills trends on demand with get-signal-points (1–200 {key, instance, signal} selectors); the signal-points reply carries the requested series’ rings — found series only, in request order. Live signal pushes stream points regardless of mode. Servers that support summary mode advertise it via settings.capabilities.signalsSummary.

A data body is split into samples by shape:

  • Canonical SouthboundSignalUpdate (the adapter contract): a body carrying a samples array yields one point per element that is an object with a value key — value verbatim (including null), quality, qualityRaw, and per-sample timestamps all optional. Invalid elements are skipped; a batch with no valid samples changes nothing. The last valid sample becomes latest / quality / qualityRaw. The series metadata fields (name, signalId, address, adapter, endpoint) are captured from the body’s signal{ id, name, address } and device{ adapter, endpoint } blocks, latest-wins.
  • Plain object (no samples): one sample — the value field if present, otherwise the whole body as the value, with quality when present.
  • Bare value (non-object body): one sample, the body itself, no quality.

A live signal frame carries one update entry per ingested sample: { key, instance, signal, point, sourceTimestamp?, publishedTs?, name?, signalId? }sourceTimestamp is the per-sample folded fallback (sourceTs ?? serverTs ?? envelope timestamp), publishedTs is the envelope header timestamp verbatim (omitted when the publisher sent none), and name/signalId appear only on a batch that sets or changes the series label, so a client that subscribed after the snapshot can still label the series.

A latest-wins projection over the metric class the Overview columns and Component-Detail Health tab render: cpuPercent?, memoryMb?, threads?, fds? (the sys.* measures), connectionState?, readErrors?, writeErrors? (adapter southbound_health), platform? (from tags.platform when a component advertises it), and cpuSeries? / memorySeries? (30-point drop-oldest sparkline rings fed by each sys heartbeat’s cpu_usage / memory_usage). All optional — a component that never emitted a measure omits it (the UI shows “—”).

Alarms — ConsoleAlarm / AlarmCounts / AlarmSnapshot

Section titled “Alarms — ConsoleAlarm / AlarmCounts / AlarmSnapshot”

Console-derived from the evt severity stream: a critical/error/warning event raises an alarm keyed by (component, type); a normal-severity follow-up on the same key clears it (into history). acked is console-side. A device going UNREACHABLE contains (suppresses from active counts, does not clear) its components’ alarms. AlarmCounts = { critical, warning, active, contained, acked }.

  • ConsoleSelf (on the heartbeat): the console’s own device/component, resolved platform?/transport?/broker?, and process cpuPercent?/memoryMb?/uptimeSecs — the Overview “Edge node” and “Edge bus” tiles.
  • ConsoleSettings (the settings frame): the console’s own effective policy, read-only — rbac (roles + allow/deny + default), connection (identity + WS listener + servesUi), staleness, commands (incl. the bridge TTL ceiling), retention (all the cache caps), and capabilities (feature flags the client detects on — signalsSummary: true marks summary-mode signal subscribe + get-signal-points support). This is a curated projection of the parsed config, never the raw document.

invoke-command → exactly one command-result, correlated by the client-chosen requestId. On success ok: true + result (the verb’s result object, e.g. ping’s {status, uptimeSecs}); on failure ok: false + error: CommandError. CommandError.code is an opaque string — either the component’s own code passed through verbatim (UNKNOWN_VERB/HANDLER_ERROR/RELOAD_FAILED/NO_CONFIG) or a console-synthesized ConsoleCommandErrorCode:

Code Meaning
FORBIDDEN RBAC denied the verb — never hit the bus (only this code drives a distinct UI affordance).
TIMEOUT No reply within the per-verb deadline (≤ the bridge reply-map TTL).
REQUEST_FAILED The request could not be issued/awaited (transport/publish error).
INVALID_TARGET (key, verb) did not form a valid UNS topic.
MALFORMED_REPLY A reply arrived whose body was not the {ok, result|error} shape.
UNAVAILABLE The gateway has no command seam wired.

The universal built-in verbs every component answers: BUILTIN_COMMAND_VERBS = ["ping", "describe", "reload-config", "get-configuration"]. A component’s custom verbs are discovered through describe.

Command capabilities (describe.commands[])

Section titled “Command capabilities (describe.commands[])”

Each entry advertises one verb the console may invoke:

Field Type Meaning
verb string The exact cmd verb remainder (sb/browse). The console never invents aliases.
title string? Display label.
scope "component" | "instance" | "both"? The verb’s addressing (below).
kind "read" | "write" | "diagnostic" | "control"? What the verb does.
builtIn boolean? Whether the library, not the component, answers it.
danger "none" | "physical-write"? Drives the confirmation affordance.
availability {state, reason?}? disabled/unsupported disables every bound widget and shows the reason.

scope drives the Panel tab’s addressing UI:

  • instance — the instance selector mounts, and every invocation of the verb names the selected instance.
  • component — no selector involvement, and no invocation ever carries instance. The component rejects an instance-addressed delivery of a component-scoped verb.
  • both — the selector mounts and offers an explicit Whole component choice, which sends no instance at all. It is offered when every instance-addressable widget in the view declares both.
  • absent — the console falls back to the panel widgets’ own scope markers.

WsErrorCode = "malformed" | "unsupported-protocol-version" — both close the connection after the error frame.