> ## Documentation Index
> Fetch the complete documentation index at: https://docs.embedder.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Project dashboards

> Create project-local Monitor dashboards from live telemetry with built-in charts, tables, controls, logs, and custom SVG or Canvas components.

A project dashboard is a JavaScript view that runs in a sandboxed Monitor tab. It can combine current values, plots, logs, tables, controls, and project-specific drawings without adding UI code to your firmware.

Ask Embedder for the operator task and the signals it should use:

```txt theme={"system"}
Create a dashboard for motor bring-up. Show drive health, target and measured
speed, the last 40 faults, and controls for target speed and emergency stop.
Use the existing serial telemetry names.
```

Embedder discovers the live channels, writes the dashboard files, resolves file diagnostics, and opens the result with `dashboard_open`.

## Dashboard files

Each dashboard lives under a lowercase kebab-case slug:

```txt theme={"system"}
.embedder/dashboards/motor-bringup/
├── manifest.json
├── dashboard.js
└── dashboard.css
```

`dashboard.css` is optional. Keep custom component code in `dashboard.js` so later edits preserve the complete view.

A current manifest uses API version 3:

```json theme={"system"}
{
  "slug": "motor-bringup",
  "name": "Motor Bring-up",
  "description": "Live motor state, faults, and controls.",
  "apiVersion": 3,
  "subscriptions": ["plot:*", "serial:*", "serial-tabs", "serial-ports"],
  "events": [],
  "allowsHardwareWrite": false
}
```

The directory and `slug` must match. Embedder manages code hashes, CSS hashes, and timestamps after validated writes. An external code change is treated as a user edit, which protects it from an unreviewed agent overwrite.

Open dashboards appear in the Monitor's add-tab menu. File changes reload an open dashboard after the project watcher settles. You can also delete a dashboard from that menu.

## Bind live data

Declare every channel family the dashboard can read in `subscriptions`. An exact string matches one channel, and a trailing `*` matches a prefix.

Common Monitor channels are:

* `plot:<port>:<channel>` for numeric plot batches
* `serial:<tabId>` for serial lines
* `serial-tabs` for serial tab connection and configuration state
* `serial-ports` for detected ports
* `text:<port>:<channel>` for text telemetry
* `trace:<sessionId>` for trace events
* `capture:<captureId>` for capture manifests

Seed a component from recent data with `embedder.history(spec)`, then update it with `embedder.subscribe(spec, callback)`:

```javascript theme={"system"}
const dashboard = embedder.ui.dashboard({
  title: "Battery",
  subtitle: "Live power state",
});

const voltage = dashboard.metric({
  id: "battery-voltage",
  label: "Battery voltage",
  unit: "V",
  digits: 3,
});

const trend = dashboard.timeSeries({
  id: "battery-voltage-history",
  label: "Voltage history",
  unit: "V",
});

const update = (payload) => {
  const points = embedder.data.plotPoints(payload);
  trend.append(points);
  const latest = points.at(-1);
  if (latest) voltage.set(latest.value, latest.unit ?? "V");
};

embedder.history("plot:/dev/ttyUSB0:battery_voltage").then((items) => {
  for (const item of items) update(item.payload);
});
embedder.subscribe("plot:/dev/ttyUSB0:battery_voltage", update);
```

Use the adapters under `embedder.data` instead of reading payload fields by guess:

* `plotPoints`, `plotValues`, and `latestPlotPoint`
* `serialLines`, `serialTabs`, and `serialPorts`
* `textPoint`, `traceEvents`, and `captureManifest`

An adapter returns the adapted field, not the original payload. Read identifiers such as `tabId`, `port`, and `channel` from the payload itself.

Hardware scripts can publish numeric data with `embedder_publish_plot(source, points)`. Dashboards receive it through `plot:<source>:<channel>` subscriptions without Teleplot firmware output.

## Built-in component catalog

Start with `embedder.ui.dashboard()` and add only the components that support the operator's task.

### Readouts

* `metric`: a current numeric value
* `status`: a discrete state with `neutral`, `success`, `warning`, `error`, or `info` tone
* `gauge`: a bounded value with optional thresholds
* `progress`: a percentage
* `sparkline`: a compact recent numeric trend

### Plots and timelines

* `timeSeries`: timestamped numeric traces
* `xy`: arbitrary connected X/Y points
* `scatter`: unconnected X/Y points
* `logic`: digital signals over time
* `spectrum`: frequency-domain points with optional logarithmic axes
* `band`: a trace with low and high bounds
* `stateTimeline`: discrete states over time
* `barChart`: categorical values
* `histogram`: a value distribution
* `heatmap`: two-dimensional magnitude cells

Axis plots use uPlot and include zoom, cursor-centered wheel scaling, panning, per-series hover values, and a scrollbar when zoomed.

### Data and custom content

* `table`: object rows
* `log`: serial lines or event messages
* `serialMonitor`: the Monitor's serial UI in a dashboard card
* `card`: static text or custom DOM
* `drawing`: a responsive SVG or Canvas surface

### Layout

* `group`: related controls or readouts in one card
* `tabs`: task-specific views that keep their child components mounted
* `add`: attach a custom object that exposes an `element`

### Controls

* `button`: a synchronous local UI action
* `action`: asynchronous work or a hardware action with pending, success, and error states
* `toggle`: a Boolean selection
* `slider`: a bounded numeric selection
* `select`: one choice
* `multiSelect`: several choices

Use stable lowercase kebab-case component IDs. The responsive grid restores card order and selected tabs by ID. API v3 requires finite positive `minWidth` and `minHeight` values on every `card`, `group`, and `drawing`.

## Include the serial monitor card

Use `serialMonitor` when the dashboard reads or controls a serial session:

```javascript theme={"system"}
dashboard.serialMonitor({
  label: "Serial monitor",
  span: 3,
  maxLines: 500,
});
```

Its manifest must include:

```json theme={"system"}
"subscriptions": ["serial:*", "serial-tabs", "serial-ports"]
```

The card mirrors output, filtering, timestamps, port and baud controls, newline configuration, connection state, command history, and the send bar. It can reveal the full Monitor tab.

Set `allowsHardwareWrite` to `true` if the card's send and connection controls must be enabled. Without that flag, the card remains readable but its write controls are disabled.

## Send events and hardware commands

`embedder.emit({ name, payload })` reports a declared event to the agent's next turn. It queues the event but does not wake the agent or perform an action. Declare the name in `events` and handle an `accepted: false` result in the UI.

Event payloads must be JSON-serializable and no larger than 64 KiB. The default event budget is 20 per turn and 60 per minute.

For device actuation, use an `action` control and `embedder.hardware.send`:

```javascript theme={"system"}
let tabId = null;

const reset = dashboard.action({
  label: "Reset device",
  hardware: true,
  confirm: "Reset the connected device?",
});
reset.setEnabled(false);

embedder.subscribe("serial:*", (payload) => {
  if (!payload.tabId) return;
  tabId = payload.tabId;
  reset.setEnabled(true);
});

reset.onAction(async () => {
  if (!tabId) throw new Error("No connected serial tab");
  await embedder.hardware.send({ tabId, data: "reset\n" });
});
```

Hardware writes require all of these conditions:

* The manifest has `allowsHardwareWrite: true`.
* The command uses the ID of an existing serial tab.
* You approve the dashboard's current JavaScript hash.
* No conflicting hardware workflow owns the target.

The dashboard cannot choose UART versus RTT from a device path. It writes through the selected live serial tab. A change to `dashboard.js` outside Embedder invalidates the code-hash grant; a validated agent edit can carry an existing grant to the new hash.

## Build a custom component

Use a custom component for a view the built-ins do not provide, such as a pie, radar, polar plot, compass, Smith chart, waterfall, spectrogram, eye diagram, or 3D surface.

Create a `drawing` and append an SVG or Canvas element:

```javascript theme={"system"}
const drawing = dashboard.drawing({
  id: "heading",
  title: "Heading",
  span: 2,
  minWidth: 300,
  minHeight: 240,
});

const canvas = document.createElement("canvas");
canvas.style.cssText = "display:block;width:100%;height:100%";
drawing.surface.appendChild(canvas);
```

Choose SVG for discrete shapes and labels. Choose Canvas 2D for thousands of points, per-frame drawing, pixel effects, or a projected 3D surface.

Custom component code should:

* Build the DOM once and expose small mutators such as `set`, `append`, or `clear`.
* Size from `drawing.surface` with `ResizeObserver`.
* Bound every sample buffer.
* Ignore missing and non-finite values.
* Show an empty state until data arrives.
* Use the injected `--dash-*` theme variables.
* Clear timers or animation frames when replacing a component.

The dashboard sandbox blocks network access, dynamic library loading, browser storage, the parent DOM, and the VS Code API. Use `embedder.subscribe`, `embedder.history`, `embedder.state`, and the declared bridge methods instead. Persisted dashboard state is limited to 64 KiB.

<Note>
  The iframe allows scripts but does not receive same-origin access. Manifest
  subscriptions, event declarations, hardware grants, and the code hash form the
  boundary between dashboard code and the Monitor host.
</Note>
