Files
SlabUploader/docs/DATA-MODEL.md
T
Ty 4e97770373 docs: lock revised capture pipeline and hybrid architecture
Rewrite PRD, tech spec, data model, and OpenAPI around the 2026-08-27
decisions: client-side mask/measure/crop, U2Net on demand, Woo as product
source of truth, draft-only local state. Architecture records topology C
through the status machine. Drop the old CONTENT-WOO spec until the
Call 2 prompt review finishes.
2026-08-27 19:26:45 -07:00

221 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Data Model
Status: SPEC · 2026-08-27 · SlabUploader
Storage: single SQLite file. Schema portable to Postgres.
Conventions:
- IDs: client-generated UUIDv4 for slabs so the PWA can reference a slab before it exists server-side.
- Timestamps: UTC ISO-8601 TEXT.
- Soft state via status column; no hard deletes for slabs (audit).
- Money: float64 USD, rounded half-up to cents at write.
- All rounded values are what gets stored. No full-precision internal storage.
- Inches stored as 1/8" increments. Sqft/bdft to 2 decimals.
- Multi-select fields stored as JSON arrays of Woo IDs.
---
## slabs
Draft-only. Deleted on successful publish. WooCommerce is the source of truth for product data.
| Column | Type | Notes |
|---|---|---|
| `id` | TEXT PK | client-generated UUID |
| `sku` | TEXT UNIQUE NOT NULL | 3-24 [A-Z0-9-] |
| `status` | TEXT NOT NULL | draft \| calibrated \| ready \| publishing \| published \| failed |
| `length_in` | REAL NOT NULL | 1/8" step |
| `thickness_in` | REAL NOT NULL | 1/8" step |
| `sqft` | REAL | 2 decimals |
| `bdft` | REAL | 2 decimals |
| `width_min_in` | REAL | min width at 6" stations, 2 decimals |
| `width_max_in` | REAL | max width at 6" stations, 2 decimals |
| `width_avg_in` | REAL | avg width at 6" stations, 2 decimals |
| `species_id` | INTEGER | → woo_categories (leaf only, 1 required) |
| `wood_category_ids` | INTEGER[] JSON | → woo_categories (leaf, 1+) |
| `edge_type_term_id` | INTEGER | → woo_attribute_terms (1 required) |
| `figure_term_ids` | INTEGER[] JSON | → woo_attribute_terms (1+) |
| `grade_term_ids` | INTEGER[] JSON | → woo_attribute_terms (1+) |
| `thickness_term_id` | INTEGER | → woo_attribute_terms (round-up) |
| `moisture_term_id` | INTEGER | → woo_attribute_terms (1 required, defaults to kiln-dried term; user overrides; not inferred) |
| `fig_tag_ids` | INTEGER[] JSON | → woo_tags (fig-*, 1+) |
| `price_per_bdft` | REAL | from pricing_rules for species |
| `price` | REAL NOT NULL | 2 decimals |
| `price_source` | TEXT | 'recommendation' \| 'override' |
| `title` | TEXT | |
| `short_title` | TEXT | |
| `description` | TEXT | |
| `content_source` | TEXT | 'manual' \| 'llm' |
| `client_rev` | INTEGER NOT NULL DEFAULT 1 | |
| `server_rev` | INTEGER NOT NULL DEFAULT 1 | |
| `created_at` | TEXT NOT NULL | |
| `updated_at` | TEXT NOT NULL | |
Indexes: `sku` (unique), `status`, `updated_at`.
### Status state machine
```
draft → calibrated → ready → publishing → published
│ │ ↑
└→ failed ────┴────────────────┘ (woo_product_id in sync_log)
any → failed (with error)
```
1. **draft** — photos taken, no mask confirmed.
2. **calibrated** — BG edge confirmed, length axis confirmed, length/thickness/SKU entered. Client computed sqft/bdft/widths. Draft uploaded to server.
3. **ready** — all mandatory fields populated. Values from Call 1, manual, or mix. Inference is optional. Publishable.
4. **publishing** — Woo create in flight. Poll for result.
5. **published** — Woo product created. This row + all photos deleted. sync_log retained.
6. **failed** — pipeline, inference, or publish error. Error detail stored. Retry available.
---
## slab_photos
| Column | Type | Notes |
|---|---|---|
| `id` | TEXT PK | |
| `slab_id` | TEXT FK → slabs.id ON DELETE CASCADE | |
| `kind` | TEXT NOT NULL | 'inventory' |
| `role` | TEXT | 'topdown' \| 'extra' |
| `seq` | INTEGER | display order |
| `original_path` | TEXT | relative to images volume |
| `processed_path` | TEXT | relative to images volume |
| `created_at` | TEXT | |
Rule: only inventory photos. Calibration photos do not exist in this design.
---
## pricing_rules
| Column | Type | Notes |
|---|---|---|
| `id` | TEXT PK UUID | |
| `species` | TEXT NOT NULL | matches woo_categories.name (leaf) |
| `price_per_bdft` | REAL NOT NULL | flat, no tiers |
| `created_at` | TEXT | |
| `updated_at` | TEXT | |
`UNIQUE(species)`. Seeded from synced Woo categories. User sets price per species.
---
## woo_categories
| Column | Type | Notes |
|---|---|---|
| `woo_id` | INTEGER PK | |
| `name` | TEXT | |
| `slug` | TEXT | |
| `parent_id` | INTEGER | NULL for root |
| `synced_at` | TEXT | |
Only species + wood category. Sync pulls all Woo categories; the app filters to the two we use.
`UNIQUE(woo_id)`.
---
## woo_attributes
| Column | Type | Notes |
|---|---|---|
| `woo_id` | INTEGER PK | |
| `name` | TEXT | |
| `slug` | TEXT | |
| `type` | TEXT | 'select' |
| `synced_at` | TEXT | |
Only the five first-class attributes: Edge Type, Figure, Grade, Thickness, Moisture.
`UNIQUE(woo_id)`.
---
## woo_attribute_terms
| Column | Type | Notes |
|---|---|---|
| `woo_id` | INTEGER PK | |
| `attribute_id` | INTEGER FK → woo_attributes.woo_id | |
| `name` | TEXT | |
| `slug` | TEXT | |
| `synced_at` | TEXT | |
The actual selectable values. E.g., "Cathedral" under Figure, "1\"–1 1/2\"" under Thickness.
`UNIQUE(woo_id)`.
---
## woo_tags
| Column | Type | Notes |
|---|---|---|
| `woo_id` | INTEGER PK | |
| `name` | TEXT | |
| `slug` | TEXT | |
| `synced_at` | TEXT | |
Only `fig-*` tags. Other Woo tags ignored.
`UNIQUE(woo_id)`.
---
## settings
| Column | Type | Notes |
|---|---|---|
| `key` | TEXT PK | |
| `value_enc` | BLOB | AES-GCM ciphertext |
| `updated_at` | TEXT | |
Keys:
- `woo_base_url`, `woo_consumer_key`, `woo_consumer_secret`
- `inference_base_url`, `inference_api_key`, `inference_model`
- `inference_enabled`, `content_llm_enabled`
- `brand_voice`, `geo_context`
- `prompts` (JSON: call1_prompt, call2_prompt, reset-to-default)
- `publish_status` ('published' | 'draft')
- `aspect_ratio` ('3:4'), `output_px` (1600), `output_px_min` (1600), `fill_target` (0.80)
- `user_sensitivity`, `user_edge_offset`, `user_feather`, `user_sheet_mode`
AES master key is an env var (`SLAB_AES_KEY`, 32 bytes base64). Never in the DB.
Each `value_enc` = `nonce(12) || ciphertext`.
---
## sync_log
Append-only history. Not deleted on publish.
| Column | Type | Notes |
|---|---|---|
| `id` | INTEGER PK AUTOINCREMENT | |
| `slab_id` | TEXT FK | |
| `sku` | TEXT | |
| `woo_product_id` | INTEGER | |
| `status` | TEXT | 'success' \| 'failed' |
| `http_status` | INTEGER | |
| `error` | TEXT | machine-readable code |
| `detail` | TEXT | Woo error body |
| `payload_hash` | TEXT | sha256 of Woo payload sent |
| `created_at` | TEXT | |
---
## inference_log
Append-only audit.
| Column | Type | Notes |
|---|---|---|
| `id` | INTEGER PK AUTOINCREMENT | |
| `slab_id` | TEXT FK | |
| `call` | TEXT | '1' \| '2' |
| `request_payload` | TEXT | JSON of what was sent |
| `response_payload` | TEXT | JSON of what came back |
| `created_at` | TEXT | |