# AnyBrief - Local HTTP API Contract

## Общие правила

- bind only to `127.0.0.1`
- default port `47823`, переопределяется в settings
- JSON only (Content-Type: `application/json`)
- API key required for all endpoints except `/health`
- header: `X-API-Key: <key>`
- API key генерируется приложением при первом запуске (random 32 bytes, hex-encoded) и сохраняется в Keychain
- при логировании запросов значение `X-API-Key` должно маскироваться
- CORS: `Access-Control-Allow-Origin` — только `http://127.0.0.1` и `http://localhost`; для остальных Origin header возвращать без CORS-заголовков

## Error response format

Любая ошибка возвращается в едином формате:

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "API key is missing or invalid",
    "details": {}
  }
}
```

HTTP status codes:
- `400` — validation error (`invalid_request`, `invalid_state`)
- `401` — missing/invalid API key (`invalid_api_key`)
- `404` — ресурс не найден (`not_found`)
- `409` — конфликт состояния (`already_recording`, `no_active_recording`)
- `422` — семантическая ошибка запроса
- `500` — внутренняя ошибка (`internal_error`)
- `503` — сервис недоступен (`service_unavailable`, например recorder CLI missing)

## Pagination

Эндпоинты, возвращающие коллекции (`/meetings/recent`, `/jobs`, `/logs/recent`), поддерживают:
- `?limit=<int>` (default `50`, max `500`)
- `?cursor=<opaque>` — возвращается в поле `nextCursor` в ответе

## Endpoints

### GET /health

Без auth. Всегда 200.

```json
{ "ok": true, "service": "anybrief", "version": "0.1.0" }
```

### GET /status

```json
{
  "app": "running",
  "recording": false,
  "currentJobId": null,
  "calendarConnected": true,
  "permissions": {
    "microphone": "granted",
    "systemAudio": "granted",
    "calendar": "granted",
    "notifications": "granted"
  }
}
```

### GET /permissions

Возвращает детальный статус всех permissions:

```json
{
  "permissions": [
    { "kind": "microphone", "status": "granted", "lastCheckedAt": "..." },
    { "kind": "system_audio", "status": "missing", "lastCheckedAt": "..." }
  ]
}
```

### POST /recording/start

Request:
```json
{
  "mode": "manual",
  "title": "Manual recording",
  "speakerCountMode": "auto",
  "speakerCount": null
}
```

Response `200`:
```json
{
  "jobId": "job_001",
  "meetingId": "meeting_001",
  "status": "recording"
}
```

Ошибки: `409 already_recording`, `503 recorder_unavailable`, `400 invalid_request`.

### POST /recording/stop

Request:
```json
{ "jobId": "job_001" }
```

Если `jobId` не передан — останавливается текущая запись.
Ошибки: `409 no_active_recording`.

### POST /recording/disable-auto-stop

Отключает автоостановку для текущей календарной записи (runtime flag).
Request: пустой body.
Response:
```json
{ "jobId": "job_001", "autoStopDisabled": true }
```

Ошибки: `409 no_active_recording`, `422 not_a_calendar_recording`.

### GET /recording/current

Возвращает информацию о текущей активной записи или `null`.

### GET /jobs

Список последних jobs с пагинацией.

```json
{
  "items": [ { "id": "job_001", "status": "completed", "..." } ],
  "nextCursor": "eyJvZmZzZXQiOjUwfQ=="
}
```

### GET /jobs/{id}

```json
{
  "id": "job_001",
  "meetingId": "meeting_001",
  "status": "summarizing",
  "stage": "summarizing",
  "progressPercent": 85,
  "updatedAt": "2026-04-20T14:42:10+03:00",
  "error": null
}
```

### POST /jobs/{id}/cancel

Отменяет активный job. Допустимо только для статусов до `packaging`.
Response:
```json
{ "id": "job_001", "status": "cancelled" }
```

Ошибки: `409 job_not_cancellable`, `404 not_found`.

### GET /calendar/today

Список событий на сегодня (с учётом текущего state refresh).

### POST /calendar/sync

Принудительный refresh календарных данных. Body пустой.

### GET /meetings/today

### GET /meetings/recent

С пагинацией (см. общее правило выше). Основной способ для агентов получать готовые результаты.

### GET /meetings/{id}

```json
{
  "id": "meeting_001",
  "title": "Product Sync",
  "status": "completed",
  "summaryPath": "~/anybrief/.../summary.md",
  "zipPath": "~/anybrief/.../bundle.zip",
  "fallbackSummary": false
}
```

### GET /meetings/{id}/summary
Возвращает содержимое summary как текст (Content-Type: `text/markdown`).

### GET /meetings/{id}/transcript
Возвращает содержимое merged transcript. По умолчанию JSON; с `?format=txt` — plain text.

### GET /logs/recent

С пагинацией. Параметры: `?level=info|warn|error`, `?since=<iso-timestamp>`.

### GET /settings

Возвращает текущие настройки **без секретов** (секретные поля возвращаются как `"***"` или `null`).

### PUT /settings

Принимает частичный patch. Секреты (`summaryApiKey`, `caldavPassword`, `localApiKey`)
отправляются в отдельных специальных полях и записываются в Keychain,
а не в `settings.json`. Поля только для чтения (`apiBindHost`) игнорируются.

Response: обновлённый объект (снова без секретов).

## Rate limiting

Локальный API не ограничивает RPS по умолчанию, но:
- `/recording/start` — не чаще 1 раза в 2 секунды (защита от случайных double-tap)
- `/calendar/sync` — не чаще 1 раза в 10 секунд
