app/rcc/ai/skills/workspace_design.md
dashboard_layout covers the API: slugs, enums, min/max pairs, the
addWidget pre-flight. This skill covers the design judgment that
comes before you call addWidget. When the user says "organize my
dashboard" or "build an overview", they're asking for a curated layout,
not a dump of every group as a tile.
The dashboard exists to answer one question fast: is the system operating normally right now? Everything else is secondary.
A workspace holds 5-9 tiles before the user starts tab-hopping instead of reading. Miller's Law: short-term recall holds 5±2 items; more than that and the operator scans rather than absorbs. Use the count as a budget, not a target: three well-chosen tiles beat seven adequate ones.
Default workspace breakdown for a multi-system project:
| Workspace | Purpose | Tile count |
|---|---|---|
| Overview | Is the system OK? Top-level KPIs + alarms only. | 4-6 |
| <Subsystem> | One per major subsystem (Engine, Battery, Comms, ...). | 5-8 |
| Diagnostics | Raw flags, debug, individual sensors, status grids. | 6-10 |
| Signal Analysis | FFT / Waterfall / Plot3D, one signal at a time. | 2-4 |
The Overview workspace is the most-used and the least-edited. Treat it like the front page of a newspaper: every tile fights for the slot.
For each candidate group, ask in order:
widgetMin/widgetMax to operating bounds, not sensor bounds).If a group fails (1) and (4), it does NOT belong on Overview; push it to its subsystem workspace or Diagnostics. Resist the user's urge to "just show everything". That request gets harder to take back once the layout is built.
The Peak-End rule applies to dashboards: the operator's recall of the session is anchored by the most striking tile they saw AND the last tile they looked at. Use this:
relativeIndex controls ordering. Omit it for auto-assignment when you
don't care; pass it explicitly when corner placement matters.
The user isn't picking a widget; they're picking how the operator will read the value. Map the task to the tile, not the dataset's type:
| Operator task | Best widget | Why |
|---|---|---|
| "Is X in range?" (yes/no) | LED, Gauge w/ band | Position-in-band beats a number |
| "What is X right now?" (exact) | DataGrid, Gauge p1 | Number is unambiguous |
| "Is X trending up/down?" | Plot | Trend is the whole point |
| "Frequency content?" | FFT, Waterfall | Time-series hides structure |
| "Compare X to Y, Z, W?" | MultiPlot, Bar | Side-by-side beats four plots |
| "Where is X in 3D space?" | Plot3D, GPS | Spatial beats coordinate triples |
| "Has X crossed a threshold?" | NotificationLog | Events are events, not samples |
A Gauge for "what's the exact value" is the wrong choice: needles read to ±5%, digital readouts to the last digit. A Plot for "is X in range right now" is the wrong choice: the operator has to scan a line and mentally compare to bounds. Match the tile to the question.
Serial Studio runs on operator stations where mouse use is sometimes
blocked (gloves, panel-mount, accessibility need). Workspaces are
tab-navigable from the taskbar; widget focus follows DOM order, which
matches relativeIndex order. So:
relativeIndex order; don't put the most-important
tile last in the tab cycle."ALARM" label), shape (filled vs
outlined), or position (LED moving from a normal zone to an alarm
zone). theme.alarm is fine for emphasis, NOT for the sole signal..units field renders on every
tile that uses it; don't override with an empty string for "cleaner
look". An operator reading a number without units is guessing.project.workspace.autoGenerate{} produces a "reasonable starting
layout" (one tile per group, default widget per dataset). It's a
starting point, not a finish line. After auto-generate:
project.workspace.list to see what landed where.autoGenerate did it and
it felt rude to delete tiles. Cut ruthlessly. A noisy Overview is a
failed Overview.dashboard_layout min/max section: wgtMin /
wgtMax are mandatory write-form names.