docs: unslop UX, README, and store copy for a human reader
Same locks. Drop ticket numbers and agent shorthand from the mill-owner facing files.
This commit is contained in:
@@ -1,58 +1,54 @@
|
||||
# SlabUploader
|
||||
|
||||
Mobile-first, self-hosted app for publishing one-of-a-one wood slabs to WooCommerce.
|
||||
Phone capture (online) → hybrid measure/crop on device → review → WooCommerce create.
|
||||
Phone app for listing one-of-a-one wood slabs on the Whidbey Wood Store.
|
||||
|
||||
Owner: Ty (Whidbey). Store: www.whidbeywoodstore.com. Deploy: Docker Compose + Caddy on an inventoried host (`APP_HOSTNAME`).
|
||||
You photograph a slab, confirm the cut and the numbers on the phone, review the listing, and create the WooCommerce product. The phone does the measuring and crop. The server stores the draft, talks to the store, and (later) runs vision.
|
||||
|
||||
## Status
|
||||
Owner: Ty (Whidbey). Store: www.whidbeywoodstore.com. Runs in Docker Compose behind Caddy on a host you inventory first (`APP_HOSTNAME`).
|
||||
|
||||
**Gate A freeze signed 2026-08-31.** Docs refactor on branch `docs/refactor-v2` (2026-09-01).
|
||||
Read `AGENTS.md` before any implementation. TECH-SPEC + ARCHITECTURE + AGENTS beat archived PRD lines.
|
||||
## Where we are
|
||||
|
||||
POC bar: phone → customer-ready listing on the real store; UAT creates as **Woo draft**
|
||||
with `SLAB-UAT-*` SKUs; one real SKU **Woo publish** after Ty review. Online-only for POC
|
||||
(offline deferred). Build plan: `docs/IMPL-PLAN.md` (Phase 0 = hybrid Gates A+B; Gate C =
|
||||
early Phase 1 first draft listing).
|
||||
Docs freeze signed 2026-08-31. The current docs branch is `docs/refactor-v2`. Read `AGENTS.md` before writing code. If an old PRD line fights TECH-SPEC, ARCHITECTURE, or AGENTS, ignore the PRD. The old PRD is in `docs/archive/`.
|
||||
|
||||
The bar for this first version: a real slab, photographed on a phone, listed on the real store. Practice listings are WooCommerce **drafts** with SKUs that start `SLAB-UAT-`. After Ty looks, one real SKU goes **live**. Online only for now. Offline capture waits.
|
||||
|
||||
Build order is in `docs/IMPL-PLAN.md`. Empty app on the real host first, then the first draft listing, then vision.
|
||||
|
||||
## Documentation
|
||||
|
||||
| File | What |
|
||||
|---|---|
|
||||
| `AGENTS.md` | Working contract + hard rules (read first) |
|
||||
| `docs/ARCHITECTURE.md` | Hybrid client/server split, data flow |
|
||||
| `docs/TECH-SPEC-PIPELINE.md` | Deterministic math + image pipeline, constants, test vectors |
|
||||
| `docs/DATA-MODEL.md` | SQLite schema + slab status state machine |
|
||||
| `docs/OPENAPI.md` | Client↔server API contract |
|
||||
| `docs/CONTENT-WOO.md` | Content templates + WooCommerce payload/integration |
|
||||
| `docs/PROMPTS.md` | Vision + content prompts (server files) |
|
||||
| `docs/DEPLOYMENT.md` | Env-agnostic compose, Caddy, backup/restore |
|
||||
| `deploy/INVENTORY.md` | Host inventory worksheet (Gate B) |
|
||||
| `docs/UX.md` | POC interaction spec (review, capture knobs, errors, first-run) |
|
||||
| `docs/IMPL-PLAN.md` | Phased build plan with acceptance criteria + exit gates |
|
||||
| `docs/archive/PRD-2026-08.txt` | Frozen historical PRD only (not a source for new work) |
|
||||
| `AGENTS.md` | Hard rules. Read first. |
|
||||
| `docs/ARCHITECTURE.md` | What runs on the phone vs the server |
|
||||
| `docs/TECH-SPEC-PIPELINE.md` | Math, image pipeline, test vectors |
|
||||
| `docs/DATA-MODEL.md` | SQLite tables and slab statuses |
|
||||
| `docs/OPENAPI.md` | Phone to server API |
|
||||
| `docs/CONTENT-WOO.md` | Titles, descriptions, store payload |
|
||||
| `docs/PROMPTS.md` | Vision and copy prompts (server files) |
|
||||
| `docs/DEPLOYMENT.md` | Compose, Caddy, backup |
|
||||
| `deploy/INVENTORY.md` | Host worksheet before you bind ports |
|
||||
| `docs/UX.md` | What the mill owner sees on the phone |
|
||||
| `docs/IMPL-PLAN.md` | Phases and exit gates |
|
||||
| `docs/archive/PRD-2026-08.txt` | Frozen history. Not a source for new work. |
|
||||
|
||||
## Quick orientation
|
||||
## Short map
|
||||
|
||||
Hard locks live in `AGENTS.md`. Short map:
|
||||
- Phone: mask knobs, square feet, board feet, 3:4 PNG. Server: store, WooCommerce, vision proxy. Server matting ("Try harder") is not in this first listing.
|
||||
- Board feet = square feet × thickness in inches. No divide-by-12. Test vectors TV-1 through TV-10 run on the phone.
|
||||
- Secrets never sit in the browser. Encrypted on the server. Store login is a WordPress application password on a low-privilege user, not WooCommerce consumer keys.
|
||||
- Practice SKUs `SLAB-UAT-*` are always drafts, even if Settings say publish.
|
||||
- Vision stays off until the first draft listing works. Species suggestion and Generate text come after that, still in this project.
|
||||
- Two containers (`frontend`, `fastapi`) plus Caddy. One SQLite, image volumes.
|
||||
- Not in v1: versioning, staff roles, bulk publish, analytics, editing an existing SKU.
|
||||
|
||||
- **Hybrid:** phone owns mask knobs / sqft / bdft / 3:4 PNG; server stores, Woo, inference proxy. U2Net “Try harder” deferred from POC.
|
||||
- **bdft** = sqft × thickness_in (no `/12`). TV-1 through TV-10 on the client.
|
||||
- **Secrets** never in browser; AES-GCM on server; `SLAB_AES_KEY` env only. Woo auth = WP application password (dedicated low-priv user), not consumer keys.
|
||||
- **Woo UAT:** draft default; `SLAB-UAT-*` force draft (AGENTS §5).
|
||||
- **Inference OFF** until Gate C; Call 1/2 after, still inside POC.
|
||||
- **Online-only for POC** (offline PWA deferred).
|
||||
- **Two containers** (`frontend`, `fastapi`) + Caddy. Single SQLite + image volumes.
|
||||
- **v1 non-goals:** no versioning, no staff/roles, no bulk publish, no analytics, no SKU update path.
|
||||
## Repo
|
||||
|
||||
## Repo remotes
|
||||
Source of truth: Gitea `Ty_Tech/SlabUploader` on gitea-atd
|
||||
(`https://gitea.vps1.afterthedemo.com/Ty_Tech/SlabUploader`). TrueNAS and GitHub copies are not the source of truth for this work.
|
||||
|
||||
- Canonical SoT: Gitea `Ty_Tech/SlabUploader` on **gitea-atd** (`https://gitea.vps1.afterthedemo.com/Ty_Tech/SlabUploader`)
|
||||
- Optional later mirrors: `gitea-truenas`, GitHub — not SoT for this workstream
|
||||
## Getting started
|
||||
|
||||
## Getting started (implementer)
|
||||
|
||||
1. Read `AGENTS.md` (hard rules) and Phase 0 in `docs/IMPL-PLAN.md`.
|
||||
2. Start at Phase 0 — hybrid client TECH-SPEC modules + thin FastAPI health/settings (not a server happy-path pipeline).
|
||||
3. Do not skip exit gates; Ty signs each phase via the delivery process.
|
||||
1. Read `AGENTS.md` and Phase 0 in `docs/IMPL-PLAN.md`.
|
||||
2. Build the phone math modules and a thin FastAPI health/settings app. Do not put measuring on the server.
|
||||
3. Ty signs each phase. Do not skip that.
|
||||
4. Do not implement from `docs/archive/PRD-2026-08.txt`.
|
||||
|
||||
+59
-86
@@ -1,15 +1,11 @@
|
||||
# Content Generation & WooCommerce Integration
|
||||
# Listing copy and the store
|
||||
|
||||
Status: SPEC · 2026-08-24 · SlabUploader
|
||||
Covers: FR16 (deterministic content templates + optional LLM refinement), FR17
|
||||
(brand voice / GEO), FR18 (editable, never auto-published), FR25–28 (taxonomy sync,
|
||||
product create, sync log), and the exact WooCommerce REST v3 payload.
|
||||
How titles and descriptions are built, how we talk to WooCommerce, and the exact create payload. Templates always run. Generate text is optional and never publishes on its own.
|
||||
|
||||
## 1. Content generation (FR16–18)
|
||||
## 1. Titles and descriptions
|
||||
|
||||
### 1.1 Deterministic templates (always on)
|
||||
Templates are configured in Settings (`content_templates`, editable per FR32).
|
||||
Defaults below. Placeholders use `{field}`.
|
||||
### 1.1 Templates (always on)
|
||||
Templates live in Settings. Defaults below. Placeholders use `{field}`.
|
||||
|
||||
**title** (default):
|
||||
```
|
||||
@@ -32,32 +28,26 @@ average width of {width_avg_in}in and {thickness_in}in thickness ({bdft} bdft).
|
||||
Each slab is naturally shaped — width varies along the length at {width_min_in}in to
|
||||
{width_max_in}in. Sold by the slab; dimensions as measured. {geo_sentence}
|
||||
```
|
||||
`geo_sentence` = GEO context sentence from Settings (FR17), e.g.
|
||||
`geo_sentence` is the geography sentence from Settings, e.g.
|
||||
`"Locally milled and shipped from the Pacific Northwest."` (default if unset: none).
|
||||
|
||||
All template math fields come from the deterministic pipeline (never from the LLM).
|
||||
|
||||
### 1.2 Call 2 content generation (toggleable)
|
||||
- Triggered only if `content_llm_enabled` is true AND the user taps "Generate text"
|
||||
on the review screen (never automatic).
|
||||
- **LLM writes prose only**: the prompt sends the slab's deterministic measurements
|
||||
as facts, and asks the model to write compelling product description prose
|
||||
(character, color, warmth, grain, use cases). Dimensions are never generated
|
||||
by the LLM — they are injected by deterministic templates.
|
||||
- **Template assembly**: after the LLM returns, the server assembles the final
|
||||
title, short description, and full description from deterministic templates
|
||||
with the LLM prose injected into the description.
|
||||
- No numeric guardrail needed — dimensions never pass through the LLM.
|
||||
- Output is always editable (FR18); `content_source` = `llm` when used.
|
||||
- If the inference endpoint is text-only or unreachable → the button is disabled,
|
||||
templates still work. No failure mode blocks publish.
|
||||
### 1.2 Generate text (optional)
|
||||
The mill owner taps Generate text on review. It never runs by itself. It is off unless `content_llm_enabled` is true.
|
||||
|
||||
The model writes prose only (character, color, grain, uses). Measured length, width, thickness, square feet, and board feet are facts in the prompt. The model does not invent numbers. Templates inject those numbers into the title and description after the prose comes back.
|
||||
|
||||
The mill owner can always edit the result. `content_source` is `llm` when this path ran.
|
||||
|
||||
If the text endpoint is missing or down, disable the button. Templates still work. Publish is never blocked because Generate text failed.
|
||||
|
||||
### 1.3 Audit
|
||||
Every content generation (template or LLM) is logged with inputs (template version,
|
||||
slab facts), the LLM request/response payload when used (inference calls logged for audit),
|
||||
and the final stored content.
|
||||
|
||||
## 2. WooCommerce integration (FR24–28)
|
||||
## 2. WooCommerce
|
||||
|
||||
### 2.1 Auth & client
|
||||
- REST v3 surface unchanged: `GET/POST/PATCH/DELETE {WOO_BASE_URL}/wp-json/wc/v3/…`
|
||||
@@ -79,7 +69,7 @@ and the final stored content.
|
||||
- Store URL is **`WOO_BASE_URL`** from Docker Compose / host `.env` (HTTPS only).
|
||||
FastAPI refuses `http://` at startup. Not a Settings field; UI shows it read-only.
|
||||
|
||||
### 2.2 Taxonomy sync (FR25)
|
||||
### 2.2 Store lists (species, tags, attributes)
|
||||
`POST /api/v1/admin/taxonomy/sync` pulls, in order:
|
||||
1. `GET /products/categories?per_page=100` (paginate) → species + wood-category leaves
|
||||
2. `GET /products/tags?per_page=100` → `fig-*` / `feat-*` only (other tags ignored)
|
||||
@@ -96,29 +86,27 @@ invent a Woo-side taxonomy timestamp. Do not sync per navigation.
|
||||
|
||||
**Triggers:** (1) successful `test-woo` **always** resyncs; (2) publish if
|
||||
`now - taxonomy_last_sync_at > 1 hour` then reconcile (bump only on real change);
|
||||
(3) optional app launch / Settings refresh. Stale selected IDs after a bump are
|
||||
#18 422, not a silent remap.
|
||||
(3) optional app launch / Settings refresh. If a picked id vanished after a refresh, show that field as no longer valid. Do not silently swap it.
|
||||
|
||||
**Species exclusivity.** UI pickers, pricing seeds, and Call 1 species lists are
|
||||
**exactly** the synced species-category leaves. No hardcoded, invented, or fallback
|
||||
species set. A species new in Woo is selectable only after the next sync stores it.
|
||||
|
||||
### 2.3 Taxonomy assignment (FR26)
|
||||
### 2.3 What goes on the listing
|
||||
|
||||
**Primary source when inference is on and confidence ≥ threshold (default 0.7):**
|
||||
Call 1 vision results pre-fill the review screen for species, wood categories,
|
||||
edge type, figure, grade, and mapped `fig-*` / `feat-*` tags (character /
|
||||
inclusions / voids / checks → existing `feat-*`). All pre-filled values are
|
||||
mutable. The user confirms or overrides; manual entry always wins.
|
||||
When vision is on and it is at least 0.7 sure, it fills species, wood categories,
|
||||
edge, figure, grade, and feature tags (character, inclusions, voids, checks map
|
||||
to existing `feat-*` tags only). The mill owner can change every filled value.
|
||||
Typed values always win.
|
||||
|
||||
**Below threshold:** those fields stay empty. The review screen gates continue
|
||||
until the user manually selects **exactly one species, ≥1 wood category, and ≥1
|
||||
figure**. Inline nudges: `docs/UX.md`. Do not auto-fill below-threshold
|
||||
guesses. Full functional rules: AGENTS §6.
|
||||
If it is less sure, those fields stay empty. Review will not continue until they
|
||||
pick exactly one species, at least one wood category, and at least one figure.
|
||||
How that looks on the phone: `docs/UX.md`. Do not fill a low-confidence guess.
|
||||
|
||||
Names from vision or from typing match store ids case-insensitive, trimmed. No
|
||||
match leaves the field empty so they can pick from the list. The store will still
|
||||
reject a missing required field at publish.
|
||||
|
||||
**Deterministic name-matching** resolves Call 1 names (or manual picks) to synced
|
||||
Woo IDs case-insensitive, trimmed. No match → empty; user picks in review. Woo
|
||||
required-field validation still surfaces gaps at publish.
|
||||
|
||||
Field rules:
|
||||
- **Categories**: species leaf (1 required) plus wood-category leaf ids (1+).
|
||||
@@ -129,9 +117,9 @@ Field rules:
|
||||
character, length, or free-text thickness as product attributes. Missing
|
||||
attributes are skipped, not created (v1: no attribute creation).
|
||||
|
||||
Call 1 is assist-only and never gates publish. Inference OFF → full manual path.
|
||||
Vision only suggests. It never blocks publish. With vision off, everything is typed.
|
||||
|
||||
### 2.4 Product create payload (FR27)
|
||||
### 2.4 Create payload
|
||||
`POST /products` — body (only non-null fields sent):
|
||||
```jsonc
|
||||
{
|
||||
@@ -161,31 +149,27 @@ For reliability the server:
|
||||
2. Reference those `source_url` values in `images` (≤5 inventory photos).
|
||||
3. Only `kind=inventory` photos. Calibration photos do not exist in this design.
|
||||
|
||||
**Status** comes from Settings key **`woo_create_status`** (`draft` | `publish`, default
|
||||
**`draft`**). Woo **safety policy** (UAT draft default, `SLAB-UAT-*` force draft even when
|
||||
Settings say publish, no edit-same-SKU, purge originals + processed images after successful
|
||||
publish) is owned by **AGENTS.md §5**. This file owns payload shape and publish sequence
|
||||
only. Do not diverge from AGENTS on force-draft. Do not conflate Woo create status with
|
||||
slab lifecycle status `published` (means Woo create succeeded locally).
|
||||
**Status** comes from Settings `woo_create_status` (`draft` or `publish`, default
|
||||
`draft`). Practice SKUs `SLAB-UAT-*` are always created as drafts, even if Settings
|
||||
say publish. Do not edit an existing SKU in this version. After a successful create,
|
||||
the server deletes the original and processed photos. A slab marked published in the
|
||||
app means the store create succeeded. That is not the same as WooCommerce "publish."
|
||||
The force-draft rule is in `AGENTS.md`. This file only owns the payload and the steps.
|
||||
|
||||
### 2.5 Dedupe & idempotency (FR24a)
|
||||
Before create: `GET /products?sku={sku}` (any Woo status).
|
||||
- Found → `409 duplicate_sku` (v1 has no update path).
|
||||
- Not found → proceed. (Single-owner, slab-by-slab; check still required.)
|
||||
### 2.5 Duplicate SKU
|
||||
Before create: `GET /products?sku={sku}` (any store status).
|
||||
- Found: stop. The phone shows the SKU field with Edit SKU and Open existing listing. This version does not update the other product.
|
||||
- Not found: create. Still do the check every time.
|
||||
|
||||
### 2.6 Post-submission recovery (409 / 422) and logging (FR28)
|
||||
### 2.6 After a failed publish
|
||||
|
||||
Business-logic failures after the user taps publish map to **field-level recovery**
|
||||
on the review screen. No raw HTTP codes or stack traces in the UI. The draft stays
|
||||
on the phone across error-and-retry.
|
||||
The mill owner sees a message on the field, not a status number or a stack. The form stays on the phone. The server marks the slab failed so they can try again.
|
||||
|
||||
| Class | When | UI recovery |
|
||||
|---|---|---|
|
||||
| **409 Conflict** | SKU already exists in Woo, any status | Inline on the **SKU** field. Copy names the conflict. Two paths: **edit SKU** and retry, or **open the existing listing** (store admin URL / product id). Never "update in place" of the other product in MVP. |
|
||||
| **422 Validation** | Stale or rejected value (category/attribute deleted since sync, price rule, etc.) | Inline on the **affected field**. Copy: value is no longer valid; show **current options** from the synced taxonomy cache (or re-pull if anchor advanced). |
|
||||
If the SKU already exists: under SKU, "This SKU already exists in the store (any status)." Edit SKU, or open the existing listing (admin link when we have it).
|
||||
|
||||
Server still writes `sync_log` and sets slab `failed` with machine codes for pollers.
|
||||
Success path unchanged: set `woo_product_id`, local `published`, purge images.
|
||||
If a category, attribute, or price is no longer valid: under that field, "This value is no longer valid. Pick from the current list," with the current store options.
|
||||
|
||||
The server still writes `sync_log`. Success still sets `woo_product_id`, marks published locally, and deletes photos.
|
||||
|
||||
- **Partial-failure note** (risk, see IMPL-PLAN): if media uploads succeed but the
|
||||
final product POST fails, orphaned Woo attachments may exist. v1: log it; surface
|
||||
@@ -193,30 +177,19 @@ Success path unchanged: set `woo_product_id`, local `published`, purge images.
|
||||
|
||||
## 3. End-to-end publish sequence (server side)
|
||||
```
|
||||
1. validate via shared validation module (hash check on submit; see ARCHITECTURE §8)
|
||||
2. dedupe by SKU (GET /products?sku=, any status) → 409 duplicate_sku if exists
|
||||
3. (re)sync taxonomy if last_sync >1h; bump taxonomy_anchor only on stored-subset hash change
|
||||
4. resolve category/tags/attributes (Call 1 / manual + name-match; synced set only)
|
||||
5. upload processed PNG inventory images → media → source_urls
|
||||
6. POST /products (status per AGENTS §5 + woo_create_status) → woo_product_id
|
||||
7. write sync_log(success), set slab published, purge originals + processed images
|
||||
on failure: sync_log(failed), slab=failed, images retained; map 409/422 to fields (§2.6)
|
||||
1. check the listing against the shared rules (current copy of the rules on submit)
|
||||
2. look up the SKU in the store (any status). Stop if it already exists.
|
||||
3. refresh store lists if the last successful pull is older than 1 hour. Only bump the client list-version if something actually changed.
|
||||
4. resolve categories, tags, and attributes from the synced lists (vision names or typed names)
|
||||
5. upload processed PNG inventory photos to media, keep the URLs
|
||||
6. POST /products with draft/publish per Settings, always draft for SLAB-UAT-* SKUs
|
||||
7. on success: log, mark published locally, delete originals and processed photos
|
||||
on failure: log, mark failed, keep photos, put the error on the field (§2.6)
|
||||
```
|
||||
|
||||
## 4. What is deterministic vs scoped-inference here
|
||||
- Deterministic: templates, all dimension text, category/tag/attribute resolution
|
||||
against the Woo-synced cache only, payload assembly, dedupe, logging, title assembly.
|
||||
- Scoped inference: optional LLM prose generation for description only. The LLM
|
||||
never touches dimensions and may only choose species/attributes present in the
|
||||
synced taxonomy lists.
|
||||
## 4. What the model may and may not do
|
||||
|
||||
## 4. Field-level Woo errors (presentation in `docs/UX.md`)
|
||||
Templates, numbers, matching names to store ids, payload, dedupe, logging: code.
|
||||
Generate text may write description prose only. It must not invent dimensions. It may only pick species and attributes that exist in the synced store lists.
|
||||
|
||||
Wire shape: `{ field, code, message, actions[] }`. The phone never displays HTTP codes.
|
||||
|
||||
- **409 duplicate SKU**: actions `edit_sku` | `open_existing`. Include Woo id + admin URL when GET-by-sku works; if lookup fails, keep `open_existing` (SKU + “open this SKU in Woo admin”). No in-place update of the other product.
|
||||
- **422 stale field**: current cache options after publish-time sync if #14 fired.
|
||||
- Draft: phone keeps the form; server slab → `failed` (retryable). No IndexedDB.
|
||||
- Module-stale hash is **412**, not 409.
|
||||
|
||||
The shared validation module is `GET /api/v1/validation/module`. Hash check on submit only.
|
||||
How errors look on the phone is in `docs/UX.md`. The wire uses `{ field, code, message, actions[] }`. The mill owner never sees those codes. If the phone's copy of the rules is stale, refresh the copy and retry. That is not a duplicate-SKU conflict.
|
||||
|
||||
+84
-112
@@ -1,172 +1,144 @@
|
||||
# SlabUploader UX — POC interaction spec
|
||||
# How the phone app should feel
|
||||
|
||||
Status: SPEC · 2026-09-01 · issue #10 (presentation) · functional rules in #9 / AGENTS
|
||||
This is the UAT-facing interface contract. Wireframes are described as screens and states, not pixels. Implementers and testers use this file plus `AGENTS.md` and `docs/TECH-SPEC-PIPELINE.md`.
|
||||
This is the screen-by-screen description testers and builders use. It is not pixel mockups. Numbers and math live in `docs/TECH-SPEC-PIPELINE.md`. Hard product rules live in `AGENTS.md`. This file is what the mill owner sees.
|
||||
|
||||
Functional numbers (0.7 threshold, empty vs prefill) live in AGENTS §6. This file only says how those rules look and feel.
|
||||
A suggestion from the vision model never beats a typed value. Empty is better than a confident-looking wrong species.
|
||||
|
||||
---
|
||||
## Before you can list
|
||||
|
||||
## 0. Principles
|
||||
You cannot mark a slab ready until all of these are set:
|
||||
|
||||
- One-handed phone, outdoor or mill-floor lighting, thumb reach.
|
||||
- The user always sees what is missing **on the field**, not only after Submit.
|
||||
- Manual values always beat inference. Empty is honest; fake prefill is not.
|
||||
- No raw HTTP codes or stack traces. Recovery stays on the same screen as the field.
|
||||
- exactly one species
|
||||
- at least one wood category
|
||||
- at least one figure (the store's figure list, including `fig-*` tags)
|
||||
|
||||
Mandatory before **ready** (continue to publishable):
|
||||
The missing field is marked on the control itself, not only after you tap continue.
|
||||
|
||||
- exactly **one species**
|
||||
- **at least one wood category**
|
||||
- **at least one figure** (`fig-*` or the Figure attribute — same locked set as DATA-MODEL)
|
||||
## First time you open the camera
|
||||
|
||||
Issue #9 named species + ≥1 figure as the confidence-gate floor. Wood category is also required to list (original product). Presentation treats all three as mandatory with inline nudges (#10).
|
||||
A short overlay, once. You can dismiss it and "Don't show again." Settings can bring it back.
|
||||
|
||||
---
|
||||
1. Lay the slab on a green or black sheet.
|
||||
2. Shoot from above, lens parallel to the broad face.
|
||||
3. Put a tape along the long grain so *you* can type length in inches. The app does not read the tape.
|
||||
4. Take one to five photos. Confirm the cut around the wood before you trust square footage.
|
||||
|
||||
## 1. First-run (calibration trick)
|
||||
This is photography advice, not computer vision of a ruler.
|
||||
|
||||
Not OpenCV ruler detection (that path is deferred). First launch of capture shows a short overlay, dismissible, remembered in localStorage:
|
||||
## Capture: the mask is the measurement
|
||||
|
||||
1. Lay the slab on a **green or black sheet**.
|
||||
2. Shoot **top-down**, lens parallel to the broad face.
|
||||
3. Put a tape or known length along the prevailing grain so *you* can type length in inches. The app does not read the tape.
|
||||
4. Take 1–5 photos. Confirm the mask before trusting numbers.
|
||||
The cut around the wood is the footprint. Length, the 6" width samples, square feet, and board feet all sit on that mask. A fat or clipped cut quietly moves every number.
|
||||
|
||||
Skip control: “Don’t show again.” Reset lives in Settings, not on the capture chrome.
|
||||
### One screen
|
||||
|
||||
---
|
||||
The photo fills the screen. The mask overlay sits on the original photo (kept vs dropped), not only on a cropped PNG.
|
||||
|
||||
## 2. Capture — mask is the load-bearing step
|
||||
Four knobs sit next to the preview. You do not leave this screen for Settings to tune the cut:
|
||||
|
||||
Background removal defines the pixel footprint. Length, widths, sqft, and bdft all sit on that mask. A clipped or fat mask silently moves the bounding rectangle and the 6" width samples.
|
||||
- Sheet: auto, green, or black
|
||||
- Sensitivity
|
||||
- Edge offset
|
||||
- Feather
|
||||
|
||||
### Layout (one screen)
|
||||
Confirm the edge. Then confirm the length axis (rotate if it is skewed). Then type length, thickness, and SKU.
|
||||
|
||||
- Full-bleed photo with **mask overlay** on the original (included vs excluded), not only the cropped PNG.
|
||||
- Four knobs **on this screen**, next to the preview — never a trip to Settings:
|
||||
- Sheet: auto / green / black
|
||||
- Sensitivity
|
||||
- Edge offset
|
||||
- Feather
|
||||
- Confirm edge. Then length-axis overlay (rotate / confirm). Then length, thickness, SKU.
|
||||
- Retake photo. POC does **not** show “Try harder” (server U2Net). That control is deferred, not deleted — do not remove it from architecture docs.
|
||||
Retake if the photo is bad. This first listing does not show a "Try harder" server matting button. Keep that path in the architecture docs; do not ship it here.
|
||||
|
||||
### Live re-run
|
||||
|
||||
Changing any knob re-runs client background removal on the current photo and refreshes the overlay immediately. No save button for knobs. No server round trip for mask tuning in POC.
|
||||
Move a knob and the overlay updates on this photo immediately. No save. No trip to the server for mask tuning.
|
||||
|
||||
### Empty / error states (capture)
|
||||
### What you see when it fails
|
||||
|
||||
| Situation | What the user sees |
|
||||
| Situation | On screen |
|
||||
|---|---|
|
||||
| Sheet color not auto-detected | Prompt: pick green or black. Stay on this screen. |
|
||||
| Mask not one contiguous slab | Banner on the photo: “Cut is broken — retake or tighten the sliders.” Confirm stays disabled. |
|
||||
| Axis looks wrong | Rotate control; if still wrong, retake. |
|
||||
| After crop, shorter side would be < 1600px | Warn: this photo cannot list until retake. No fake upscale. |
|
||||
| User tries to continue without confirming mask | Confirm is the gate; Continue is disabled until confirm. |
|
||||
| Sheet color not obvious | Pick green or black. Stay here. |
|
||||
| Cut is broken into pieces | "Cut is broken. Retake or tighten the sliders." Confirm stays off. |
|
||||
| Length axis looks wrong | Rotate. If it is still wrong, retake. |
|
||||
| After crop, the short side would be under 1600 pixels | This photo cannot list until you retake. The app will not invent pixels. |
|
||||
| Continue without confirming the cut | Continue stays off until you confirm. |
|
||||
|
||||
Knobs persist in localStorage (reset-to-default in Settings). They are not server secrets.
|
||||
Knob values stay on the phone (reset in Settings). They are not store secrets.
|
||||
|
||||
---
|
||||
## Review
|
||||
|
||||
## 3. Review screen
|
||||
After the draft is uploaded. If vision is on, a species pass may have run.
|
||||
|
||||
After calibrated upload (and Call 1 if inference is on).
|
||||
### How fields fill
|
||||
|
||||
### Field rendering
|
||||
The default bar is 0.7. There is no slider for it in this app.
|
||||
|
||||
| Call 1 confidence | Species, wood categories, edge, figure, grade, feat-* |
|
||||
|---|---|
|
||||
| **≥ 0.7** (default; configurable in server settings later, not a POC slider) | Prefill from Call 1. Every value is editable. Show a quiet “suggested” affordance, not a lock. |
|
||||
| **< 0.7** | Leave **empty**. Do not stash the low-confidence guess in a hidden field. |
|
||||
| Inference OFF | Empty; user fills by hand. Same mandatory set. |
|
||||
If the model is at least that sure, it fills species, wood categories, edge, figure, grade, and feature tags. Every filled value is still editable. A quiet "suggested" mark is enough. Do not lock the field.
|
||||
|
||||
Pickers for species, wood category, and figure list **only** Woo-synced leaves. No “other”, no hardcoded mill list. A species that appeared in Woo after last sync is invisible until the next successful sync.
|
||||
If it is less sure, leave those fields empty. Do not hide the guess in a secret field.
|
||||
|
||||
Price: recommendation from species $/bdft × bdft when a rate exists; otherwise empty. Override always allowed.
|
||||
If vision is off, the fields start empty. Same required set. You fill them by hand.
|
||||
|
||||
Title / description: empty until the user taps **Generate text** (Call 2) or types. Call 2 never auto-runs.
|
||||
Species, wood category, and figure lists are only what we last pulled from the store. No "other." No mill list baked into the app. A species added in WooCommerce after the last sync does not appear until the next successful sync.
|
||||
|
||||
### Per-field nudges (not submit-only)
|
||||
Price: if this species has a $/board-foot rate, show `rate × board feet`. You can always override. If there is no rate, leave price empty.
|
||||
|
||||
While a mandatory field is empty, that control shows an inline state (mark + short line under the field):
|
||||
Title and description start empty until you type or tap Generate text. Generate text never runs on its own.
|
||||
|
||||
- Species: “Pick the species from the store list.”
|
||||
- Wood category: “Pick at least one wood category.”
|
||||
- Figure: “Pick at least one figure.”
|
||||
### Nudges on the field
|
||||
|
||||
Nudges appear as soon as the screen loads with empties, and clear when the field is valid. Submit/Continue to ready stays disabled while any of the three is empty — **and** the user can still see which one without scrolling to a toast after tap.
|
||||
Empty required fields show a line under the control as soon as the screen opens:
|
||||
|
||||
Do not use a single post-submit modal as the only signal.
|
||||
- Species: "Pick the species from the store list."
|
||||
- Wood category: "Pick at least one wood category."
|
||||
- Figure: "Pick at least one figure."
|
||||
|
||||
The line goes away when the field is valid. Continue stays off while any of the three is empty. You should not have to tap continue to find out which one.
|
||||
|
||||
### Overrides
|
||||
|
||||
Changing a prefilled species/figure is a normal edit. No confirm dialog. Manual value is what publishes.
|
||||
Change a suggested species or figure like any other edit. No extra confirm. What you leave in the field is what goes to the store.
|
||||
|
||||
---
|
||||
## Settings (what the mill owner touches)
|
||||
|
||||
## 4. Settings (relevant bits)
|
||||
|
||||
- **Store URL**: read-only text from `WOO_BASE_URL` (compose). Not an input.
|
||||
- Woo username + application password: inputs. Password never round-trips on GET.
|
||||
- Inference endpoint + key + model; enable flags.
|
||||
- Species $/bdft table (seeded from synced species only).
|
||||
- `woo_create_status` draft | publish.
|
||||
- Store URL: text only, from deploy. Not an input.
|
||||
- WordPress username and application password. The password never comes back on load.
|
||||
- Vision endpoint, key, model, on/off.
|
||||
- Species $/board-foot table, filled from synced species only.
|
||||
- New listings: draft or published. Default draft.
|
||||
- Reset capture knobs to factory.
|
||||
- Manual “Refresh taxonomy” is optional for POC; test-connection always syncs.
|
||||
- Refresh store lists is optional. Testing the store connection always refreshes them.
|
||||
|
||||
---
|
||||
## Typing checks
|
||||
|
||||
## 5. Validation (field exit and submit)
|
||||
Rules live on the server. The phone keeps a copy. The copy is checked when you submit, not on every tap.
|
||||
|
||||
Shared module on FastAPI (`docs/OPENAPI.md`). Client caches it; hash checked **on submit**, not on every blur.
|
||||
Leaving a field: the phone checks the copy. Required empty fields block. Optional empty is fine. Optional with text must pass.
|
||||
|
||||
- Blur: local check with cached rules. Required fields block; optional empty is valid; optional with content must pass.
|
||||
- Length: hard cap at the Woo limit (cannot type past it).
|
||||
- Character set: SKU `[A-Z0-9-]{3,24}`; title/description UTF-8 within Woo max. Invalid characters rejected on exit. Not “extended ASCII”.
|
||||
- Submit: full pass + hash check. Mismatch → “Updating rules” (HTTP 412, never shown) → re-validate → retry. No data loss.
|
||||
Length: you cannot type past the store's max.
|
||||
|
||||
Presentation: errors sit under the field. No top-of-screen dump for field problems.
|
||||
SKU: letters A–Z, digits, hyphen, 3 to 24 characters. Title and description: normal text, store length limits. Bad characters fail when you leave the field.
|
||||
|
||||
---
|
||||
Submit: full check plus "is my copy of the rules still current?" If not, the phone shows "Updating rules," fetches the new copy, checks again, and retries. You do not lose the form. Do not show a status number.
|
||||
|
||||
## 6. Publish errors (after commit)
|
||||
Errors sit under the field. Not a pile at the top of the screen.
|
||||
|
||||
The phone keeps the form. The server slab goes `failed` (retryable). No IndexedDB.
|
||||
## After you tap publish
|
||||
|
||||
**409 duplicate SKU** — inline on SKU:
|
||||
The form stays on the phone. The server marks the slab failed so you can try again. Nothing is stored in a special offline database.
|
||||
|
||||
- “This SKU already exists in the store (any status).”
|
||||
- Actions: **Edit SKU** (focus the field) · **Open existing listing** (required). Use the admin URL/id when the server returned them; otherwise keep the button and “look this SKU up in Woo admin”.
|
||||
- Never show `409` as the message.
|
||||
**This SKU already exists** (any status in the store). Message under SKU: "This SKU already exists in the store (any status)." Two actions: Edit SKU (focus the field), and Open existing listing. If we have the admin link, use it. If not, keep the button and tell them to look the SKU up in store admin. Never print "409".
|
||||
|
||||
**422 stale value** — inline on the field Woo rejected (category, attribute, price, …):
|
||||
**This value is no longer valid** (category, attribute, price, and the like). Message under that field: "This value is no longer valid. Pick from the current list." Show the current store options after the submit-time refresh. Never print "422".
|
||||
|
||||
- “This value is no longer valid. Pick from the current list.”
|
||||
- Options from the synced cache after the submission-time refresh.
|
||||
- Never show `422` as the message.
|
||||
If the store is down, times out, or auth fails: one banner, "Couldn't reach the store. Try again." Form intact. Not a field nudge.
|
||||
|
||||
Server 5xx / timeout / auth: one recoverable banner (“Couldn’t reach the store — try again”), draft intact. Not a field nudge.
|
||||
## Checks a tester can run
|
||||
|
||||
---
|
||||
1. First-run overlay shows once. Skip stays skipped.
|
||||
2. Capture knobs are on the photo. Each slider redraws the overlay on that photo.
|
||||
3. Confirm stays off until the cut is one piece.
|
||||
4. Vision on and sure: fields filled, still editable.
|
||||
5. Vision unsure: species, wood category, and figure empty, with lines under them. Cannot mark ready until all three are set.
|
||||
6. Species list is only synced names. You cannot type a name that is not there.
|
||||
7. Duplicate SKU: message under SKU, two actions, no status number.
|
||||
8. Stale category: message under that field, current options.
|
||||
|
||||
## 7. UAT can write cases against
|
||||
## Not this pass
|
||||
|
||||
1. First-run overlay appears once; skip persists.
|
||||
2. Knobs on capture; each slider live-reruns mask overlay on the source photo.
|
||||
3. Confirm disabled until mask is contiguous.
|
||||
4. Review with inference ON and confidence ≥ 0.7: fields filled, editable.
|
||||
5. Review with confidence < 0.7: species/wood category/figure empty; inline nudges; cannot mark ready until all three satisfied.
|
||||
6. Species picker contains only synced names; typing a non-synced name is impossible.
|
||||
7. Duplicate SKU: inline SKU recovery, two actions, no raw code.
|
||||
8. Stale category after sync: 422 copy on that field with current options.
|
||||
|
||||
---
|
||||
|
||||
## 8. Non-goals (this spec)
|
||||
|
||||
- Visual branding, type scale, color tokens.
|
||||
- Server U2Net “Try harder” control (deferred).
|
||||
- Configurable 0.7 slider in the UI.
|
||||
- Offline / airplane-mode chrome.
|
||||
Brand colors and type. Server "Try harder" matting. A slider for the 0.7 bar. Airplane-mode capture.
|
||||
|
||||
Reference in New Issue
Block a user