# Pending endpoints — frontend wishlist

Эти 5 endpoint'ов нужны admin-UI для завершения UI-фич, заплейсхолженных в
sprint UI polish (MR !74→!93, mai-2026). Каждый блокирует конкретный
component на frontend — сегодня там показывается placeholder ("backend
changelog pending" notice / disabled button / `0` count).

Frontend готов подключить через Orval — нужны только OpenAPI спеки.
Приоритет в порядке user-impact: 1 (highest) → 5 (nice-to-have).

## Status (2026-05-11)

| # | Endpoint | Status |
|---|---|---|
| 1 | `GET /dictionaries/{name}/changelog` | ✅ shipped (MR !96) |
| 2 | `GET /dictionaries/{name}/snapshots` | ✅ shipped (MR !97) |
| 3 | Workflow API (4 endpoints) | ⏸ deferred — separate design + sprint |
| 4 | `PATCH /dictionaries/{name}/schema` | ✅ shipped (MR !98) |
| 5 | `GET /dictionaries/graph/outgoing` | ✅ shipped (MR !95) |

**4/5 endpoints landed** в session 2026-05-11 backend sprint. Workflow API (#3)
требует отдельной design discussion — state machine + RBAC pool model
parallel record-level drafts (см. `DraftController.java` для record-level
draft architecture как reference). Frontend WorkflowBanner currently рендерит
placeholder banner — кнопки скрыты пока endpoints не shipped.

---

## 1. Schema versions timeline — `GET /api/v1/dictionaries/{name}/changelog`

**Purpose:** History tab в editor (`/dictionaries/$name?tab=history`) и
Time-travel modal — список всех schema versions с change summaries.

**Frontend usage:**

- `HistoryTabContent` (`routes/dictionaries.$name.tsx:HistoryTabContent`)
  показывает HEAD row + N исторических versions с `v{N} +X ~Y −Z`
  change summary, who/when, diff link.
- `TimeTravelModal` использует timestamps versions для slider marks.

**Route:** `GET /api/v1/dictionaries/{name}/changelog`

**Query params:**

| param | type | default | description |
|---|---|---|---|
| `limit` | int | 50 | max entries (cap 200) |
| `cursor` | string | — | opaque pagination token (next-page link) |
| `from` | ISO datetime | — | filter versions с `publishedAt >= from` |
| `to` | ISO datetime | — | filter versions с `publishedAt <= to` |

**Response 200:**

```json
{
  "dictionary": "satellites",
  "currentVersion": "1.4.0",
  "entries": [
    {
      "version": "1.4.0",
      "publishedAt": "2026-04-12T10:23:00Z",
      "publishedBy": { "sub": "u-42", "name": "zimin.an" },
      "headers": "x-references массив + поле altitude",
      "diff": {
        "added":   ["properties.altitude"],
        "removed": [],
        "changed": ["properties.operator (enum +1 value)"]
      },
      "recordsAffected": 0,
      "isCurrent": true
    },
    {
      "version": "1.3.2",
      "publishedAt": "2026-03-30T08:11:00Z",
      "publishedBy": { "sub": "u-17", "name": "petrov.iv" },
      "headers": "fix description перевод EN",
      "diff": { "added": [], "removed": [], "changed": ["description.en"] },
      "recordsAffected": 0,
      "isCurrent": false
    }
  ],
  "nextCursor": "eyJ2IjoiMS4zLjEiLCJ0IjoiMjAyNi0wMy0xNVQwMDowMDowMFoifQ=="
}
```

**Errors:** 404 dict not found, 401 unauth.

**Notes:**
- `diff` — best-effort summary, не полный JSON-patch. Если backend
  считает дорого — допустим только counts: `{ "added": 3, "removed": 0, "changed": 2 }`.
- `headers` ≈ commit message от автора schema edit. Optional; покажем
  "—" если empty.

**Complexity:** medium. Нужна таблица `dictionary_schema_history` (если
ещё нет — версия + JSON snapshot + author + timestamp).

---

## 2. Records snapshots — `GET /api/v1/dictionaries/{name}/snapshots`

**Purpose:** Time-travel modal slider marks — точки во времени где
дествительно произошёл change в records (не каждая секунда). UX:
slider snaps к "интересным" моментам.

**Frontend usage:**

- `TimeTravelModal` (`components/timetravel/TimeTravelModal.tsx`) рендерит
  marks на slider — каждая mark = snapshot point. Сейчас используем
  `recordTimestamps` derived из `validFrom` — но это не учитывает closes.

**Route:** `GET /api/v1/dictionaries/{name}/snapshots`

**Query params:**

| param | type | default | description |
|---|---|---|---|
| `from` | ISO datetime | now − 30d | start of window |
| `to` | ISO datetime | now | end of window |
| `granularity` | string | `day` | `hour` / `day` / `week` — bucket size |

**Response 200:**

```json
{
  "dictionary": "satellites",
  "from": "2026-04-01T00:00:00Z",
  "to": "2026-05-11T00:00:00Z",
  "snapshots": [
    {
      "at": "2026-04-01T00:00:00Z",
      "totalRecords": 124,
      "changedSinceLast": 0,
      "label": "1 апр"
    },
    {
      "at": "2026-04-15T14:00:00Z",
      "totalRecords": 127,
      "changedSinceLast": 3,
      "label": "15 апр"
    },
    {
      "at": "2026-05-11T00:00:00Z",
      "totalRecords": 130,
      "changedSinceLast": 5,
      "label": "сейчас",
      "isCurrent": true
    }
  ]
}
```

**Notes:**
- Backend free выбрать какие точки маркировать — main критерий ≥1 change
  с прошлой snapshot. Пустые часы/дни skip.
- `label` — backend-formatted RU label для UI (избегаем locale logic
  на front).

**Complexity:** medium. Можно делать `SELECT DISTINCT date_trunc('day', valid_from)`
+ count(*) over window. Кеш ~5min на горячих dict'ах.

---

## 3. Workflow state machine — workflow API

**Purpose:** Workflow buttons в editor toolbar (`Request review`,
`Approve`, `Publish`, `Create draft`). Сейчас показываются placeholder /
hidden когда `approvalRequired=true` потому что нет actions.

**Frontend usage:**

- `WorkflowBanner` (`components/editor/WorkflowBanner.tsx`) показывает
  "live" / "pending review" / "draft" badge.
- Action buttons прячутся пока нет API. После shipping — wire'им
  кнопки в banner / editor toolbar.

**Routes (4 endpoints, RESTful):**

### 3.1 `POST /api/v1/dictionaries/{name}/drafts`

Create draft от текущего HEAD. Returns draft id.

```json
{ "fromVersion": "1.4.0", "reason": "обновить altitude диапазон" }
```

Response:
```json
{ "draftId": "d-42a7", "branchedFrom": "1.4.0", "status": "draft" }
```

### 3.2 `POST /api/v1/dictionaries/{name}/drafts/{draftId}/review`

Submit draft for review.

```json
{ "reviewers": ["u-17", "u-92"], "note": "проверьте altitude rounding" }
```

Response: 202 Accepted, `{ "status": "review_pending" }`.

### 3.3 `POST /api/v1/dictionaries/{name}/drafts/{draftId}/decision`

Reviewer's decision.

```json
{ "decision": "approve", "comment": "OK" }
```

`decision`: `approve` | `request_changes` | `reject`. Response 200 с new status.

### 3.4 `POST /api/v1/dictionaries/{name}/drafts/{draftId}/publish`

Publish approved draft → becomes new HEAD. Optimistic-lock на текущую version.

```json
{ "expectedHeadVersion": "1.4.0", "publishNote": "v1.5.0 — altitude диапазон" }
```

Response 200:
```json
{ "newVersion": "1.5.0", "publishedAt": "2026-05-11T22:00:00Z" }
```

409 если кто-то другой опубликовал свой draft первым (concurrent publish race).

**Notes:**
- State machine: `draft` → `review_pending` → (`approved` | `changes_requested` | `rejected`) → (publish | discard).
- Все 4 endpoint'а должны вернуть updated draft state — frontend invalidate'ит
  query keys по dictionary name.
- RBAC: только approvers могут вызвать 3.3 с decision=approve.

**Complexity:** high. Нужны таблицы `drafts`, `draft_reviews`, state transition
guards. Webhook event hooks (frontend ожидает `dict.draft.*` events).

---

## 4. PATCH schema fields — `PATCH /api/v1/dictionaries/{name}/schema`

**Purpose:** Inline edit single field в Fields tab без открытия full
DictionaryEditorDialog. UX: clickable field row → inline form → save →
HEAD bumps minor version.

**Frontend usage:**

- `FieldsTabContent` (`routes/dictionaries.$name.tsx:FieldsTabContent`)
  сейчас read-only. После endpoint — кнопка edit на каждой row.

**Route:** `PATCH /api/v1/dictionaries/{name}/schema`

**Body (RFC 6902 JSON-patch subset или structured ops — backend выбирает):**

Option A — JSON Patch:
```json
{
  "expectedHeadVersion": "1.4.0",
  "ops": [
    {
      "op": "replace",
      "path": "/properties/altitude/description",
      "value": { "ru": "Высота орбиты в км", "en": "Orbit altitude in km" }
    },
    { "op": "add", "path": "/properties/altitude/minimum", "value": 0 }
  ]
}
```

Option B — structured (более ergonomic для UI):
```json
{
  "expectedHeadVersion": "1.4.0",
  "field": "altitude",
  "changes": {
    "description": { "ru": "...", "en": "..." },
    "minimum": 0
  }
}
```

**Constraints:** допустимые ops — non-breaking changes только (description,
title, minimum/maximum bounds expansion, enum value добавление). Breaking
ops (rename, type change, remove) → 422 с `code: "breaking_change_requires_draft"`.

**Response 200:**

```json
{
  "newVersion": "1.4.1",
  "schemaJson": { /* full updated schema */ }
}
```

**Errors:**

| code | status | meaning |
|---|---|---|
| `breaking_change_requires_draft` | 422 | op требует workflow draft, не inline patch |
| `version_mismatch` | 409 | someone else patched schema first (refetch) |
| `validation_failed` | 422 | op breaks existing record validation |

**Complexity:** medium-high. Нужен validator который проверяет что patch
не breaks существующие записи (semver-minor только).

---

## 5. Batch outgoing FK counts — `GET /api/v1/dictionaries/graph/outgoing`

**Purpose:** Catalog list (`/dictionaries`) — колонка `→` показывает
сколько FK rel'ов идёт ОТ каждого dict'а. Сейчас на front считаем
loop'ом per-row через `extractOutgoingFkCount(schemaJson)` — работает,
но требует все schemaJson на клиенте.

**Frontend usage:**

- `routes/dictionaries.index.tsx` catalog table — `→` column показывает
  outgoing FK count. Если backend вернёт батчом — frontend сделает 1
  request вместо включения schemaJson в каждую row useDictionaries.

**Route:** `GET /api/v1/dictionaries/graph/outgoing?dicts=satellites,operators,...`

**Query params:**

| param | type | default | description |
|---|---|---|---|
| `dicts` | comma-separated string | all visible | optional filter |
| `scope` | comma-separated string | all | `PUBLIC,INTERNAL` etc |

**Response 200:**

```json
{
  "outgoing": {
    "satellites":  { "fkCount": 3, "targets": ["operators", "launch_sites", "missions"] },
    "operators":   { "fkCount": 0, "targets": [] },
    "missions":    { "fkCount": 2, "targets": ["operators", "rockets"] }
  }
}
```

**Notes:**
- `targets` опционально — если дорого, можно вернуть только `fkCount`.
- Скорость: вся таблица обычно ≤200 dict'ов — простой SELECT с
  `jsonb_array_length(schema_json->'x-references')` или `LATERAL`
  enumeration. Кеш 60s.

**Complexity:** low. Это derived data из существующих schemaJson —
просто batch SELECT + aggregate.

---

## Frontend integration timeline

| Endpoint | Component | Estimate after backend ship |
|---|---|---|
| 1. Changelog | `HistoryTabContent`, `TimeTravelModal` | ~2h (Orval hook + render entries) |
| 2. Snapshots | `TimeTravelModal` slider marks | ~1h (replace mock derivation) |
| 3. Workflow API | `WorkflowBanner` + new buttons in toolbar | ~6h (4 mutations + state UI) |
| 4. PATCH schema | `FieldsTabContent` inline edit | ~4h (form + diff preview) |
| 5. Batch graph | `dictionaries.index` `→` column | ~30min (single query swap) |

Total frontend work post-backend: **~14h** (1.5 sprint days). Per
endpoint frontend готов в течение часа после OpenAPI freeze.

## Conventions reminder

- Все endpoints должны быть Authenticated (Keycloak JWT, см. [auth.md](auth.md)).
- Errors следуют [errors.md](errors.md) — `{ code, message, details? }`.
- Optimistic-lock 409 везде где есть concurrent edits.
- Webhook events для async state changes — см. [webhooks.md](webhooks.md).
- Idempotency-Key header на mutating endpoints (Time-travel race
  safety per existing API guidelines).
