0eb2b0f697
- backend/db/schema.sql: all 12 tables from DATA-MODEL.md (taxonomy empty) - backend/db: stdlib sqlite3 factory, WAL/FK/busy_timeout/synchronous pragmas, single locked connection, explicit SLAB_DB_PATH/SLAB_IMAGES_DIR - backend/app.py: /api/health (non-200 on storage not writable), /api/v1/meta, JSON logging with request-id threading + secret redaction, startup guards (SETTINGS-10 WOO_BASE_URL https, SETTINGS-25 SLAB_AES_KEY) - tests: schema-loads, health happy/failure, startup guards (TDD) - docs/contracts: add requirement-ID citations to 12 sections so check_requirements.py passes (pre-existing gate debt) Gates: check_requirements.py + check_coverage.py exit 0; pytest 5 passed
208 lines
11 KiB
Markdown
208 lines
11 KiB
Markdown
# App API contract — client ↔ FastAPI
|
||
|
||
Status: SPEC · 2026-09-18.
|
||
Base path `/api/v1`. Health at `/api/health`. All JSON unless noted. Timestamps UTC ISO-8601.
|
||
The outbound WooCommerce payload is owned by `docs/contracts/WOO-API.md`.
|
||
|
||
## Requirements impact
|
||
|
||
| Requirement | Impact on this design |
|
||
|---|---|
|
||
| APP-5 | slab status values and transitions |
|
||
| APP-20 | client ids, UTC timestamps, `client_rev`/`server_rev` |
|
||
| APP-25 | FastAPI is the only Woo and inference caller |
|
||
| APP-30 | `/api/v1` base path, `/api/health` |
|
||
| APP-35 | error envelope, full lists, JSON |
|
||
| APP-40 | server stores client numbers, never recomputes |
|
||
| APP-45 | health body and non-200 condition |
|
||
| APP-50 | upstream Woo 4xx mapped into the envelope |
|
||
| CAPTURE-15, CAPTURE-165 | offline and U2Net are out of scope |
|
||
| CAPTURE-25, CAPTURE-30, CAPTURE-115, CAPTURE-130 | calibration choice and batch photos |
|
||
| CAPTURE-70, CAPTURE-80, CAPTURE-85, CAPTURE-90 | length/thickness units and derived numbers |
|
||
| CAPTURE-95, CAPTURE-105, CAPTURE-135, CAPTURE-141, CAPTURE-145, CAPTURE-150, CAPTURE-155 | PNG output, upload, preview/controls |
|
||
| METADATA-5, METADATA-10, METADATA-15, METADATA-30 | taxonomy cardinality and scoping |
|
||
| METADATA-35, METADATA-40, METADATA-45, METADATA-50, METADATA-55 | Call 1 / Call 2 |
|
||
| METADATA-60, METADATA-65, METADATA-70, METADATA-75, METADATA-100, METADATA-105, METADATA-110, METADATA-115 | context, constraints, fallback, prompts, failure |
|
||
| OPS-55 | no client auth |
|
||
| PUBLISH-5, PUBLISH-10, PUBLISH-15, PUBLISH-20 | create sequence, draft rule, duplicate, stale |
|
||
| PUBLISH-25, PUBLISH-30, PUBLISH-31, PUBLISH-35, PUBLISH-40, PUBLISH-45, PUBLISH-50, PUBLISH-55, PUBLISH-60 | create outcomes, images, payload |
|
||
| REVIEW-5, REVIEW-10, REVIEW-15, REVIEW-20, REVIEW-25, REVIEW-30, REVIEW-40, REVIEW-45, REVIEW-50 | ready set, fields, limits |
|
||
| SETTINGS-5, SETTINGS-10, SETTINGS-15, SETTINGS-20, SETTINGS-25, SETTINGS-30 | env URL, credentials, crypto |
|
||
| SETTINGS-35, SETTINGS-40, SETTINGS-45, SETTINGS-50, SETTINGS-51, SETTINGS-55, SETTINGS-60, SETTINGS-65, SETTINGS-75, SETTINGS-80, SETTINGS-85, SETTINGS-90 | rates, tests, taxonomy, flags |
|
||
|
||
## 1. Conventions
|
||
|
||
- **Auth:** no client auth; the app relies on the trusted LAN/Tailscale network plus HTTPS.
|
||
- **Envelope:** every error is `{ "error": { "code": string, "message": string, "detail": object|null } }`.
|
||
- **Lists:** returned in full, no pagination.
|
||
- **Revisions:** create/update carry `client_rev`; the server echoes `server_rev`; a mismatch is
|
||
`409 revision_conflict`. A background inference write bumps `server_rev`; the client adopts the
|
||
polled `server_rev` before its next write.
|
||
- **Async work:** inference and publish run through one in-process job queue with a single
|
||
consumer; a status transition is compare-and-set inside one write transaction, so a poll never
|
||
observes a half-applied transition. Tests run the queue inline so contract tests are
|
||
deterministic.
|
||
- **IDs and time:** slab ids are client-generated UUIDv4; timestamps are UTC ISO-8601.
|
||
- **Photos:** `multipart/form-data` with `data` (JSON), `files[]` (original images), and `meta[]`
|
||
(`{ "seq": n }`). One photo is the calibration photo, chosen by the owner.
|
||
- **Secrets:** never returned; only a configured boolean.
|
||
- **Error display:** raw status codes, stack traces, and internal codes never reach the owner.
|
||
|
||
## 2. Error codes
|
||
|
||
| Status | Code | Meaning |
|
||
|---|---|---|
|
||
| 400 | `validation` | payload failed validation |
|
||
| 404 | `not_found` | unknown slab, photo, or setting |
|
||
| 409 | `revision_conflict` | `client_rev`/`server_rev` mismatch; `detail.current_server_rev` |
|
||
| 409 | `duplicate_sku` | SKU exists in Woo under any status |
|
||
| 422 | `unpublishable` | slab not publishable or missing a required field |
|
||
| 422 | `stale_field` | a picked taxonomy id vanished; returns current options |
|
||
| 502 | `woo_error` | WooCommerce rejected the request; `detail` carries the Woo body |
|
||
| 500 | `internal` | unexpected |
|
||
|
||
There is no `412 validation_module_stale` path.
|
||
|
||
## 3. Health and meta
|
||
|
||
- `GET /api/health` → `{ "status": "ok", "version": string, "woo_reachable": bool }`. Non-200 when
|
||
the database or image volume is not writable.
|
||
- `GET /api/v1/meta` → `{ "version": string }`.
|
||
|
||
## 4. Slabs
|
||
|
||
### `POST /api/v1/slabs`
|
||
Multipart create of a calibrated draft. Returns `201` with the slab at `status: "calibrated"`.
|
||
Stores the client-sent derived numbers and never recomputes them. Auto-runs Call 1 only when
|
||
`inference_enabled` is true; inferred values persist only at confidence ≥ 0.7. `409 duplicate_sku`
|
||
if the SKU already exists. Cites APP-20, CAPTURE-135, METADATA-45, PUBLISH-15.
|
||
|
||
`SlabCreate` fields:
|
||
|
||
| Field | Type | Notes |
|
||
|---|---|---|
|
||
| `id` | uuid | client-generated |
|
||
| `sku` | string | 3–24, alphanumeric and hyphen |
|
||
| `length_in`, `thickness_in` | number | 1/8 inch steps |
|
||
| `sqft`, `bdft`, `width_min_in`, `width_max_in`, `width_avg_in` | number | client-computed |
|
||
| `species_id` | int | one species leaf |
|
||
| `wood_category_ids` | int[] | one or more |
|
||
| `edge_type_term_id` | int | one |
|
||
| `figure_term_ids` | int[] | one or more |
|
||
| `grade_term_id` | int | exactly one |
|
||
| `thickness_term_id` | int | round-up band |
|
||
| `moisture_term_id` | int | default kiln-dried |
|
||
| `fig_tag_ids` | int[] | optional |
|
||
| `feat_tag_ids` | int[] | optional |
|
||
| `price` | number | 2 decimals |
|
||
| `price_source` | string | `recommendation` \| `override` |
|
||
| `title`, `short_description`, `description` | string\|null | prose |
|
||
| `content_source` | string | `manual` \| `llm` \| `template` |
|
||
| `client_rev` | int | optimistic concurrency |
|
||
|
||
Grade, edge, thickness, and moisture are singular; figure and the tag lists are arrays.
|
||
|
||
### `GET /api/v1/slabs`
|
||
Returns `[Slab]`, most recent first.
|
||
|
||
### `GET /api/v1/slabs/{id}`
|
||
Returns the `Slab`. Also the poll endpoint for inference and publish. The client backs off
|
||
(1s, 2s, 4s, cap 10s). On publish success the terminal response carries `woo_product_id`; the
|
||
working row is then discarded, so a later GET may return `404`, which the client treats as
|
||
completion.
|
||
|
||
### `PUT /api/v1/slabs/{id}`
|
||
Partial update of editable fields plus `client_rev`. Stores client-sent derived fields and does
|
||
not recompute them. Checks run in order: `409 revision_conflict`, then `409 duplicate_sku` (SKU
|
||
field actions `edit_sku` / `open_existing`), then `422 stale_field` (current options included).
|
||
`status` becomes `ready` only when the full required set is present; it is server-derived and a
|
||
client never sets it. Cites APP-20, REVIEW-40, PUBLISH-15, PUBLISH-20.
|
||
|
||
### `DELETE /api/v1/slabs/{id}`
|
||
Soft delete of unpublished drafts only (sets `deleted_at`); `204`. Deleted rows are excluded from
|
||
all GET responses. Published slabs cannot be deleted from the app.
|
||
|
||
## 5. Photos
|
||
|
||
- `GET /api/v1/slabs/{id}/photos/{pid}/original` → `image/*` while retained.
|
||
- `GET /api/v1/slabs/{id}/photos/{pid}/processed` → `image/*` (transparent PNG) while retained.
|
||
- After publish both return `410 Gone`.
|
||
- `POST /api/v1/slabs/{id}/u2net/{pid}` is reserved post-POC and is not exposed.
|
||
|
||
## 6. Inference
|
||
|
||
### `POST /api/v1/slabs/{id}/infer-taxon`
|
||
Runs or retries Call 1. Sends the clean processed photos at 1024 px long edge or less, the synced
|
||
taxonomy, and the user metadata. Returns `202`; the client polls. A failure or timeout follows the
|
||
runtime failure rule (recoverable offers Retry, unrecoverable explains).
|
||
|
||
### `POST /api/v1/slabs/{id}/infer-content`
|
||
Runs Call 2 on user action only. Fresh request carrying the processed photos, the review-confirmed
|
||
metadata, the primary and secondary wood categories, the parent-category path, the dimension fact
|
||
block, and brand/geo context; no provider thread id and no raw Call 1 result. Requires
|
||
`test-inference` `ok`. Returns `202`; the client polls. The model writes the long description only
|
||
and never touches dimensions. Cites METADATA-55, METADATA-65.
|
||
|
||
Call 1 result fields: `species_id`, `species_confidence`, `wood_category_ids`,
|
||
`edge_type_term_id`, `figure_term_ids`, `figure_confidence`, `grade_term_id`, `grade_confidence`,
|
||
`character`, `inclusions`, `voids`, `checks`. Free-text observations map to synced `feat-*` tags
|
||
only.
|
||
|
||
Call 2 result field: `description`. The name and short description are assembled by the server from
|
||
the primary wood category's patterns, not returned by the call.
|
||
|
||
## 7. Publish
|
||
|
||
### `POST /api/v1/slabs/{id}/publish`
|
||
Preconditions are the full ready set, else `422 unpublishable` with field keys. Sequence: validate
|
||
the ready set; look up the SKU and stop if present; refresh taxonomy when the last sync is over
|
||
one hour old; resolve categories, tags, and attributes; upload the processed PNGs to Woo media;
|
||
`POST /products` per `docs/contracts/WOO-API.md`; finalize. `SLAB-UAT-*` always creates a draft.
|
||
Returns `202`; the client polls. Cites PUBLISH-5, PUBLISH-10, PUBLISH-15, PUBLISH-25.
|
||
|
||
- Success: `woo_product_id` set and returned on the terminal poll, photos purged, the local working
|
||
slab row discarded, device draft cleared. Nothing is deleted from WooCommerce.
|
||
- Failure: attempt marked failed, photos retained, `sync_log` written, plain message on the
|
||
affected field or action, retry from the same screen.
|
||
- Partial failure (media uploaded, product POST failed): orphaned media left, logged, manual
|
||
cleanup note surfaced.
|
||
|
||
## 8. Taxonomy
|
||
|
||
- `GET /api/v1/admin/taxonomy` → grouped categories, attributes with terms, and tags, plus
|
||
`taxonomy_anchor` and `taxonomy_last_sync_at`. Only fig-* and feat-* tag families are kept.
|
||
These lists are the only legal picker options.
|
||
- `POST /api/v1/admin/taxonomy/sync` → pulls from Woo, rebuilds the cache in one transaction, sets
|
||
`taxonomy_last_sync_at`, bumps `taxonomy_anchor` only when the stored-subset hash changes, and
|
||
on failure returns `502` while keeping the previous cache.
|
||
|
||
## 9. Pricing
|
||
|
||
- `GET /api/v1/admin/pricing` → `[PriceRule]`
|
||
- `POST /api/v1/admin/pricing` → `201`
|
||
- `PUT /api/v1/admin/pricing/{id}` → `200`
|
||
- `DELETE /api/v1/admin/pricing/{id}` → `204`
|
||
|
||
One flat `price_per_bdft` per species leaf, no tiers.
|
||
|
||
## 10. Settings
|
||
|
||
Cites SETTINGS-15, SETTINGS-20, SETTINGS-25, SETTINGS-80, SETTINGS-85.
|
||
|
||
- `GET /api/v1/settings` → `SettingsView` with secrets masked.
|
||
- `PUT /api/v1/settings` — partial update. `woo_base_url` is not writable; the server rejects the
|
||
write. Woo credentials are write-only and stored AES-GCM. Inference base URL and key are
|
||
encrypted; flags, brand voice, and geo context are settings. Secrets are never echoed.
|
||
- `POST /api/v1/settings/test-woo` → `{ "ok": bool, "detail": string, "taxonomy_anchor": string,
|
||
"taxonomy_last_sync_at": string }`. Fails closed on missing credentials or non-HTTPS; an auth
|
||
rejection is distinct from an unreachable store; on success always resyncs.
|
||
- `POST /api/v1/settings/test-inference` → `{ "ok": bool, "mode": "stateful"|"stateless"|"fail",
|
||
"vision_capable": bool, "detail": string }`. `fail` blocks Call 2 only.
|
||
|
||
## 11. Removed from v1
|
||
|
||
- The shared validation module and its `412` path.
|
||
- Provider thread ids and `previous_response_id` (Call 2 is a fresh full-context call).
|
||
- The U2Net control; the endpoint is reserved only.
|
||
- Offline/IndexedDB sync.
|