Multi-Renderer (Browser / CLI / WinUI3)
Consystence renders its operator and engineering surfaces on the server. Clients are thin: the server holds state in Orleans grains, builds a renderer-agnostic description of each surface, and pushes it to whichever client is attached. That description is the semantic UI tree, carried by a JSON wire format with two shipped consumers — the csy command-line client (CLI) and the browser’s wire renderer — and a native Windows client (WinUI3) specified against the same wire format as roadmap. The wire renderer is the default browser surface: the browser and the CLI render the same tree. The browser’s original transport — server-rendered HTML plus DOM patches over SignalR — is legacy: no longer served by default, and being deleted.
What stays invariant is the wire-format protocol shape, not any one client. Renderers version in lockstep with the server, and a new renderer consumes the same protocol rather than growing a private one. This page describes the tree, the wire format, which renderers consume it, and how the browser routes into it. For where this sits in the wider system, see the architecture overview.
flowchart TB SG["Scene graph<br/>authoritative server state"] UT["Semantic UI tree<br/>renderer-agnostic primitives"] WF["Wire format<br/>L1 render · L2 patch · L3 action · viewInvalidated"] SG --> H["Server-side HTML renderer<br/>HTML/SVG → DOM patches<br/>legacy (being deleted)"] SG --> UT UT --> WF WF --> B["Browser wire renderer<br/>default browser surface"] WF --> C["CLI renderer<br/>text → terminal<br/>csy ui"] WF --> W["WinUI3 renderer<br/>XAML/Win2D → Visual tree<br/>(roadmap)"]
The semantic UI tree
Section titled “The semantic UI tree”The scene graph — sites, devices, tags, states, alarms, and commands — remains the authoritative server-side domain model. Between it and any renderer sits the semantic UI tree: a tree of primitives defined by what they express, not how they paint. Each wire-format renderer consumes the same tree shape and produces target-specific output — terminal text in the CLI, DOM elements in the browser wire renderer, a WinUI3 Visual tree on the roadmap. The legacy browser branch instead renders HTML from the scene graph on the server.
The primitive set is deliberately small and closed:
| Primitive | Expresses |
|---|---|
View | The top-level container for a rendered surface |
Card | Grouped content with an optional title and body |
Stack | Vertical or horizontal child layout, spaced by a gap token |
StateBadge | A compound state display — { state, color } |
Gauge | A scalar numeric value with optional unit and range |
Button | An action trigger carrying an enabled flag and an optional confirmToken — a type-to-confirm gate the renderer enforces before invoking a destructive action |
Table | Tabular rows of typed cells, with row-scoped actions |
SceneRef | A reference to a process scene (SVG) — identity, viewport, and lifecycle, never pixels; the scene’s graphics ride a separate view-bound channel |
TrendChart | A historian trend — the initial render carries a bounded backfilled series plus axis/range metadata; the live tail arrives as patches |
Chat | An assistant chat surface — the initial render carries the bounded transcript, streamed assistant and agent progress arrive as patches, and sending a message is a declared action |
Form and List are specified as candidates; their final shapes lock at their first real surface. Adding a primitive is a protocol-version event — renderers version in lockstep with the server — not a tolerant extension, so an unknown node type fails the read rather than being silently ignored. The protocol is at v1.4 today; earlier revisions added quality/staleness descriptors on value nodes (v1.1), SceneRef and TrendChart (v1.2), the Chat surface carrying assistant chat and streamed agent progress (v1.3), and type-to-confirm on Button (v1.4).
Layout uses tokens, never raw pixels. gap is drawn from a closed enum (xs, sm, md, lg, xl) and direction from { vertical, horizontal }. Each renderer maps the token at render time — the CLI renderer to terminal whitespace, the browser wire renderer to its component stylesheet, and a WinUI3 consumer to XAML spacing. Keeping pixels out of the wire format is what keeps the tree renderer-agnostic.
Bindings are property paths only — for example dev_BVSWSZ1Q.tag.Speed, dev_BVSWSZ1Q.state, or dev_BVSWSZ1Q.alarm.lowFlow.active. There is no arithmetic, comparison, unit conversion, or computed expression in the binding language yet; alarm-condition expressions are a deferred extension. Entity references in the wire format are canonical identifiers (for example dev_BVSWSZ1Q), not the operator-facing AssetNumber (for example PP-01) — the human-readable asset number is a presentation-layer concern applied at render time.
One wire format, four message layers
Section titled “One wire format, four message layers”A single JSON protocol carries the tree to every wire-format renderer — the CLI and the browser wire renderer today, WinUI3 when it lands. It has three layers plus a distinct invalidation event.
L1 — initial render. On subscribing to a surface, a renderer receives the full semantic tree for a View. The View carries a template (the surface or bundle identifier) and an entity (the canonical ID it is bound to); its children are nested primitives. Two stable handles thread through the tree: a pointId anchors later L2 patches, and an actionId anchors L3 invocations. Both remain stable across the L1 → L2 lifetime of a View.
{ "type": "View", "id": "v_pump001_faceplate", "template": "pump-faceplate", "entity": "dev_BVSWSZ1Q", "children": [ { "type": "Card", "children": [] } ]}Containers always carry "children": [] and actions always carry "data": {} explicitly — omission is a malformed document, which keeps the non-null contract honest on the consuming side.
L2 — patch. Fine-grained updates to an existing View, addressed by stable pointId. The op set is closed at three operations:
| Op | Effect |
|---|---|
replace | Update the value at a pointId |
add | Insert into a collection at a pointId (table rows, list items, alarm banners, trend samples) |
remove | Delete from a collection at a pointId |
L3 — action. Client-initiated invocations addressed by actionId. The data field is a closed schema per action, declared in the view’s server-side action registration and validated server-side — missing and extra keys both reject. Shipped actions include admin-surface operations such as revoke on the API-keys view (data: { "keyId": "…" }) and device commands such as start, stop, or a parameterised set-speed (data: { "speed": 75.0 }) with schemas derived from the device type — device commands over the wire pass through the same fail-closed authorisation and audit gates as every other command path through the platform.
viewInvalidated — structural re-render. When a surface’s structure (not just its values) changes, the server emits a distinct viewInvalidated event — not a flag piggybacked on an L3 response. The renderer responds by re-requesting L1 for the affected viewId. Four reasons are defined:
| Reason | Trigger |
|---|---|
structural-change-from-action | An action result implies a structural re-render (for example a commissioning gate adds or removes sections) |
entity-shape-change | The bound entity changed type or substantive shape |
template-version-change | The surface’s template bundle was bumped |
override-applied | A template override was applied and the view must re-materialise |
Only structural-change-from-action has a live producer today; the other three reasons are defined in the protocol ahead of the template-bundle and override system that will emit them.
pointId is scoped per viewId, so the same point name in two different Views never collides — addressing is unambiguous by construction.
Element type drives value shape
Section titled “Element type drives value shape”A renderer decides how to interpret a value by the element type at a pointId, never by parsing the point’s name. A StateBadge value is the compound { state, color }; a Gauge value is a scalar wrapped in the quality envelope { "value": 50, "quality": … } (introduced in v1.1); a Button’s enabled is a scalar boolean. The optional quality descriptor names one of the five platform quality states (good, uncertain, cached, stale, bad) with an asOf timestamp where the state implies one; when omitted, the value is good. A stale or bad value renders as such in every renderer — never as live. Each renderer dispatches on the primitive type; the pointId is purely an addressing handle.
The renderers
Section titled “The renderers”The renderers share the scene graph at the top and the wire-format shape as a contract; they do not share a runtime substrate. Each owns its own transport and its own client.
Browser — the wire renderer, now the default
Section titled “Browser — the wire renderer, now the default”The browser’s default surface is the wire renderer; the original HTML path is legacy and being deleted:
- The wire renderer (default). A browser client that consumes the same L1 / L2 / L3 /
viewInvalidatedprotocol as the CLI — over SignalR on/hubs/ui-wire— and renders it with a bespoke component stylesheet keyed to the closed primitive vocabulary — no CSS utility framework, no SPA framework — under a strict Content-Security-Policy. The browser and thecsyCLI render the same server-resolved tree. - The legacy HTML path (being deleted). The server-side HTML renderer produces HTML/SVG, transported as DOM patches over SignalR (
/hubs/ui-page), which a thin TypeScript layer applies to the live DOM. Client-side events are delegated to the server throughdata-csy-*attributes on the rendered elements. It is no longer served by default, and the whole HTML branch is scheduled for deletion.
Browser routing
Section titled “Browser routing”/ and every extensionless navigation route serve the wire shell; the start view is overview. Old /wire/{route} URLs permanently redirect (301) to /{route}, so existing bookmarks keep working. Transitionally, GET /legacy sets a csy-shell=legacy cookie (7-day) and serves the legacy HTML shell at its native routes; GET /legacy/exit clears it.
CLI — the csy command-line client
Section titled “CLI — the csy command-line client”The CLI renderer is a full peer, not a test harness bolted on. csy ui consumes the L1 / L2 / viewInvalidated wire format directly and renders it to the terminal: View, Card, and Stack become text blocks, StateBadge a status indicator, Gauge and Button labelled scalars, and Table typed rows with row-scoped actions. Form and List terminal rendering follows the same first-scenario lock as the primitives themselves.
It runs in two modes:
- REPL — an interactive session (
goto,inspect,action,wait) that applies L2 patches to the current view over a persistent connection. - Script — a declarative YAML scenario that runs single-shot and deterministic, emitting structured output plus an exit code, which makes it the integration-test surface in place of browser click-throughs.
A working CLI consuming the wire format against a radically different output target is what demonstrates — rather than merely asserts — that the semantic tree is genuinely renderer-agnostic. CLI authentication rides the platform’s existing schemes (OIDC device-code / PKCE for interactive login, X-Api-Key / CONSYSTENCE_TOKEN for automation); see the CLI client and authentication.
WinUI3 — native Windows
Section titled “WinUI3 — native Windows”The WinUI3 renderer consumes the wire format and produces a native Visual tree (XAML with Win2D / Composition visuals), targeting Windows control rooms and engineer workstations where native fidelity and system integration (multi-monitor layout, notifications, file dialogs) matter. It is Windows-only by design; macOS, Linux, and mobile users use the browser client.
Sessions and renderer identity
Section titled “Sessions and renderer identity”Each connected client drives one server-side session object, keyed by session ID. It owns the session’s semantic view tree and is renderer-agnostic: one session is one client connection is one renderer, and the renderer is recorded as a session attribute, not as a separate session type per renderer. A CLI client and a browser client therefore attach to the same kind of session — only the attached renderer differs.
Routing events to the right View within a session is handled per-session: the session routes patches and invalidations to the right view. That routing state is per-session, kept off the global routing path. High-frequency value updates ride the value-coalescing path described under the scaling pipeline.
Where to go next
Section titled “Where to go next”- Architecture overview — the four-tier topology, Orleans grains, and where rendering sits.
- The CLI client — installing and driving
csy ui, the CLI renderer. - Scaling pipeline — the high-frequency value-coalescing path that tag-bound L2 patches ride.
- AI in the platform — how the advisory AI uses the same server-side surface.