# InstantGUI

> Live dashboards for anything that can send an HTTP request. AI agents declare a board in one JSON call, push live values by widget key, add buttons/toggles the viewer uses to act back, and embed it anywhere.

## Quickstart (curl)

1. Credential. Use the edit key from the edit link you were given (`/b/<board>/join?key=<edit key>`) — or create a board:
```bash
curl -s -X POST https://instant-gui.com/api/v1/boards -H 'Content-Type: application/json' -d '{"title":"Shop live"}'
# → {"board":{"id":"<board>",…},"editLink":"…/b/<board>/join?key=<edit key>","writeToken":"igw_…"}
```
Every API call accepts either credential: `Authorization: Bearer <edit key or write token>`.
Need the write token (for scripts/devices) from an edit key? `curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/token -H 'Authorization: Bearer <edit key>'` (`{"rotate":true}` issues a new one).

2. Declare the whole board in ONE idempotent call (create/update/move widgets, rename the board; `"prune": true` deletes widgets not listed):
```bash
curl -s -X PUT https://instant-gui.com/api/v1/boards/<board>/spec -H 'Authorization: Bearer <credential>' -H 'Content-Type: application/json' -d @spec.json
# → {"ok":true,"created":[…],"updated":[…],"unchanged":[…],"deleted":[],"hints":[…],"lint":{…},"urls":{…}}
```
Read `hints` in the response: they flag defaults that change behaviour — controls are editor-only unless `config.viewersCanUse: true`, a number with `decimals: 0` shows rounded, overlapping layouts.
spec.json:
```json
{
  "title": "Shop live",
  "prune": true,
  "widgets": [
    {
      "key": "headline",
      "type": "markdown",
      "layout": {
        "x": 0,
        "y": 0,
        "w": 12,
        "h": 2
      },
      "data": {
        "text": "**Sales are up 12% today.** Ads paused at 14:05 by Dana."
      }
    },
    {
      "key": "revenue",
      "type": "kpi",
      "title": "Revenue today",
      "layout": {
        "x": 0,
        "y": 2,
        "w": 3,
        "h": 3
      },
      "config": {
        "prefix": "$",
        "icon": "wallet"
      },
      "data": {
        "value": 4820
      }
    },
    {
      "key": "orders",
      "type": "kpi",
      "title": "Orders today",
      "layout": {
        "x": 3,
        "y": 2,
        "w": 3,
        "h": 3
      },
      "config": {
        "icon": "shopping-cart"
      },
      "data": {
        "value": 131
      }
    },
    {
      "key": "conversion",
      "type": "gauge",
      "title": "Conversion",
      "layout": {
        "x": 6,
        "y": 2,
        "w": 3,
        "h": 3
      },
      "config": {
        "max": 10,
        "suffix": "%",
        "decimals": 1
      },
      "data": {
        "value": 3.4
      }
    },
    {
      "key": "ads",
      "type": "actions",
      "title": "Ad campaign",
      "layout": {
        "x": 9,
        "y": 2,
        "w": 3,
        "h": 3
      },
      "config": {
        "buttons": [
          {
            "id": "pause",
            "label": "Pause ads",
            "tone": "danger"
          },
          {
            "id": "resume",
            "label": "Resume ads",
            "tone": "success"
          }
        ]
      }
    },
    {
      "key": "revenue-trend",
      "type": "line",
      "title": "Revenue per day ($)",
      "layout": {
        "x": 0,
        "y": 5,
        "w": 8,
        "h": 5
      },
      "data": {
        "series": [
          {
            "name": "revenue",
            "points": [
              [
                "2026-10-08",
                4100
              ],
              [
                "2026-10-09",
                4390
              ],
              [
                "2026-10-10",
                4820
              ]
            ]
          }
        ]
      }
    },
    {
      "key": "channels",
      "type": "pie",
      "title": "Orders by channel",
      "layout": {
        "x": 8,
        "y": 5,
        "w": 4,
        "h": 5
      },
      "data": {
        "slices": [
          {
            "name": "Organic",
            "value": 61
          },
          {
            "name": "Ads",
            "value": 44
          },
          {
            "name": "Email",
            "value": 26
          }
        ]
      }
    },
    {
      "key": "health",
      "type": "status",
      "title": "Systems",
      "layout": {
        "x": 0,
        "y": 10,
        "w": 4,
        "h": 4
      },
      "data": {
        "services": [
          {
            "name": "Checkout",
            "status": "up"
          },
          {
            "name": "Payments",
            "status": "up"
          }
        ]
      }
    },
    {
      "key": "orders-log",
      "type": "log",
      "title": "Recent orders",
      "layout": {
        "x": 4,
        "y": 10,
        "w": 8,
        "h": 4
      },
      "data": {
        "entries": [
          {
            "t": 1760000000000,
            "text": "#1045 · $129.00 · Organic",
            "level": "success"
          }
        ]
      }
    }
  ]
}
```

3. Keep it live — push by key:
```bash
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/widgets/revenue -H 'Authorization: Bearer <credential>' -d '{"value": 5010}'
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/widgets/revenue-trend -H 'Authorization: Bearer <credential>' -d '{"append": {"revenue": 5010}}'          # x = now
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/widgets/revenue-trend -H 'Authorization: Bearer <credential>' -d '{"append": {"revenue": ["2026-10-11", 5010]}}'  # explicit x (date/ms)
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/widgets/orders-log -H 'Authorization: Bearer <credential>' -d '{"append": {"t": 1760000100000, "text": "#1046 · $54.50 · Email", "level": "success"}}'   # t = epoch ms (default: now)
```

4. Read controls back (what the viewer clicked): `GET /api/v1/boards/<board>/widgets/ads` → `data.lastAction`, or set `config.webhookUrl` to be called on every click.

5. Check the design: `GET /api/v1/boards/<board>/lint` → `{"ok":true,"warnings":[],"suggestions":[…]}`. Fix every warning.

6. Embed (iframe, works for private boards too):
```bash
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/view-tokens -H 'Authorization: Bearer <edit key>' -d '{"label":"Website"}'
# → {"embedUrl":"https://instant-gui.com/e/<board>?token=<view token>","iframe":"<iframe src=…>"}
```
Look (validated; the board's saved `embedStyle` applies first): `&theme=auto|light|dark|<theme id>` (auto = viewer's light/dark), `&bg=transparent` (no page background/padding — sits on the host card), `&accent=<hex>`, `&radius=<px 0-32>`, `&density=compact`, `&font=system|serif|mono|<font-family list>`, `&c.<token>=<color>` (tokens: background, foreground, card, cardForeground, border, input, primary, primaryForeground, muted, mutedForeground, success, warning, danger, info, chart2–chart6), `&page=<page title>`. Easiest on a web page: `<div data-instantgui-board="<board>" data-token="<view token>" data-inherit-theme></div><script src="https://instant-gui.com/embed.js" async></script>` (auto height, adopts the page's colors/font, follows its light/dark), or `InstantGUI.mount(el,{board,token,transparent:true,colors:{background:"var(--surface)",foreground:"var(--text)",primary:"var(--accent)"}})`. Host messages: post `{type:"instantgui:theme",reset:true,theme,bg,radius,density,font,vars:{background,foreground,card,border,primary,…}}` to retheme live; the embed posts `{type:"instantgui:ready"}`, `{type:"instantgui:resize",height}` (it never sizes by the frame's height) and `{type:"instantgui:scheme",scheme}` (set the iframe's color-scheme to it when transparent). `https://instant-gui.com/theme.css` gives a host page InstantGUI's tokens as `--ig-*` variables. Default board embed look: `PATCH /api/v1/boards/<board>` with `{"embedStyle":{"theme":"auto","bg":"transparent","vars":{…}}}`. Controls in an embed are locked unless the token was created with `{"canUseControls":true}` (the default token is view-only; a button also only does something if a Connector, `config.webhookUrl` or your script reading `data.lastAction` handles it; edit key only; use it only on private pages — presses show "by embed: <label>"). The parameter is `token` (`t` works as an alias). A private board without a valid token answers 403, a wrong or revoked token is refused too. Revoke: `DELETE /api/v1/boards/<board>/view-tokens/<id>`.

Change one widget: `PUT /api/v1/boards/<board>/widgets/<key>` with `{"type","title","config","data","layout"}` (create or update), or `PATCH …/widgets/<key>` with `{"layout":{"x":0,"y":0,"w":6,"h":4}}` to move/resize.
Rename/theme/visibility/default embed look: `PATCH /api/v1/boards/<board>` with `{"title","theme","visibility","embedStyle"}` (visibility needs the edit key).
List widgets: `GET /api/v1/boards/<board>/widgets`. Widget schemas: `GET /api/v1/widgets`. OpenAPI: `GET /api/v1/openapi.json`. MCP: `https://instant-gui.com/api/mcp` (tool `apply_board_spec`).

## Change a board in place
"Rename it, move that widget, pause that button, embed it": each is ONE call on the board you already built. Never recreate the board or send the spec again for these.
```bash
# Rename the board
curl -s -X PATCH https://instant-gui.com/api/v1/boards/<board> -H 'Authorization: Bearer <credential>' -d '{"title":"Shop live (EU)"}'
# Move or resize a widget (only the fields you send change)
curl -s -X PATCH https://instant-gui.com/api/v1/boards/<board>/widgets/revenue -H 'Authorization: Bearer <credential>' -d '{"layout":{"x":0,"y":0,"w":6,"h":3}}'
# Pause a control: viewers can no longer press it (editors still can). Turn it back on with true
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/widgets/ads -H 'Authorization: Bearer <credential>' -d '{"config":{"viewersCanUse":false}}'
# Embed a private board: give the site a view token (revocable, read-only)
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/view-tokens -H 'Authorization: Bearer <credential>' -d '{"label":"Website"}'
# → the embed answer holds "embedUrl" (https://instant-gui.com/e/<board>?token=<view token>) and a ready "iframe"
```
MCP: update_board (rename), update_widget_layout (move/resize), push_data with {"config":{…}} (pause), create_view_token (embed).

## Press a control you just wired
Wiring a button is not done until you have pressed it. The press runs the Connector command on the user's machine (no connector, or a name no control sends, answers 404 with the names that exist).
```bash
# 1. Press it as the editor (edit key or write token). Name = <widget key> or <widget key>/<button id>; list the valid names with GET https://instant-gui.com/api/v1/boards/<board>/actions
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/actions -H 'Authorization: Bearer <credential>' -d '{"action":"backup/run"}'
# → 202 {"ok":true,"event":{"id":"<event id>","action":"backup/run","status":"pending"},"result":"…/actions?events=<event id>"}
# 2. Read the result (poll until status is "done" or "error"; "result" holds the command's output)
curl -s https://instant-gui.com/api/v1/boards/<board>/actions?events=<event id> -H 'Authorization: Bearer <credential>'
# → {"ok":true,"events":[{"id":"<event id>","action":"backup/run","status":"done","result":"backup ok",…}],…}
```
MCP: trigger_action {action:"backup/run"}, then list_actions for the result. Viewers can press a control only if its `config.viewersCanUse` is true; with the edit key or write token you always can.

Calling it with plain HTTP? Send both Content-Type: application/json and Accept: application/json, text/event-stream — without that Accept header the server answers 406 and names the missing header. The reply is a server-sent event: a line 'event: message' followed by a line starting 'data: ' that holds the JSON-RPC response; read the JSON from that data line.
```bash
curl -X POST https://instant-gui.com/api/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

## Docs
- [Agent guide + design rules](https://instant-gui.com/docs/agents): this page as HTML, with the design guide
- [Full reference for LLMs](https://instant-gui.com/llms-full.txt): this file + design guide + every widget type with data examples
- [OpenAPI 3.1](https://instant-gui.com/api/v1/openapi.json)
- [Widget catalogue (JSON Schemas)](https://instant-gui.com/api/v1/widgets)
- [MCP server](https://instant-gui.com/api/mcp): Streamable HTTP; tools apply_board_spec, push_data, lint_board, get_write_token, create_view_token, list_actions, trigger_action, get_design_guide…
- [Connector (Markdown)](https://instant-gui.com/docs/connector.md): buttons on the board run allow-listed commands on the user's machine (outbound-only). Short version: `POST /api/v1/boards/<board>/connectors` (edit key) → token; on the machine `curl -fsSL https://instant-gui.com/connect.sh | sh -s -- --board <board> --token <igc_…> [--no-service]`; list actions in `~/.instantgui/connect.yml` as `<widget key>` or `<widget key>/<button id>` → `run: <command>` (value in "$INSTANTGUI_VALUE"); end a silent command with `&& echo "<summary>"` so the board shows what happened (a command with no output logs "(no output)"); check with `GET /api/v1/boards/<board>/actions` (warnings for names no control sends).
- [Agent guide (Markdown)](https://instant-gui.com/docs/agents.md)

## Connector: a board button runs a command on this machine
1. Create a token: `POST https://instant-gui.com/api/v1/boards/<board>/connectors` with the edit key → `igc_…` token (shown once).
2. Install on a shared or headless machine WITHOUT a login service, in a private directory:
```bash
curl -fsSL https://instant-gui.com/connect.sh | INSTANTGUI_HOME=/tmp/ig sh -s -- --board <board> --token <igc_…> --no-service
```
   `--no-service` skips the start-at-login service (LaunchAgent / systemd / scheduled task); without it the installer registers one. `INSTANTGUI_HOME=<dir>` keeps the install and `connect.yml` in `<dir>` instead of `~/.instantgui`.
3. Allow-list what a button may run in `$INSTANTGUI_HOME/connect.yml` (nothing else runs): `actions: { <widget key>: { run: <command>, output: <log widget key> } }`; use `<widget key>/<button id>` for an `actions` widget. The control value is in `"$INSTANTGUI_VALUE"`.
4. Start it yourself: `INSTANTGUI_HOME=/tmp/ig node /tmp/ig/bin/instantgui-connect.mjs`.
5. Check: `GET https://instant-gui.com/api/v1/boards/<board>/lint` warns about an action name no control sends (`unknown-action`) and an `output`/`report` widget that doesn't exist (`missing-output-widget`).
Full guide: https://instant-gui.com/docs/connector.md

# Designing an InstantGUI board (for AI agents)

A board is read in 3 seconds on a phone. Show, don't tell. Every rule below is checked by `GET /api/v1/boards/<board>/lint`.

## Pick the widget by the question
| The user asks… | Use |
|---|---|
| "How much / how many right now?" | `kpi` (one number + trend) or `stats` (2–6 numbers in a row) |
| "How full / how close to the limit?" | `gauge` (dial) or `progress` (bar toward a goal) |
| "How is it changing?" | `line` — append one point per update |
| "Which is bigger?" | `bar` |
| "What's the split?" | `pie` (≤ 6 slices) |
| "Is it healthy?" | `status` (several services) or `indicator` (one light) |
| "What just happened?" | `log` (append lines) or `timeline` |
| "What do I do now?" | `actions` / `button` / `toggle` / `slider` / `input` / `select` |
| "Why?" (context) | ONE `markdown` headline, ≤ 2 sentences |

## Rules
1. **Numbers and charts, not prose.** At most one markdown widget: a headline of ≤ 2 sentences (≤ 280 characters). Never put numbers, lists or status in text — they each have a widget.
2. **Most important thing top-left** (x 0, y 0). Then group widgets by the question they answer; one page per audience or topic (≤ 16 widgets a page).
3. **Always add actions** the viewer would want next: Restart, Pause, Resume, Run now, Acknowledge, Refresh. An on/off state (paused, maintenance) is a toggle in the actions widget, not two buttons: `config.toggles: [{"id":"paused","label":"Paused","on":"pause-all","off":"resume-all"}]` + push the real state as `data.states: {"paused": true}`. A toggle whose ON state is bad (a safety stop, an outage flag) gets `"onTone": "danger"` so it turns red when on. A pick-one-or-more set (focus list, filter) is a chip set: buttons + `data.selected: ["<button id>", …]` — selected chips are filled with a check and `aria-pressed="true"`, the rest neutral; push the real selection after each change (a click only sends the button's action). Never show a selection only by colour or a ✓ in the label. A click reaches you three ways: the InstantGUI Connector runs a command on the user's machine (`/docs/connector`), a webhook (`config.webhookUrl`, https) gets a POST, or your script reads it back (`GET …/widgets/<key>` → `data.lastAction` / `data.on` / `data.value`). Controls are editor-only by default (the human's edit link); set `config.viewersCanUse: true` only if anyone with the view link may press them.
4. **Trends beat snapshots.** If a number changes over time, also keep a `line` of it (append a point each update). Don't put numeric tables where a bar/line chart works.
5. **Title every widget with what it answers and the unit**: "Revenue today ($)", "p95 latency (ms)". Put units in kpi `prefix`/`suffix`, not in the title AND the value.
6. **Color means state**: use thresholds (kpi/gauge `thresholds`) and `status` levels up/degraded/down — not decoration. Green = fine, amber = look, red = act.
7. **Mobile-first layout.** 12-column grid, rows ≈ 52 px. Phones (≤ 640 px) stack widgets in reading order (y, then x). Good sizes: kpi w 3 h 3 · stats w 6–12 h 2 · charts w ≥ 6 h ≥ 4 · status/log w 4–6 h 4–6 · actions w 3–4 h 2–3 · headline w 12 h 2.
8. **Update cadence**: push when the value changes, at most every few seconds per widget; append to charts/logs instead of rewriting them; send the full spec again only when the board's *structure* changes (the spec is idempotent, so re-sending it is safe — in a spec, `data` of streams like line/log/table only seeds a new widget and never wipes what you appended).
9. **Stable keys**: lowercase, short, meaningful (`revenue`, `orders-log`). Your scripts push by key forever.
10. **Use the same labels across widgets so they cross-filter.** Clicking a bar, pie slice, line or a text cell in a table filters the page by that value (a chip bar shows it; click again, ✕ or Esc clears): charts highlight the matching category/series/slice, tables and logs narrow to rows/lines containing it, other widgets are untouched. It works only when values match exactly (case-insensitive) — name bar categories, pie slices and table cells the same ("North", not "north region").

## Anti-patterns (the lint flags these)
- Walls of text, several markdown widgets, or a markdown "report" of numbers.
- A table of numbers where a chart would answer at a glance.
- 20+ widgets on one page; widgets 1–2 columns wide; charts < 3 rows tall.
- No actions: a dashboard you can only look at.
- Leaving sample data in widgets.

## Widget types
Settings go in `config` (or `title` as a shorthand), live values in `data`. Exact JSON Schemas: https://instant-gui.com/api/v1/widgets
- **kpi** (metrics, default 3×3): A headline number with change vs. previous value and a trend sparkline. Push: `{"value":48320}` · `{"value":48320,"previous":45910}`
- **stats** (metrics, default 6×2): Several small numbers side by side, each with an optional change. Push: `{"items":[{"label":"Visitors","value":12840,"change":8.1},{"label":"Signups","value":342}]}`
- **gauge** (metrics, default 3×4): A dial for a value within a range — CPU, temperature, utilisation. Push: `{"value":63}`
- **progress** (metrics, default 3×2): Progress toward a goal, as a bar or ring. Push: `{"value":27}`
- **line** (charts, default 6×5): One or more series over time. Push points and the chart scrolls live. Push: `{"append":{"api":912,"web":455}}` · `{"append":{"api":[1767225600000,912]}}`
- **bar** (charts, default 6×5): Compare values across categories. Vertical or horizontal, grouped or stacked. Push: `{"categories":["Mon","Tue","Wed"],"series":[{"name":"Orders","values":[12,19,7]}]}`
- **pie** (charts, default 4×5): Share of a whole across a few categories. Push: `{"slices":[{"name":"Yes","value":62},{"name":"No","value":38}]}`
- **scatter** (charts, default 6×5): Correlation between two measures, with an optional linear trend line. Push: `{"append":{"Campaigns":[820,74]}}`
- **radar** (charts, default 4×5): Compare a few profiles across the same set of dimensions. Push: `{"indicators":[{"name":"Tests","max":100},{"name":"Docs","max":100},{"name":"Perf","max":100},{"name":"Security","max":100},{"name":"UX","max":100}],"series":[{"name":"v2.0","values":[95,72,85,88,80]}]}`
- **tree** (charts, default 6×6): Hierarchies: org charts, file trees, decision trees. Push: `{"root":{"name":"Root","children":[{"name":"A"},{"name":"B"}]}}`
- **table** (data, default 6×5, interactive): Rows of data with sorting and search. Append rows from scripts. Push: `{"append":{"Order":"#1046","Customer":"Ken T.","Total":77,"Status":"Paid"}}` · `{"rows":[{"Order":"#1047","Customer":"Ada L.","Total":54,"Status":"Paid"}]}`
- **log** (data, default 6×5): A scrolling feed of events — perfect for bots, jobs and deploys. Push: `{"append":"Deploy finished in 42s"}` · `{"append":{"text":"Disk at 91%","level":"warning"}}`
- **checklist** (data, default 4×4, interactive): A shared to-do list. Viewers can tick items when allowed. Push: `{"append":"Rotate API keys"}`
- **kanban** (data, default 8×5, interactive): Cards in columns — drag them along as work moves. Scripts can add cards. Push: `{"append":"Fix login redirect"}` · `{"append":{"title":"Ship v2.1","column":"doing","tag":"release"}}`
- **timeline** (data, default 4×5): Milestones and events in order — releases, incidents, project history. Push: `{"append":"Deployed v2.1"}` · `{"append":{"title":"Incident resolved","date":"2026-10-09","tone":"success"}}`
- **people** (data, default 4×4): Who's on the team and who's around — on-call, shifts, presence. Push: `{"append":{"name":"Maya Cohen","status":"away"}}`
- **status** (status, default 4×4): Health of several services at a glance. Update one service at a time. Push: `{"append":{"name":"API","status":"down","detail":"502 from upstream"}}`
- **indicator** (status, default 3×2): One big status light: is the bot running? Is the door open? Push: `{"status":"up","label":"Running"}` · `{"status":"down","label":"Exchange API unreachable"}`
- **button** (controls, default 3×2, interactive): Trigger something: restart a bot, run a job, send a message. Push: `{"presses":0}`
- **actions** (controls, default 4×3, interactive): Several buttons in one panel — restart, deploy, pause, clear cache. Push: `{"counts":{},"lastAction":null,"lastAt":null}`
- **toggle** (controls, default 3×2, interactive): An on/off switch your script reads — feature flags, pause trading, lights. Push: `{"on":true}`
- **slider** (controls, default 4×2, interactive): Pick a number in a range — thresholds, volume, risk level. Push: `{"value":40}`
- **input** (controls, default 4×2, interactive): Send a line of text or a number to your script. Push: `{"value":"hello"}`
- **select** (controls, default 3×2, interactive): Choose one option from a list — mode, environment, strategy. Push: `{"value":"Aggressive"}`
- **markdown** (content, default 4×3): Headings, notes and links, written in Markdown. Push: `{"text":"# Deploy status\nAll green ✅"}`
- **accordion** (content, default 4×4): Questions and answers that expand on click — runbooks, FAQs, notes. Push: `{"append":{"q":"Who owns this board?","a":"The platform team."}}`
- **image** (content, default 4×4): Show an image or a camera snapshot that refreshes on a timer. Push: `{"url":"https://example.com/cam.jpg"}`
- **embed** (content, default 6×5): Embed a web page, video or form (YouTube, Google Docs, Loom…). Push: `{"url":"https://example.com"}`
- **clock** (time, default 3×2): Current time in any time zone.
- **countdown** (time, default 4×2): Days, hours and minutes until a launch, deadline or event. Push: `{"target":"2026-12-31T23:59:00Z"}`

## Push semantics
- `{"value": 42}` → sets data.value. `{"data": {...}}` merges data fields. Other top-level fields are data too.
- `{"append": …}` → adds to charts (point per series name), logs (line), tables (row), status (upsert one service by name), timeline.
- `{"config": {...}}` → merges settings.
- GET `https://instant-gui.com/api/v1/boards/<board>/widgets/<key>/push?token=<write token>&value=42` for devices that can only call a URL.

## Errors
Every error is JSON: `{"ok":false,"error":"<what went wrong and what to do next>","details":[…]}`. 401/403 → wrong or missing credential; 404 → unknown board/widget/route; 422 → invalid body (details name the field); 429 → slow down (Retry-After).

## InstantGUI Connector

Press a button on your dashboard — from your phone, anywhere — and a command runs on your computer. The Connector is
one small file (Node 18+, no dependencies) that keeps an outbound connection to the board: no port forwarding, no
inbound access, works behind NAT and firewalls.

### 1. Install (one line)
In the board: **Connect → Run things on your computer → Set up the Connector**. It shows a command with this board's
connector token, e.g.:
```bash
curl -fsSL https://instant-gui.com/connect.sh | sh -s -- --board <board> --token <igc_…>
```
Windows (PowerShell): `$env:IG_BOARD="<board>"; $env:IG_TOKEN="<igc_…>"; irm https://instant-gui.com/connect.ps1 | iex`

It downloads `~/.instantgui/bin/instantgui-connect.mjs`, writes `~/.instantgui/connect.yml` (chmod 600) and starts it at
login (macOS LaunchAgent, Linux systemd user service, Windows scheduled task).
- `--no-service`: don't start at login (run it yourself: `node ~/.instantgui/bin/instantgui-connect.mjs`).
- `INSTANTGUI_HOME=<dir>` (before `sh`): install a separate connector in `<dir>` instead of `~/.instantgui` — e.g.
  `curl -fsSL https://instant-gui.com/connect.sh | INSTANTGUI_HOME=/tmp/ig sh -s -- --board <board> --token <igc_…> --no-service`. Agents can create the token with
`POST https://instant-gui.com/api/v1/boards/<board>/connectors` (edit key) — the response includes the install commands.

### 2. Say what each button may run
`~/.instantgui/connect.yml` — **nothing runs unless it is listed here**:
```yaml
url: https://instant-gui.com
board: <board>
token: igc_…
report: actions-log            # optional: a log widget that gets one line per result

actions:
  deploy:                      # a button with key "deploy"
    label: Deploy site
    run: cd ~/site && git pull && npm run build
    confirm: true              # press twice within 15 s
    timeout: 300
  server/restart:              # an "actions" widget "server", button id "restart"
    run: sudo systemctl restart nginx
  focus:                       # a text input with key "focus"
    run: ~/bin/set-focus "$INSTANTGUI_VALUE"
    output: focus-output       # a markdown/log widget that shows the command's output
```
Action names: `<widget key>` for button, toggle, slider, input and select; `<widget key>/<button id>` for an
`actions` widget. The control's value (toggle on/off, slider number, input text) is in **`$INSTANTGUI_VALUE`** —
always quote it. The file is re-read when it changes. `… actions` checks every name against the board and warns about
names no control sends (and missing `report`/`output` widgets) — the same warnings show in the Connect panel and in
`GET /api/v1/boards/<board>/actions`. Tip: end a silent command with `&& echo "<summary>"` so the board shows what happened; a command with no output still logs "(no output)" and updates the button's last-run time and count.

### 3. See it work
The Connect panel shows the connector **online/offline**, when it was last seen, and every action with its last result
(✓/✗, exit code, first line of output). `report:`/`output:` widgets show results on the board itself.
Locally: `node ~/.instantgui/bin/instantgui-connect.mjs test deploy` runs an action without the board;
`… actions` lists them; `… service status|uninstall`.

### Security — what the board can and can't do
- **The board never sends commands.** A click sends an action *name* and a value. The Connector runs only names in its
  own config; unknown names are refused and reported.
- **Values are data, not code**: passed only as `$INSTANTGUI_VALUE`, never pasted into the command line.
- **Signed, fresh events**: each event is HMAC-SHA256-signed with a key derived from your connector token and ignored if
  older than 60 s (a click made while your machine was asleep doesn't fire hours later).
- **Who can press**: controls are editor-only unless a widget sets `viewersCanUse: true` — anyone who can press a
  button can trigger its action, so only allow viewers on harmless actions.
- **Limits**: 30 queued clicks a minute per board on the server; on the machine one run per action at a time, 2 s
  between runs, 20 runs a minute; `confirm: true` needs two presses.
- **Revocable**: Connect → Connector → revoke (or `DELETE /api/v1/boards/<board>/connectors/<id>`); the token stops
  working at once. A connector token can only report results to this board — it can't edit it or read other boards.
- Treat `connect.yml` like an SSH key: it holds the token and decides what runs. Don't list commands you wouldn't run
  from your phone.

### API (for agents)
| Call | Auth | |
|---|---|---|
| `POST /api/v1/boards/<board>/connectors` `{"name"}` | edit key | token (once) + install commands |
| `GET /api/v1/boards/<board>/connectors` | edit key / write token | connectors, online, actions + last results, recent events |
| `DELETE /api/v1/boards/<board>/connectors/<id>` | edit key | revoke |
| `GET /api/v1/boards/<board>/actions` | edit key / write token | actions the connectors handle |
| `POST /api/v1/boards/<board>/actions` `{"action","value"}` | edit key / write token | run an action (202 + event id) |
| `POST …/connector/hello`, `GET …/connector/events?wait=25`, `POST …/connector/events/<id>/result` | connector token | the Connector's own protocol |

In a board spec, actions are just control widgets (`actions`, `button`, `toggle`…) — their keys/button ids are the
action names. MCP: `list_actions`, `trigger_action`.

## InstantGUI dashboards (recommended instructions — paste into CLAUDE.md, .cursor/rules or custom instructions)
When I ask for a dashboard, status page or "a UI for this", publish it to InstantGUI (https://instant-gui.com):
- If I give you an edit link (…/b/<board>/join?key=<edit key>), use the edit key as "Authorization: Bearer <edit key>". Otherwise create a board: POST /api/v1/boards and give me its editLink.
- Declare the whole board in one call: PUT /api/v1/boards/<board>/spec (idempotent; include layout). Then push live values with POST /api/v1/boards/<board>/widgets/<key>.
- Charts and numbers over text: kpi/stats/gauge for numbers, line for trends, bar for comparisons, status for health, log for events; at most ONE short markdown headline.
- Always add the actions I'd want next (pause/restart/run now) as actions/button/toggle widgets.
- Check GET /api/v1/boards/<board>/lint and fix every warning before telling me it's done.
- For a website/iframe: POST /api/v1/boards/<board>/view-tokens and use the returned embedUrl.
Docs: https://instant-gui.com/llms.txt
