# Local HTTP API — handoff

## Summary

Paarrot exposes a localhost HTTP API (port **33384**) so external tools — Stream Deck, scripts, macros — can control the running desktop app: mute/deafen, switch rooms, send messages, and read status. The Electron main process runs an Express server; actions are forwarded to the renderer via IPC and executed against the live Matrix client and call service.

## User-facing behavior

- API starts automatically when the Electron app launches (no user setting).
- Only binds to `127.0.0.1` — not reachable from the network.
- No authentication today; security relies on localhost-only binding.

## Architecture

```
External client (curl, Stream Deck, script)
  → GET/POST http://127.0.0.1:33384/...
    → electron/api-server.js (Express)
      → IPC to renderer (executeAction)
        → cinny/src/app/paarrot-api.ts (initPaarrotAPI)
          → Matrix client / CallService / router navigate
```

Navigation for room changes uses `setPaarrotNavigate()` registered from the React router in `ClientNonUIFeatures.tsx`.

## Key files

| Path | Role |
|------|------|
| `electron/api-server.js` | Express routes, IPC bridge to renderer |
| `electron/main.js` | Starts `PaarrotAPIServer` with the main window |
| `cinny/src/app/paarrot-api.ts` | Action handlers (mute, deafen, channel, message, status) |
| `cinny/src/app/pages/client/ClientNonUIFeatures.tsx` | Calls `initPaarrotAPI()` and `setPaarrotNavigate()` |
| `test-api.js` / `test-api.sh` | Smoke tests for endpoints |
| `paarrot-api.postman_collection.json` | Postman collection (generated) |

## Data model

No persistent storage. Actions read/write ephemeral app state (current room, call mute/deafen).

### IPC actions (renderer)

| Action | Params | Returns |
|--------|--------|---------|
| `get-status` | — | mute, deafen, currentRoom, connected, userId |
| `toggle-mute` / `set-mute` | `{ muted? }` | `{ muted }` |
| `toggle-deafen` / `set-deafen` | `{ deafened? }` | `{ deafened }` |
| `change-channel` | `{ roomId }` | navigation result |
| `get-channels` | — | room list |
| `send-message` | `{ roomId, message }` | send result |
| `send-message-current` | `{ message }` | send to active room |
| `get-current-room` | — | current room id |

## Dependencies

- `express`, `cors`, `body-parser` (Electron)
- `matrix-js-sdk` (renderer)
- Call feature: `getCallService()` from `features/call/useCall.ts`

## Integration points

- **Voice calls**: mute/deafen actions delegate to `CallService`
- **Stream Deck**: `streamdeck-plugin/` consumes this API
- **Room navigation**: uses `getHomeRoomPath`, `getDirectRoomPath`, `getSpaceRoomPath`
- **Space detection**: `isSpaceLikeRoom()` checks `im.paarrot.room.kind` and `m.forum` type

## Testing

### Manual

1. Start Paarrot desktop (`npm run dev` or built app).
2. `curl http://127.0.0.1:33384/health` → `{ "status": "ok" }`
3. `curl http://127.0.0.1:33384/status` while logged in.
4. Join a voice call; `POST /mute/toggle` and verify UI state.
5. `POST /channel` with a room id; verify navigation.

### Automated

- `node test-api.js` from repo root

## Known issues & gotchas

- Mute/deafen only work when `CallService` has an active call; otherwise handlers return a message, not an error.
- API is Electron-only; `initPaarrotAPI` no-ops when `window.electron.api` is missing (web/Tauri builds).
- Adding endpoints requires changes in **both** `api-server.js` and `paarrot-api.ts`.

## Future work

- Optional auth token for local API
- WebSocket or SSE for status push (Stream Deck polling today)
- Document all endpoints in Postman collection generator (`scripts/generate-postman-collection.js`)

## Related docs

- [API.md](../../API.md) — full endpoint reference
- [API-QUICKSTART.md](../../API-QUICKSTART.md)
- [streamdeck HANDOFF](../streamdeck/HANDOFF.md)

## Add your extra things here

- Default port: **33384** (`PaarrotAPIServer` constructor in `api-server.js`)
- `isSpaceLikeRoom` treats `forum_space` room kind and `m.forum` create type as spaces for channel listing
