Skip to content
GitHub

Dashboards

A dashboard is a page of cards somebody built. Gauges, trends, lists of entities, a button that starts the pump, a floor plan with the temperatures sitting where the zones are. Build as many as the plant needs, give each to the roles that need it or to the one screen it lives on, and the old Board is just one of them.

It is in HotLoop IoT, Edge and Gateway. The Edge Relay has no UI and no dashboards. The Edge HMI is built out of this page and nothing else, which is the reason the rest of it is the way it is.

A card commands nothing. The first design question for any page of buttons is whether the page is a second way to run the plant. It isn’t. A card names a tag, an entity, a script or a recipe, and the browser does what every other screen does with them. A write button posts to POST /api/write. An entity’s controls call POST /api/services. A script button opens the Scripts screen’s run prompt and a recipe card opens the Recipes screen’s apply drawer. Every one of those has its own permission and meets the write gate, so a dashboard can’t be a way round either. What dashboards.write guards is who decides what the buttons are, which is an engineer’s call and is admin-only for the same reason writing a script is.

The consequence: a dashboard saved by an admin does nothing until an operator presses something, and what the press does is exactly what pressing it on any other screen does, down to the audit row.

A layout is a flow, not coordinates. Cards go in reading order. Each says how wide it is out of twelve columns and how tall in rows, and knows nothing about where it lands. Twelve columns on a panel PC, six on a laptop with another window beside it, two on a tablet, one on a phone. A layout of pixel positions is right on exactly the screen it was drawn on, and the Edge HMI has to be right on whichever screen the machine has.

The consequence: nobody ever builds the phone version. There isn’t one.

Saves name the version. Two people editing one dashboard is a thing that happens at shift change. A save carries the version it was opened at, and a stale one is refused with a 409 that says who got there first. Without that the second person to press Save deletes the first one’s afternoon without either of them seeing it.

{
"id": "line-2",
"name": "Line 2",
"description": "The extruder, for the floor.",
"kind": "cards",
"roles": ["operator", "admin"],
"homeFor": ["operator"],
"screens": ["line-2-panel"],
"position": 1,
"version": 3,
"cards": [
{"id": "temp", "type": "gauge", "title": "Zone 1", "w": 3, "h": 3,
"config": {"tag": "press-01/Zone1_Temp"}},
{"id": "go", "type": "button", "w": 3, "h": 1,
"config": {"tag": "line-02/Speed", "value": 250, "label": "Run at 250", "confirm": true}}
]
}
  • id is lowercase letters, digits, hyphens and underscores, starting with a letter. It is in the address and never changes. wall is the wall board’s.
  • kind is cards, or wall for the wall board.
  • roles are the roles offered the dashboard. Empty is every role. Whoever holds dashboards.write sees every dashboard whatever it says, because they have to be able to fix the one nobody else can open. A role that isn’t offered one gets a 404 for it by name, so the list of what exists isn’t something a viewer can learn by asking.
  • homeFor are the roles it opens for when they come in with no address asking for anything. A role has at most one; the dashboard has to be offered to the roles it is home for.
  • screens are the physical screens it is for. A screen name belongs to one dashboard.
  • position orders the switcher. New ones go last.
  • cards are in reading order, up to 60 per dashboard, 2 MB for all of them.
  • version goes up by one on every save.

A card is id (unique in its dashboard, made by the page), type, an optional title, w (1 to 12) and h (1 to 8), and a config that depends on the type. Leave w and h out and the type’s default is used. A config is checked strictly: a key the type doesn’t have is an error, and what is stored has every default filled in. A card that names a tag, entity, script or recipe that doesn’t exist is refused when the dashboard is saved. A button on a tag that isn’t marked writable, or a button whose number is outside the tag’s range, is refused too: the gate would refuse every press, and the person building it should hear that and not the person pressing it.

Type What it is Config Default size
status Devices up, tags reporting, alarms, historian, uptime: the Board’s strip none 12 x 1
gauge One tag as the Board’s dial, with its range and its alarm limits marked tag 3 x 3
value One tag as a big number, colored by its alarm state tag, decimals 3 x 2
trend Up to six tags over 15 minutes to a week tags, minutes, labels 6 x 3
entities The entities you pick, as their own tiles with their own controls entities (up to 30) 6 x 3
alarms The alarm list, with Acknowledge for whoever may outstanding, limit, devices, equipment 6 x 3
button Writes one value to one tag, after asking tag, value, label, confirm 3 x 1
script Runs a script through the Scripts screen’s prompt script, label 3 x 1
recipe Picks one of up to eight recipes and opens its apply drawer recipes 4 x 2
markdown Text: headings, lists, bold, italic, code, http(s) links text 4 x 2
camera A snapshot that refreshes, or a motion JPEG stream url, stream, refreshSec 6 x 4
map A floor plan with live tags placed on it image, markers 6 x 5

Things worth knowing about the ones that aren’t obvious:

  • button sends value as the type it was saved as. A number is a number, true is a boolean, a string is text. 1 and 1.0 are different writes on a PLC and the gate decides by the JSON type, so the card doesn’t turn one into the other. confirm is on unless it’s switched off, and then it asks “this commands real equipment” with the current value and a reason that goes in the audit trail (dashboard <id> / <card> if left empty). Switching it off is for a panel where holding the button is the point; it is an engineer’s decision and the Settings screen’s “Confirm before writing” overrides it back on.

  • alarms lists the whole plant’s alarms unless you point it somewhere. devices is a list of device ids. equipment is a list of equipment node ids from GET /api/equipment, any level (a site, an area, a line, a cell), and a node stands for every device placed at or under it, so an area’s page can show the area’s alarms and nobody else’s. With both, an alarm is listed when it is on either one. Leave them out, or empty, and the card is the whole plant, which is what every card saved before these fields did. The limit counts what is left after the filter, and outstanding still drops what is back to normal and acknowledged. A card limited to a place with no alarms says “here” so nobody reads it as a quiet plant, and a card whose equipment has since been deleted says that instead of showing an empty list. Equipment is stored as the node’s id, so renaming or moving a node doesn’t break the card. The dashboard is refused at save if a device or a node isn’t there. In the builder you type the path (Plant 2 / Scrubbers / Kettle 3) and it keeps the id. This card reads the tree, so a role needs equipment.read (operators and viewers have it).

    {
    "id": "kettle-alarms", "type": "alarms", "title": "Scrubber alarms", "w": 6, "h": 3,
    "config": { "outstanding": true, "limit": 10, "equipment": [14], "devices": ["oven-1"] }
    }
  • trend draws the window you asked for, ending now, and not just the span the readings happened to cover: minutes: 480 is an axis of the last eight hours with a tick on each round hour, in the time zone of whoever is looking. A tag that hasn’t moved is a flat line at its last good reading, because that is what a stopped mill or a closed valve looks like, and the historian only keeps a reading when a value moves past its deadband, so there may be one reading in the window, or none. Nothing on the line is made up. What the tag held when the window began is its last good reading before it (GET /api/history says so with carry=true), and the line is that value from the start of the window to the first reading inside it. After the last reading it is held on to the last time the tag was heard from, never past now. There is no line before the first reading anybody has, and a tag whose last reading before the window was bad has no carry at all, because what it held is not known. A stretch of bad readings is a gap, the same as it always was. One reading on its own is a line to now, or a dot if there is nowhere to draw it to.

    The legend names each line by the tag’s own name, the same name the number card shows, without the device in front. Two lines with the same name from different devices keep their full ids so you can tell them apart. labels is an optional object from a tag in tags to the text you want instead, 1 to 80 characters, and a label for a tag that isn’t plotted is refused. The hover tooltip always has the full id next to the label.

    {
    "id": "mills", "type": "trend", "title": "Mill temperatures", "w": 6, "h": 3,
    "config": {
    "tags": ["plant-5-temperatures/Large Sand Mill Temperature", "plant-5-temperatures/Small Sand Mill Temperature"],
    "minutes": 480,
    "labels": { "plant-5-temperatures/Large Sand Mill Temperature": "Large mill" }
    }
    }
  • camera loads the address in the browser, not on the server. A card’s config is readable by every role that opens the dashboard, so an address with a login or a token in it is refused when the dashboard is saved. Use an address on the plant network that needs none, or a proxy that adds the login. A still is fetched again every refreshSec seconds with a _hl query parameter so the browser doesn’t show the first one forever.

  • map is a picture and markers. The picture is an http(s) address or one uploaded into the card (png, jpeg, webp or gif, up to 500 KB, stored in the card). Markers are tags, placed in percent of the picture’s width and height so they stay where they were put at any size. This is a floor plan, not a street map: a tile map needs a tile server on the internet, and a plant’s panel PC usually can’t reach one.

  • markdown is built into DOM nodes and never into HTML, so nothing typed into it can become markup or a script.

  • gauge, value, map show “not on this gateway any more” if their tag is deleted after the dashboard was saved, instead of a dial that never moves.

The wall board is dashboard wall, kind wall. It is always there, it can’t be deleted, and it has no cards: its gauges are the tags pinned on the Board screen, kept in the board’s own setting where they always were. GET and PUT /api/board, the #board address, the pin picker and the drag to arrange are untouched.

What it gets from being a dashboard is the switcher (it is the first pill on every dashboard and the Board has the bar too) and the assignments: it can be offered to some roles, be a role’s home, and be a physical screen’s. A TV on the wall opens /?screen=wall-tv and gets the Board with the header and tabs gone.

Per role. roles decides who is offered a dashboard. homeFor decides where a role lands. An operator whose home is line-2 opens HotLoop on Line 2. With a URL fragment in the address (#alarms) the page opens where the address says.

Per screen. A screen is a physical display. Open HotLoop at /?screen=<name> and it shows the dashboard whose screens has that name with no header, no tabs and bigger touch targets, because nobody walking past a panel PC has a use for a way out of the page. A name that no dashboard has says so instead of showing something else. The panel signs in like anyone: give it an operator or viewer account that stays signed in, and keep the account’s role to what the panel is for.

This is what the Edge HMI is: a dashboard with screens set, on a machine that opens that address.

Whoever holds dashboards.write (admins) sees Edit on a dashboard and New dashboard in the bar. While editing:

  • cards are inert, so grabbing a button to move it can’t press it;
  • drag a card onto another to put it there, or use the arrows on its toolbar, which are what a phone has, because a touchscreen doesn’t start a drag;
  • drag the corner to resize: the width snaps to a column of twelve, the height to a row;
  • the gear edits the card, the copy button duplicates it, the cross deletes it;
  • Settings is the name, who it is offered to, who it opens first for, its physical screens and its place in the switcher.

Nothing is saved until Save. Discard throws the changes away. A 409 on save means somebody saved first: the page offers to load theirs.

Width Columns A card of w takes
Over 1100 px 12 w
700 to 1100 px 6 half of w, rounded up
480 to 700 px 2 one, or both when w is over 4
Under 480 px 1 all of it

Rows are 76 px with a 16 px gap on the wide layouts. On a phone a card is as tall as its content with a floor from its rows, so nothing is cut off. The switcher scrolls sideways inside itself and the page never does.

Permission Viewer Operator Admin
dashboards.read yes yes yes
dashboards.write no no yes

Reading is every role’s, filtered by roles. Writing is the admin’s. What a card can do when pressed is the pressing person’s own permissions: a viewer sees a write button disabled, and the server would refuse it if they got past that.

Method Route Permission
GET /api/dashboards dashboards.read
POST /api/dashboards dashboards.write
GET /api/dashboards/{id} dashboards.read
PUT /api/dashboards/{id} dashboards.write
DELETE /api/dashboards/{id} dashboards.write

The reference with bodies and errors is in API.md. Every create, save and delete is on the record as a config event on subject dashboard:<id>, so the Logbook says who changed what a panel shows.

The table is dashboards, added in migration 0023 for PostgreSQL, TimescaleDB and SQLite. It only adds (MIGRATIONS.md). The wall board’s row is made by the application when it is first asked for, not by the migration, so a database restored from a backup taken before dashboards has it again.

  • Pixel-exact layouts. On purpose, above. If a machine’s HMI really needs a control at a fixed spot, that is a screen built for that machine, and it is not what this is.
  • Per-card visibility. A dashboard is offered to roles; a card isn’t. Two audiences with different cards are two dashboards.
  • A tile map. See the map card.
  • Holding a button. A button writes once when pressed. A momentary control that writes on press and again on release is a different card and isn’t built.
  • Dashboards in the Edge Relay. It has no UI.