diff --git a/docs/integration/api/pending-endpoints.md b/docs/integration/api/pending-endpoints.md new file mode 100644 index 0000000..e003b8c --- /dev/null +++ b/docs/integration/api/pending-endpoints.md @@ -0,0 +1,354 @@ +# 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). + +--- + +## 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). diff --git a/docs/toc.yaml b/docs/toc.yaml index 5481108..0d3b7ca 100644 --- a/docs/toc.yaml +++ b/docs/toc.yaml @@ -29,6 +29,8 @@ items: href: integration/api/best-practices.md - name: Bundle справочников cuod href: integration/api/bundle-cuod.md + - name: Pending endpoints (admin-UI wishlist) + href: integration/api/pending-endpoints.md - name: Прикладные сценарии items: