# 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
