feat(frontend): add editor integration shell and draft read-model views

- add editor integration shell
- add editor context read flow
- add draft flow entry handling
- add summary, structure, validation and compare-preview views
- render backend read models for draft and published states
- verify sample-contract entities: 3 seats, 1 group, 1 sector
This commit is contained in:
greebo
2026-03-20 18:51:21 +03:00
parent 89e52e3193
commit 796f0f4af1
20 changed files with 2836 additions and 187 deletions

View File

@@ -0,0 +1,528 @@
# Backend Integration Contract
This document is the frontend handoff contract for the `svg-service` backend. It is written as an integration baseline, not as an internal backend README.
## 1. Base URL and Auth
- Base URL: `http://<host>:9020`
- API prefix: `/api/v1`
- Auth header: `X-API-Key`
All non-`/healthz` routes require an API key.
Auth failure contract:
- missing API key -> `401` with string detail: `Missing API key`
- invalid API key -> `403` with string detail: `Invalid API key`
- valid non-admin key on admin-only route -> `403` with string detail: `Admin role required`
## 2. Roles and Access Boundaries
- `admin`
- full access to protected routes
- required for all `/api/v1/admin/...` routes
- `operator`
- allowed on non-admin protected routes
- denied on admin-only routes
- `viewer`
- allowed on non-admin protected routes
- denied on admin-only routes
Frontend implication:
- admin UI must treat admin routes as optional capabilities gated by role
- frontend must not assume `operator` or `viewer` can call cleanup, audit, backfill, or current-artifact admin routes
## 3. Core Entities
### Upload
Represents one uploaded SVG source and its normalized/sanitized artifacts.
Important fields:
- `upload_id`
- `original_filename`
- `content_type`
- `size_bytes`
- `original_storage_path`
- `sanitized_storage_path`
- `normalized_storage_path`
- `normalized_elements_count`
- `normalized_seats_count`
- `normalized_groups_count`
- `normalized_sectors_count`
### Scheme
Top-level business object created from upload.
Important fields:
- `scheme_id`
- `source_upload_id`
- `name`
- `status`
- `current_version_number`
- `published_at`
### Scheme Version
Versioned snapshot of the scheme structure and publish state.
Important fields:
- `scheme_version_id`
- `scheme_id`
- `version_number`
- `status`
- `normalized_storage_path`
- `normalized_*_count`
### Sector
Structure entity in a specific `scheme_version`.
Important fields:
- `sector_record_id`
- `sector_id`
- `element_id`
- `name`
Business identity priority:
- use `sector_id` when present
- fallback to `element_id`
- never treat `sector_record_id` as business identity across versions
### Group
Important fields:
- `group_record_id`
- `group_id`
- `element_id`
- `name`
Business identity priority:
- use `group_id` when present
- fallback to `element_id`
- never treat `group_record_id` as business identity across versions
### Seat
Important fields:
- `seat_record_id`
- `seat_id`
- `element_id`
- `sector_id`
- `group_id`
- `row_label`
- `seat_number`
Business identity priority:
- use `seat_id` when present
- fallback to `element_id`
- never treat `seat_record_id` as business identity across versions
### Pricing Category
Important fields:
- `pricing_category_id`
- `scheme_id`
- `name`
- `code`
### Price Rule
Important fields:
- `price_rule_id`
- `scheme_id`
- `pricing_category_id`
- `target_type`
- `target_ref`
- `amount`
- `currency`
### Artifact
Artifact registry row for generated backend files.
Important fields:
- `artifact_id`
- `artifact_type`
- `artifact_variant`
- `storage_path`
- `status`
- `meta_json`
Important artifact types currently exercised by regression:
- `sanitized_svg`
- `normalized_json`
- `display_svg`
- `publish_preview`
## 4. Lifecycle State Machine
### Fresh Upload
Flow:
1. `POST /api/v1/schemes/upload`
2. backend creates:
- `upload`
- `scheme`
- initial `scheme_version`
- structure rows
- initial artifacts
Expected initial state:
- `scheme.status = draft`
- `scheme.current_version_number = 1`
- current version status = `draft`
### Current Draft
If current scheme/version is still draft:
- editor works directly against current version
- `draft/ensure` is idempotent
- `draft/ensure` returns `created=false`
### Ensure Draft From Published Current
If current scheme/version is published:
- `POST /api/v1/schemes/{scheme_id}/draft/ensure`
- backend creates a new draft version
- current pointer switches to the new draft
- version number increments
### Publish
Preconditions:
- current scheme is draft
- current version is draft
- publish readiness must be satisfied
Publish path:
1. optional `draft/pricing/snapshot`
2. `GET draft/publish-readiness`
3. optional `GET draft/publish-preview`
4. `POST /api/v1/schemes/{scheme_id}/publish`
Expected result:
- scheme becomes `published`
- current version becomes `published`
### Rollback
Path:
- `POST /api/v1/schemes/{scheme_id}/rollback`
Effect:
- current pointer switches to requested historical `version_number`
- scheme returns to `draft`
- target version becomes current editable draft
### Unpublish
Path:
- `POST /api/v1/schemes/{scheme_id}/unpublish`
Effect:
- current scheme becomes `draft`
- current version becomes `draft`
## 5. Editor Flow
### Entry Point
- `GET /api/v1/schemes/{scheme_id}/editor/context`
Use it first to decide whether:
- current draft can be edited directly
- or a new draft must be created from published current
Important response fields:
- `current_scheme_version_id`
- `current_version_number`
- `scheme_status`
- `scheme_version_status`
- `current_is_draft`
- `create_draft_available`
- `recommended_action`
### Draft Read Models
- `POST /api/v1/schemes/{scheme_id}/draft/ensure`
- `GET /api/v1/schemes/{scheme_id}/draft/summary`
- `GET /api/v1/schemes/{scheme_id}/draft/structure`
- `GET /api/v1/schemes/{scheme_id}/draft/validation`
- `GET /api/v1/schemes/{scheme_id}/draft/compare-preview`
Frontend should treat `draft/structure` as the main editable read model.
### Patch Operations
Supported flows:
- single seat patch
- bulk seat patch
- sector create/patch/delete
- group create/patch/delete
- repair references
- remap preview/apply
Frontend rule:
- always send `expected_scheme_version_id` when mutating or reading draft state after editor entry
### Stale Conflict Handling
If backend returns a stale or draft editability conflict:
- stop optimistic local mutation flow
- re-read:
- `editor/context`
- `draft/summary`
- `draft/structure`
Do not keep editing against stale cached `scheme_version_id`.
## 6. Pricing Flow
### Categories
- `GET /api/v1/schemes/{scheme_id}/pricing`
- `POST /api/v1/schemes/{scheme_id}/pricing/categories`
- `PUT /api/v1/schemes/{scheme_id}/pricing/categories/{pricing_category_id}`
- `DELETE /api/v1/schemes/{scheme_id}/pricing/categories/{pricing_category_id}`
### Rules
- `POST /api/v1/schemes/{scheme_id}/pricing/rules`
- `PUT /api/v1/schemes/{scheme_id}/pricing/rules/{price_rule_id}`
- `DELETE /api/v1/schemes/{scheme_id}/pricing/rules/{price_rule_id}`
### Read Models
- `GET /api/v1/schemes/{scheme_id}/pricing`
- `GET /api/v1/schemes/{scheme_id}/pricing/coverage`
- `GET /api/v1/schemes/{scheme_id}/pricing/unpriced-seats`
- `GET /api/v1/schemes/{scheme_id}/pricing/explain/{seat_id}`
- `GET /api/v1/schemes/{scheme_id}/pricing/rules/diagnostics`
- `GET /api/v1/schemes/{scheme_id}/current/seats/{seat_id}/price`
- `GET /api/v1/schemes/{scheme_id}/test/seats/{seat_id}`
Frontend rule:
- empty pricing on a fresh upload is valid
- do not treat `categories=[]` and `rules=[]` as backend failure
## 7. Publish Flow
Main endpoints:
- `POST /api/v1/schemes/{scheme_id}/draft/pricing/snapshot`
- `GET /api/v1/schemes/{scheme_id}/draft/publish-readiness`
- `GET /api/v1/schemes/{scheme_id}/draft/publish-preview`
- `POST /api/v1/schemes/{scheme_id}/publish`
Frontend sequencing rule:
1. ensure draft
2. mutate if needed
3. create/refresh pricing
4. build pricing snapshot
5. read publish readiness
6. read publish preview if UI needs preview surface
7. publish
## 8. Admin/Ops Flow
Admin-only endpoints:
- `GET /api/v1/admin/schemes/{scheme_id}/current/artifacts`
- `GET /api/v1/admin/schemes/{scheme_id}/current/validation`
- `POST /api/v1/admin/schemes/{scheme_id}/current/display/regenerate`
- `POST /api/v1/admin/display/backfill`
- `GET /api/v1/admin/artifacts/publish-preview/audit`
- `POST /api/v1/admin/artifacts/publish-preview/cleanup`
- `GET /api/v1/admin/schemes/{scheme_id}/pricing/categories/cleanup-preview`
- `POST /api/v1/admin/schemes/{scheme_id}/pricing/categories/cleanup`
Healthy publish-preview audit contract:
- `orphan_files_count = 0`
- `missing_files_for_db_rows_count = 0`
- `db_rows_count == disk_files_count`
Frontend implication:
- admin tools must not be shown as generally available functionality
- admin cleanup/destructive flows must be role-gated on the client and still handle backend `403`
## 9. Typed Error Catalog
### Auth
- `401` string detail: `Missing API key`
- `403` string detail: `Invalid API key`
- `403` string detail: `Admin role required`
### Lifecycle / Draft / Publish
- `stale_draft_version`
- `stale_current_version`
- `current_version_inconsistent`
- `draft_not_editable`
- `publish_not_ready`
### Editor Uniqueness / References
- `editor_uniqueness_error`
- `editor_reference_error`
- `duplicate_seat_id`
- `duplicate_seat_id_in_payload`
- `duplicate_sector_id`
- `duplicate_group_id`
- `duplicate_sector_element_id`
- `duplicate_group_element_id`
- `unknown_sector_id`
- `unknown_group_id`
- `unknown_sector_ids`
- `unknown_group_ids`
- `unknown_target_sector_id`
- `unknown_target_group_id`
- `business_identifier_nullification_forbidden`
### Pricing / Remap / Test
- `invalid_amount`
- `remap_filter_required`
- `test_preview_failed`
### Validation Report Codes
These appear inside validation report payloads rather than as top-level HTTP conflict codes:
- `duplicate_seat_ids`
- `missing_seat_contract`
- `seats_without_sector_or_group`
- `seats_without_price`
Frontend rule:
- do not parse only HTTP status
- always inspect structured `detail.code` when `detail` is an object
## 10. Frontend Obligations
- always handle auth failures `401` and `403`
- always handle stale/conflict responses on draft, publish, and lifecycle operations
- never treat `*_record_id` as stable cross-version business identity
- always prefer business ids:
- seat -> `seat_id`, fallback `element_id`
- sector -> `sector_id`, fallback `element_id`
- group -> `group_id`, fallback `element_id`
- re-read current/draft state after:
- any `409`
- publish
- rollback
- unpublish
- `draft/ensure` returning a newly created draft
- do not assume current version remains stable across concurrent operator sessions
- do not assume publish-preview artifacts or display artifacts are frontend-owned resources
## 11. Non-Persistent Assumptions Frontend Must Avoid
The frontend must not assume that these remain stable forever:
- `scheme_version_id`
- `seat_record_id`
- `sector_record_id`
- `group_record_id`
- artifact `storage_path`
- publish-preview cache artifacts
These are safe to treat as business-stable:
- `scheme_id`
- `version_number` within one scheme
- `seat_id` when present
- `sector_id` when present
- `group_id` when present
## 12. Known Limitations / Deferred Tech Debt
- some lifecycle negative contracts still return mixed styles:
- typed object conflicts for `409`
- plain string details for some `404` and auth cases
- validation warnings and error code families are not yet unified into one single global error envelope
- admin/ops routes are backend-internal tools, not end-user product APIs
- corruption remediation smoke exists only for `publish_preview`, not for every artifact type
## 13. Regression Baseline Frontend Can Rely On
The frontend can rely on the following regression-backed flows:
- fresh upload on clean DB
- current/draft/editor read flow
- editor mutations and stale draft protection
- pricing setup and publish flow
- version lifecycle:
- publish
- ensure draft from published current
- rollback
- unpublish
- admin ops:
- audit
- cleanup
- destructive pricing cleanup for safe fixture categories
- full admin permission matrix on implemented admin endpoints
- controlled `publish_preview` corruption detection and remediation
- negative upload validation
- negative auth matrix
- negative lifecycle matrix
## 14. Recommended Frontend Integration Sequence
For normal editor work:
1. authenticate
2. upload or pick `scheme_id`
3. read `editor/context`
4. call `draft/ensure` if needed
5. read `draft/structure`
6. mutate using current `scheme_version_id`
7. on `409`, reload editor state before retry
8. configure pricing if needed
9. create pricing snapshot
10. read publish readiness / preview
11. publish
For admin UI:
1. verify admin role in client auth state
2. call admin endpoints
3. still handle backend `403`
4. treat cleanup and remediation as explicit operator actions, not background automation