# 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
  jobs/retry:                  # a row action "retry" on table "jobs": the row's key is the value
    label: Retry job
    run: ~/bin/retry-job "$INSTANTGUI_VALUE"
    value: '^[0-9]{1,9}$'      # anything else is refused before the command runs
```
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. Write each action as a block (as above); inline `{run: …}` maps
are not supported. `… 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.

## Row and record actions (keyed)
A button on every row of a table (`rowActions`) or on a record widget (`actions`, and `toggles` bound to a field)
sends **`<widget key>/<action id>`** with the **row's key as the value** — e.g. "Retry" on the row whose `id` is
`task-12` runs `issues/retry` with `$INSTANTGUI_VALUE=task-12`.
```json
{ "key": "jobs", "type": "table", "config": { "rowKey": "id", "viewersCanUse": false,
  "rowActions": [
    { "id": "retry",  "label": "Retry",  "tone": "warning", "when": [{ "field": "status", "op": "in", "value": ["failed", "reverted"] }] },
    { "id": "cancel", "label": "Cancel", "tone": "danger", "confirm": true, "when": [{ "field": "status", "op": "eq", "value": "queued" }] } ] } }
```
- The row key is `config.rowKey` (default `id`) of an inline row, or the record key of the widget's dataset.
- **The server decides, not the browser**: it re-reads the record from the dataset (or the table's rows) and refuses the
  click when the key doesn't exist or the action's `when` (a toggle's `disabledWhen`) fails on its own copy — a
  stale page can't retry a task that already succeeded.
- **Guard the value on your machine**: give every keyed action `value: '<regex>'` in `connect.yml`. The whole value
  must match; anything else is refused and reported before the command runs (and before a confirm is armed).
- Each row shows its own status line: "Sending “Retry”…" → "✓ Retry · 2s ago" or "✗ Retry failed: <first line> · 1m
  ago" (kept on `data.runs["<action id>:<key>"]`, newest 50). `confirm: true` on the action asks for a second tap
  within 15 s on the board; `confirm: true` in `connect.yml` asks again from the machine (per row). Use one of the
  two, not both (both = three taps).
- No connector handling the name = the click is refused ("No computer is connected…"). Editors only unless the widget
  sets `viewersCanUse: true`; embeds made with "can use controls" work too. Limits as below (30 a minute).
- `… actions` and the Connect panel list keyed names too, and warn about `connect.yml` names no row action sends.

## 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; `value: '<regex>'` refuses any other value.
- **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; row/record actions are `<widget key>/<action id>` with the record key as the value
(`POST …/actions {"action":"jobs/retry","value":"42"}` runs one directly). MCP: `list_actions`, `trigger_action`.
