Skip to content

GUI DSL

The GUI DSL is the layout and component system that assembles operator screens. Pages are rendered entirely on the server as HTML with Tailwind CSS. The browser runs a thin TypeScript client that receives differential updates over SignalR and patches the DOM.

Server-rendered architecture

graph LR
    CT[Component Tree] --> PR[PageRenderer]
    PR --> HTML[HTML + Tailwind]
    HTML -->|SignalR| Client[Browser — TypeScript Shell]
    Client -->|User Events| PR

There is no client-side framework (no React, no Blazor). The server owns the component tree, renders full HTML, and pushes incremental updates. This keeps the client simple and eliminates client-side state synchronization.

IComponentServer

Every component type implements the IComponentServer interface:

public interface IComponentServer
{
    string Render(ComponentNode node, RenderContext context);
    Task HandleEventAsync(string eventName, JsonElement payload);
}
  • Render returns an HTML string for the component and its children.
  • HandleEventAsync processes user interactions (clicks, input changes) forwarded from the client.

Component tree

The page structure is defined as a tree of ComponentNode records:

[GenerateSerializer]
public record ComponentNode
{
    [Id(0)] public string Type { get; init; } = "";
    [Id(1)] public Dictionary<string, object> Props { get; init; } = new();
    [Id(2)] public List<ComponentNode> Children { get; init; } = [];
}

A typical page tree:

Page
├── Grid (columns: 3)
│   ├── MetricCard (tag: "SumpLevel", unit: "%")
│   ├── MetricCard (tag: "Pump1Speed", unit: "RPM")
│   └── MetricCard (tag: "Pump2Speed", unit: "RPM")
├── SplitView
│   ├── SceneViewport (scene: "pump-station")
│   └── AlarmList (filter: "active")
└── TabGroup
    ├── Tab (label: "Trends")
    │   └── TrendChart (tags: [...])
    └── Tab (label: "Events")
        └── EventLog (filter: "last-24h")

Component categories

Layout

Component Purpose
Page Top-level container with title, breadcrumbs, navigation
Grid CSS grid layout with configurable columns and gap
SplitView Resizable horizontal or vertical split
TabGroup Tabbed content panels

SCADA

Component Purpose
AlarmList Real-time alarm table with acknowledge/shelve actions
EventLog Chronological feed of state changes, commands, and alerts
SceneViewport Bridge to the scene graph — renders SVG process graphics
TrendChart Historical and real-time tag value charts

UI primitives

Component Purpose
Card Container with header, body, optional actions
Gauge Circular or linear gauge bound to a tag value
MetricCard Single tag value with label, unit, and sparkline
Button Action button for operator commands
ChatPanel AI assistant panel for querying equipment history

PageRenderer

The PageRenderer service composes the full component tree into an HTML page:

  1. Walk the tree depth-first.
  2. Call Render() on each component's IComponentServer implementation.
  3. Concatenate the HTML fragments into a complete page with Tailwind utility classes.
  4. Send the full page on initial load, then differential updates on state changes.

SceneViewport bridge

The SceneViewport component bridges the GUI DSL and the scene graph. The GUI DSL handles everything around the viewport — panels, alarm lists, navigation — while the scene graph handles the process visualisation inside it.

┌─────────────────────────────────────────┐
│  Page (GUI DSL)                         │
│  ┌─────────────────┬───────────────────┐│
│  │ SceneViewport   │ AlarmList         ││
│  │ ┌─────────────┐ │ (GUI DSL)        ││
│  │ │ Scene Graph  │ │                  ││
│  │ │ (SVG)       │ │                  ││
│  │ └─────────────┘ │                  ││
│  └─────────────────┴───────────────────┘│
│  ┌─────────────────────────────────────┐│
│  │ TrendChart (GUI DSL)               ││
│  └─────────────────────────────────────┘│
└─────────────────────────────────────────┘

Differential updates

When a tag value changes, the server:

  1. Identifies which components in the tree depend on that tag.
  2. Re-renders only those components.
  3. Sends the HTML fragment and a DOM target selector over SignalR.
  4. The TypeScript client patches the specific DOM node.

This avoids full-page re-renders and keeps update latency low.