Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| abef27cf2a |
@@ -2,37 +2,31 @@
|
||||
|
||||
## Agent context
|
||||
|
||||
Read `docs/README.md` first, then this file, `docs/CAPTURE.md`, `docs/PHOTOS.md`, and `docs/CREATE.md`.
|
||||
Read this file, `docs/CAPTURE.md`, and `docs/PHOTOS.md`.
|
||||
Implement Milestone 2 only. Specification: `docs/PHOTOS.md`.
|
||||
PRD: `docs/PRODUCT.md` for destination and field names. Do not implement Milestone 3 or 4.
|
||||
Do not widen Milestone 2 into Complete-listing taxonomy, SKU, price, copy, or Woo create.
|
||||
|
||||
Implement **Milestone 3** only. Specification: `docs/CREATE.md`.
|
||||
Product overview: `docs/PRODUCT.md`.
|
||||
## Milestone 2
|
||||
|
||||
**M3 scope** (what is in vs out) comes from Ty conversations and `docs/CREATE.md`.
|
||||
**Behavior** for Woo sync, metadata, pricing, and inference is written into `docs/CREATE.md` from the technical requirements on `main`. Do not treat older milestone splits on `main` as scope authority. Do not code against `main` docs by cross-reference. Use `docs/CREATE.md` on this branch.
|
||||
Photos.
|
||||
Acceptance: on a phone, batch of 1–N photos, user-picked calibration photo through Milestone 1 mask/axis/measure, every successful shot as a 3:4 transparent inventory PNG lined up, Milestone 1 numbers on screen, dimension-overlay copy of the calibration PNG present.
|
||||
|
||||
Do not implement work outside `docs/CREATE.md` unless Ty expands scope in chat.
|
||||
Do not touch deploy port `:10880`. UAT redeploy target is overnight Docker on ubudual5060 `:10881`.
|
||||
Milestone 2 is inference-free. Batch mask/crop reuse and dimension overlays run in the browser as ordinary code. There is no inference URL and no model call in this milestone.
|
||||
|
||||
## Milestone 3
|
||||
## Milestone 1 (historical)
|
||||
|
||||
Create. Exit criteria: a Photos-ready slab is **submission-ready for WooCommerce**, proven by a `SLAB-UAT-*` Woo draft that matches the reviewed ready set. Inference-off path required.
|
||||
|
||||
## Milestone 1 and 2 (shipped inputs)
|
||||
|
||||
- Capture: `docs/CAPTURE.md`
|
||||
- Photos: `docs/PHOTOS.md` (UAT on `:10881`)
|
||||
Capture. Spec: `docs/CAPTURE.md`. Remains the calibration math and PNG contract Milestone 2 reuses.
|
||||
|
||||
## Invariants
|
||||
|
||||
- Quantity per listing is one.
|
||||
- User enters length, thickness, and SKU.
|
||||
- App may propose remaining listing fields. User reviews before create.
|
||||
- App proposes remaining listing fields. User reviews before create.
|
||||
- Non-math fields remain editable until create.
|
||||
- `bdft = sqft × thickness_in`
|
||||
- Price proposal is `bdft × species $/bdft`. User may replace it. Missing rate leaves price empty.
|
||||
- User-entered length, thickness, SKU, price, and taxonomy override computed or inferred values.
|
||||
- **Woo attributes** means synced species and wood categories; Edge, Figure, Grade, Thickness, Moisture; `fig-*` and `feat-*`. Store is source of truth.
|
||||
- Woo auth is WordPress application password (server-side). `WOO_BASE_URL` is compose/env. Inference URL is Settings.
|
||||
- SKUs matching `SLAB-UAT-*` create as Woo draft.
|
||||
- Create works with inference off.
|
||||
- Source below the PNG size floor requires a new photo.
|
||||
|
||||
@@ -2,17 +2,12 @@
|
||||
|
||||
Lists one-of-a-kind wood slabs on Whidbey Wood Store.
|
||||
|
||||
Start at [`docs/README.md`](docs/README.md) for goals, what already shipped, and Milestone 3.
|
||||
|
||||
| Artifact | File |
|
||||
|---|---|
|
||||
| Docs map | `docs/README.md` |
|
||||
| Product overview | `docs/PRODUCT.md` |
|
||||
| PRD | `docs/PRODUCT.md` |
|
||||
| Invariants and agent context | `AGENTS.md` |
|
||||
| Milestone 1 Capture (shipped) | `docs/CAPTURE.md` |
|
||||
| Milestone 2 Photos (shipped) | `docs/PHOTOS.md` |
|
||||
| Milestone 3 Create (current) | `docs/CREATE.md` |
|
||||
| UAT practice walk | `docs/UAT-PRACTICE.md` |
|
||||
| Milestone 1 | `docs/CAPTURE.md` |
|
||||
| Milestone 2 | `docs/PHOTOS.md` |
|
||||
| Hosting | `deploy/` |
|
||||
|
||||
Owner: Ty. Store: https://www.whidbeywoodstore.com/
|
||||
|
||||
+21
-6
@@ -1,20 +1,35 @@
|
||||
# Host
|
||||
|
||||
## Overnight Milestone 2 Capture (UAT)
|
||||
## Overnight Capture UAT (primary — when Tailscale is up)
|
||||
|
||||
- Host: `ubudual5060` / Tailscale `100.64.0.9`
|
||||
- Container: `slabuploader-m2-overnight` (image `slabuploader-m2:overnight`)
|
||||
- Container: `slabuploader-m2-overnight` (image `slabuploader-m2:overnight`) — or salvage rebuild of the same role
|
||||
- Port: **10881** → 80
|
||||
- URL: `http://100.64.0.9:10881`
|
||||
- Replaces prior overnight container `slabuploader-m1-overnight` (same port).
|
||||
|
||||
## Ty local Node/SvelteKit (do not overwrite)
|
||||
## Fallback UAT — Grok Bot computer (when Tailscale to 5060 is down)
|
||||
|
||||
Use this path for salvage / Capture UAT until the box can reach `100.64.0.9` again.
|
||||
|
||||
- Host: Grok Bot computer (slab-m1 box)
|
||||
- Serve: static Capture build (`frontend` → `npm run build`, then `vite preview` or equivalent static server)
|
||||
- Port: **18181** (chosen so it never collides with Ty local on 5060)
|
||||
- Bind: `0.0.0.0:18181`
|
||||
- URL (from this box): `http://127.0.0.1:18181` — Ty reaches it via the bot computer / whatever path the app exposes; Tailscale IP of this box when mesh returns
|
||||
- Branch for salvage: `salvage-one-woo-draft` (cut from `m2-ds`)
|
||||
- Do **not** require Docker on the box for this fallback.
|
||||
|
||||
When Tailscale is restored, prefer primary overnight `:10881` again and stop advertising the fallback as the main UAT URL.
|
||||
|
||||
## Ty local Node/SvelteKit on 5060 (do not overwrite)
|
||||
|
||||
- Host: `ubudual5060` / `100.64.0.9`
|
||||
- Container: `slabuploader-m1`
|
||||
- Port: **18080** → 80 (inventory previously listed 10880; do not bind overnight there)
|
||||
- Port: **18080** → 80
|
||||
- URL: `http://100.64.0.9:18080`
|
||||
- **Never** recreate or remap this container for overnight UAT.
|
||||
- **Never** recreate or remap this container for overnight or box-fallback UAT.
|
||||
- **Never** bind overnight or fallback UAT to **10880** either (legacy Ty local).
|
||||
|
||||
Future overnight Capture deploys use a **separate** container name and an open **108XX** port other than Ty’s local port.
|
||||
Future overnight Capture deploys use a **separate** container name and an open **108XX** port other than Ty’s local ports.
|
||||
Do not put a host name in the product spec.
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
# Listing copy and the store
|
||||
|
||||
> On branch `m3-ds`, Milestone 3 exit and scope live in `docs/CREATE.md`. This file is the listing-copy and Woo create/payload reference brought from `main` so coding does not need to open `main`. If anything conflicts with a Ty conversation lock in `CREATE.md`, `CREATE.md` wins.
|
||||
|
||||
|
||||
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. Titles and descriptions
|
||||
|
||||
### 1.1 Templates (always on)
|
||||
Templates live in Settings. Defaults below. Placeholders use `{field}`.
|
||||
|
||||
**title** (default):
|
||||
```
|
||||
{species} {length_ft}ft {length_in_remainder}in × {width_avg_in}in Wood Slab — {character}
|
||||
```
|
||||
Example: `Black Walnut 16ft 0in × 18.2in Wood Slab — Cathedral`
|
||||
- `length_ft` = floor(length_in / 12), `length_in_remainder` = length_in mod 12 (1 dp).
|
||||
|
||||
**short_title** (default):
|
||||
```
|
||||
{species} Slab — {character}
|
||||
```
|
||||
Example: `Black Walnut Slab — Cathedral`
|
||||
|
||||
**description** (default, multi-paragraph):
|
||||
```
|
||||
A one-of-a-one {character} {species} slab, {length_ft}ft {length_in_remainder}in long with an
|
||||
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` 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 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
|
||||
|
||||
### 2.1 Auth & client
|
||||
- REST v3 surface unchanged: `GET/POST/PATCH/DELETE {WOO_BASE_URL}/wp-json/wc/v3/…`
|
||||
- Auth is **WordPress Application Passwords**, not WooCommerce consumer keys
|
||||
(`ck_`/`cs_`) and not the user's login password.
|
||||
- Create a **dedicated low-privilege WordPress user** (shop manager, or a custom
|
||||
role with product create/edit + media upload). A dedicated low-privilege user, not the admin account, is used.
|
||||
- Generate the application password under **Users → Profile → Application
|
||||
Passwords**. WordPress requires **HTTPS** for that UI; the store must already
|
||||
be on TLS (Caddy enforces HTTPS on the app side too).
|
||||
- Basic auth shape: `woo_wp_username:woo_app_password` (username + application
|
||||
password). Spaces in the generated password are ignored by WordPress; store
|
||||
the value as entered.
|
||||
- FastAPI is the sole Woo caller. Credentials live encrypted at rest in SQLite
|
||||
(AES-GCM, `SLAB_AES_KEY` env). The browser never holds them.
|
||||
- Rotation: revoke the application password in the WP user profile, enter a new
|
||||
one in Settings UI, then `POST /api/v1/settings/test-woo`.
|
||||
- Client: `httpx` with timeouts (connect 10s, read 60s for image-heavy create).
|
||||
- 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 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)
|
||||
3. `GET /products/attributes` (+ terms) → five locked attributes
|
||||
|
||||
Rebuilds cache tables in one transaction. Failure → `502 woo_error`; previous
|
||||
cache retained (never delete-then-fail).
|
||||
|
||||
**Manufactured stamps.** Woo REST has **no** usable last-modified on categories,
|
||||
attributes, or tags. FastAPI persists `taxonomy_last_sync_at` (every successful
|
||||
pull) and `taxonomy_anchor` (bump only when stored-subset hash changes, including
|
||||
deletions). Client re-pulls the full taxonomy iff the anchor advanced. No Woo-side
|
||||
taxonomy timestamp; no per-navigation sync.
|
||||
|
||||
**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. If a picked id vanished after a refresh, show that field as no longer valid. The selection is not silently swapped.
|
||||
|
||||
**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 What goes on the listing
|
||||
|
||||
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.
|
||||
|
||||
If it is less sure, those fields stay empty. That empty-start subset is not
|
||||
the submit list. Review will not continue until the full ready set is filled:
|
||||
SKU, length, thickness, price, species, wood category, edge type, figure,
|
||||
grade, thickness band, moisture, fig-* tags, and inventory PNG(s). Species
|
||||
plus one figure is not enough. Presentation: `docs/UX.md`.
|
||||
Low-confidence guesses are not filled.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Field rules:
|
||||
- **Categories**: species leaf (1 required) plus wood-category leaf ids (1+).
|
||||
- **Tags**: `fig-*` (1+ required) and `feat-*` (0+). Only documented
|
||||
character or species product tags are used. Woo is authoritative for tag definitions.
|
||||
- **Attributes**: the five locked Woo attributes only — Edge Type, Figure, Grade,
|
||||
Thickness, Moisture — using term ids from the cache. Species,
|
||||
character, length, or free-text thickness as product attributes. Missing
|
||||
attributes are skipped, not created (v1: no attribute creation).
|
||||
|
||||
Vision only suggests. It never blocks publish. With vision off, everything is typed.
|
||||
|
||||
### 2.4 Create payload
|
||||
`POST /products` — body (only non-null fields sent):
|
||||
```jsonc
|
||||
{
|
||||
"name": "{title}",
|
||||
"type": "simple",
|
||||
"status": "{woo_create_status}", // draft | publish; default draft; SLAB-UAT-* forced draft
|
||||
"sku": "{sku}",
|
||||
"regular_price": "{price}", // 2 dp string, e.g. "2730.00"
|
||||
"description": "{description}",
|
||||
"short_description": "{short_title}",
|
||||
"categories": [ { "id": <species category id> } ],
|
||||
"tags": [ { "id": <fig-* tag id> }, { "id": <feat-* tag id> }, … ],
|
||||
"images": [
|
||||
{ "src": "<data URL or pre-uploaded attachment URL>", "alt": "{species} {character} slab" }
|
||||
],
|
||||
"attributes": [ … if present … ],
|
||||
"virtual": false,
|
||||
"downloadable": false,
|
||||
"catalog_visibility": "visible"
|
||||
}
|
||||
```
|
||||
**Image upload strategy** (Woo v3): `images[].src` accepts a URL or a data URL.
|
||||
For reliability the server:
|
||||
1. For each customer-facing inventory photo in `seq` order (processed transparent PNGs
|
||||
from the client TECH-SPEC path): `POST /media` with the **PNG bytes** → get attachment
|
||||
`source_url`. Woo accepts `image/*`; keep alpha. JPEG re-encoding drops alpha and must not happen.
|
||||
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 `woo_create_status` (`draft` or `publish`, default
|
||||
`draft`). Practice SKUs `SLAB-UAT-*` are always created as drafts, even if Settings
|
||||
say publish. Existing SKUs are not edited 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 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 After a failed publish
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
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
|
||||
a manual cleanup note. Not auto-deleted.
|
||||
|
||||
## 3. End-to-end publish sequence (server side)
|
||||
```
|
||||
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 the model may and may not do
|
||||
|
||||
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.
|
||||
|
||||
Presentation: `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.
|
||||
-431
@@ -1,431 +0,0 @@
|
||||
# Milestone 3 Create
|
||||
|
||||
## Purpose
|
||||
|
||||
Milestone 3 takes a Photos-ready device draft and makes the slab **submission-ready for WooCommerce**.
|
||||
|
||||
Submission-ready means the mill owner has filled the full store listing package against **live synced Woo attributes**, with price set, and can create the product in the store (practice SKUs as Woo drafts). Photos and measures from Milestones 1 and 2 are inputs. They are not enough by themselves.
|
||||
|
||||
**Exit criteria.** On the M3 UAT deploy, a Photos-ready slab reaches submission-ready and a `SLAB-UAT-*` product exists in WooCommerce as a **draft** whose categories, attributes, tags, price, and inventory images match the reviewed ready set. Create still works with inference off.
|
||||
|
||||
## How to read this file
|
||||
|
||||
This file is the Milestone 3 source of truth on branch `m3-ds`.
|
||||
|
||||
- **What is in M3** comes from Ty conversations on this branch (not from older milestone splits on `main`).
|
||||
- **How Woo sync, metadata, pricing, and inference behave** is taken from the technical requirements on `main` (`CONTENT-WOO`, `DATA-MODEL`, `UX`, `ARCHITECTURE`, `OPENAPI`, `AGENTS`, `UAT-PRACTICE`, `DEPLOYMENT`) and written here so coding does not need to open `main`.
|
||||
|
||||
Related shipped specs: `docs/CAPTURE.md`, `docs/PHOTOS.md`. Listing copy and Woo payload detail: `docs/CONTENT-WOO.md` (from `main`). Product overview: `docs/PRODUCT.md`. Folder map: `docs/README.md`.
|
||||
|
||||
## Preconditions (already shipped)
|
||||
|
||||
On this branch before M3 work:
|
||||
|
||||
- Milestone 1 Capture works in the browser (`docs/CAPTURE.md`).
|
||||
- Milestone 2 Photos works in the browser (`docs/PHOTOS.md`). UAT evidence on `:10881`.
|
||||
- Device draft holds Photos-ready inventory PNGs, calibration measures, and related state.
|
||||
- `frontend/` holds that client path. `backend/` does not yet implement the Woo client or inference proxy.
|
||||
|
||||
M3 adds the server path, Settings, taxonomy sync, review/ready fields, pricing, inference dependency, and Woo create.
|
||||
|
||||
## In scope
|
||||
|
||||
1. WooCommerce auth with WordPress application password (server-side).
|
||||
2. Sync of **Woo attributes** from the store and correct use of those lists for the listing.
|
||||
3. Review UI and rules to reach submission-ready (full ready set).
|
||||
4. Pricing proposal from species `$/bdft` rules and user override.
|
||||
5. OpenAI-compatible inference endpoint as a Settings dependency, with Call 1 / Call 2 behavior when enabled.
|
||||
6. Create product in WooCommerce (force draft for `SLAB-UAT-*`), then clear the device draft on success.
|
||||
|
||||
## Out of scope unless Ty expands M3 in chat
|
||||
|
||||
- Creating new Woo categories, attributes, terms, or tags from the app.
|
||||
- Updating an existing store SKU in place.
|
||||
- Bulk create.
|
||||
- Touching deploy port `:10880`.
|
||||
- Dumping work onto `m2-ds` or doc-simplification branches.
|
||||
|
||||
---
|
||||
|
||||
## Woo attributes
|
||||
|
||||
Locked name for the synced taxonomy package. WooCommerce is the source of truth. The app caches ids. It does not invent the lists.
|
||||
|
||||
### Categories (product categories, leaf only)
|
||||
|
||||
| Field | Cardinality | Rule |
|
||||
|---|---|---|
|
||||
| Species | exactly 1 | Synced species leaves only. No hardcoded or fallback species set. A new store species appears only after the next successful sync. |
|
||||
| Wood category | 1 or more | Leaf ids. Parents are grouping only. |
|
||||
|
||||
### Attributes (exactly five locked Woo attributes)
|
||||
|
||||
| Attribute | Cardinality | Rule |
|
||||
|---|---|---|
|
||||
| Edge Type | 1 | Term id from sync. |
|
||||
| Figure | 1 or more | Term ids from sync. |
|
||||
| Grade | 1 or more | Term ids from sync. |
|
||||
| Thickness | 1 | Measured thickness rounded **up** into the store band (example: 1" lands in the store band labeled like `1"-1 1/2"` when that is the store label). |
|
||||
| Moisture | 1 | Defaults to kiln-dried. User sets or keeps it. Never inferred. |
|
||||
|
||||
v1 does not create missing attributes or terms. If a term is missing in Woo, skip it. Do not invent it.
|
||||
|
||||
Species, character, length, and free-text thickness are **not** Woo product attributes. Only the five locked attributes go on the create payload.
|
||||
|
||||
### Tags
|
||||
|
||||
| Prefix | Cardinality | Rule |
|
||||
|---|---|---|
|
||||
| `fig-*` | 1 or more | Required. Other Woo tags are ignored at sync. |
|
||||
| `feat-*` | 0 or more | Optional. Character, inclusions, voids, and checks map onto existing `feat-*` tags only. |
|
||||
|
||||
---
|
||||
|
||||
## Auth and store URL
|
||||
|
||||
From `main` deploy and agents rules, adopted here:
|
||||
|
||||
- FastAPI is the only Woo caller. The browser never holds store credentials.
|
||||
- Auth is a WordPress **application password** for a dedicated low-privilege WP user (shop manager or custom role with product create/edit and media upload). Not WooCommerce consumer keys (`ck_` / `cs_`). Not the store admin login password.
|
||||
- Basic auth shape: `woo_wp_username` + `woo_app_password`.
|
||||
- Store URL is **`WOO_BASE_URL`** from `deploy/.env` / Docker Compose (`https://` only). Injected into the FastAPI container. Settings shows it read-only. Not a SQLite settings row. Change store means edit `.env` and recreate the FastAPI container.
|
||||
- Username and application password are entered in Settings UI, stored AES-GCM encrypted in SQLite (`SLAB_AES_KEY` env).
|
||||
- REST surface: `{WOO_BASE_URL}/wp-json/wc/v3/…`
|
||||
- Client: `httpx` with connect timeout 10s and read timeout 60s for image-heavy create.
|
||||
- Rotate: revoke the application password in WP Users → Profile, enter a new one in Settings, then run `test-woo`.
|
||||
|
||||
---
|
||||
|
||||
## Taxonomy sync
|
||||
|
||||
Endpoint and behavior from `main` OPENAPI / CONTENT-WOO / ARCHITECTURE:
|
||||
|
||||
`POST /api/v1/admin/taxonomy/sync` pulls, in order:
|
||||
|
||||
1. `GET /products/categories?per_page=100` (paginate) → species and wood-category leaves after app filter.
|
||||
2. `GET /products/tags?per_page=100` → keep only `fig-*` and `feat-*`.
|
||||
3. `GET /products/attributes` plus terms → the five locked attributes.
|
||||
|
||||
Rebuild cache tables in one transaction. On failure return `502 woo_error` and **keep** the previous cache. Never delete-then-fail.
|
||||
|
||||
**Stamps.** Woo REST has no usable last-modified for categories, attributes, or tags. FastAPI stores:
|
||||
|
||||
- `taxonomy_last_sync_at` on every successful pull
|
||||
- `taxonomy_anchor` bumped only when the stored-subset hash changes (including deletions)
|
||||
|
||||
Client re-pulls the full taxonomy only when the anchor advances.
|
||||
|
||||
**Triggers.**
|
||||
|
||||
1. Successful `POST /api/v1/settings/test-woo` always resyncs.
|
||||
2. On publish, if `now - taxonomy_last_sync_at > 1 hour`, reconcile (bump anchor only on real change).
|
||||
3. Optional app launch or Settings refresh.
|
||||
|
||||
If a picked id disappeared after refresh, show that field as no longer valid. Do not silently swap another value.
|
||||
|
||||
`GET /api/v1/admin/taxonomy` returns the cached groups plus `taxonomy_anchor` and `taxonomy_last_sync_at`.
|
||||
|
||||
---
|
||||
|
||||
## Submission-ready (ready set)
|
||||
|
||||
A slab is submission-ready only when **all** of the following are present. Species plus one figure is not enough.
|
||||
|
||||
Required:
|
||||
|
||||
- At least one inventory PNG (from Milestone 2)
|
||||
- SKU matching `[A-Z0-9-]{3,24}` (practice runs use `SLAB-UAT-*`)
|
||||
- Length and thickness (from capture, still editable on review)
|
||||
- Client-computed widths, sqft, and bdft (user typed number wins if overridden)
|
||||
- Exactly one species leaf
|
||||
- At least one wood-category leaf
|
||||
- One edge type term
|
||||
- At least one figure term
|
||||
- At least one grade term
|
||||
- Thickness band term (round-up from measured thickness)
|
||||
- One moisture term (default kiln-dried)
|
||||
- At least one `fig-*` tag
|
||||
- Price (see Pricing)
|
||||
|
||||
Optional:
|
||||
|
||||
- `feat-*` tags (zero or more)
|
||||
- Title, short title, description (may be typed, filled by templates at create, or written by Generate text when inference is on). Not required to reach ready.
|
||||
|
||||
Inline per-field nudges show empty required fields as the owner works. The slab cannot reach ready until the full set is filled.
|
||||
|
||||
Server status name for this state: `ready` (see data model below). Publish/create is blocked with `422 unpublishable` until then.
|
||||
|
||||
---
|
||||
|
||||
## Pricing
|
||||
|
||||
From `main` DATA-MODEL / UX / AGENTS:
|
||||
|
||||
- Settings (or admin API) holds `pricing_rules`: one flat `$/bdft` per species name that matches a synced species leaf. No tiers in v1.
|
||||
- Seed rule rows from synced species. The mill owner sets the rate per species.
|
||||
- Proposal: `price = bdft × price_per_bdft` for the selected species.
|
||||
- User may replace the proposal. Override sticks (`price_source` = `override` vs `recommendation`).
|
||||
- If no rule exists for the species, price starts empty and the owner types it.
|
||||
- Publish/create is allowed with a manual price.
|
||||
- `bdft = sqft × thickness_in` (deterministic).
|
||||
|
||||
Admin API surface from `main` OPENAPI:
|
||||
|
||||
- `GET /api/v1/admin/pricing`
|
||||
- `POST /api/v1/admin/pricing`
|
||||
- `PUT /api/v1/admin/pricing/{id}`
|
||||
- `DELETE /api/v1/admin/pricing/{id}`
|
||||
|
||||
---
|
||||
|
||||
## Inference
|
||||
|
||||
M3 includes the OpenAI-compatible endpoint as a real server dependency and the Call 1 / Call 2 rules below. Create must pass with inference **off**.
|
||||
|
||||
### Settings (not compose)
|
||||
|
||||
Stored encrypted in SQLite Settings (not `deploy/.env`):
|
||||
|
||||
- `inference_base_url`
|
||||
- `inference_api_key`
|
||||
- `inference_model`
|
||||
- `inference_enabled`
|
||||
- `content_llm_enabled`
|
||||
|
||||
FastAPI proxies all inference calls. The browser never holds the key.
|
||||
|
||||
`POST /api/v1/settings/test-inference` returns `{ ok, mode: stateful|stateless|fail, vision_capable, detail }`. Mode `fail` blocks Call 2 enablement only. Inference-off create still works.
|
||||
|
||||
### Call 1 (vision / taxonomy assist)
|
||||
|
||||
- Runs only when `inference_enabled` is true (auto after calibrated draft upload when on, or via `POST /api/v1/slabs/{id}/infer-taxon`).
|
||||
- Input: inventory photos downscaled to max 1024px, user metadata, synced Woo taxonomy lists.
|
||||
- Output: species, wood categories, edge, figure, grade, and feature-tag suggestions constrained to synced lists, each with confidence.
|
||||
- Default confidence threshold 0.7 (configurable). At or above threshold: pre-fill on review, still editable. Below: leave empty. Do not show low-confidence guesses as defaults.
|
||||
- Moisture is never inferred.
|
||||
- Typed values always win over model values.
|
||||
- Vision never blocks create and never auto-publishes.
|
||||
|
||||
### Call 2 (Generate text)
|
||||
|
||||
- User-triggered only (`POST /api/v1/slabs/{id}/infer-content`). Never auto.
|
||||
- Requires successful `test-inference` (`ok: true`).
|
||||
- Model writes prose only (character, color, grain, uses). It must not invent dimensions.
|
||||
- Server assembles title, short title, and description from templates with prose injected.
|
||||
- If the endpoint is down, disable the button. Templates still work. Create is never blocked by Generate text failure.
|
||||
|
||||
### Templates (always available)
|
||||
|
||||
Full defaults and Woo payload text live in `docs/CONTENT-WOO.md`. Summary here:
|
||||
|
||||
**title**
|
||||
|
||||
```
|
||||
{species} {length_ft}ft {length_in_remainder}in × {width_avg_in}in Wood Slab - {character}
|
||||
```
|
||||
|
||||
Example from CONTENT-WOO: `Black Walnut 16ft 0in × 18.2in Wood Slab - Cathedral`
|
||||
|
||||
**short_title**
|
||||
|
||||
```
|
||||
{species} Slab - {character}
|
||||
```
|
||||
|
||||
**description** (multi-paragraph)
|
||||
|
||||
```
|
||||
A one-of-a-one {character} {species} slab, {length_ft}ft {length_in_remainder}in long with an
|
||||
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}
|
||||
```
|
||||
|
||||
**What `{character}` means (locked from CONTENT-WOO example + PROMPTS on `main`).**
|
||||
The word after the dash in the example is `Cathedral`, which is a **Figure** attribute term.
|
||||
Fill `{character}` with the primary (first) Figure term **name** from the ready set.
|
||||
If somehow no figure is present, leave that slot empty (ready set normally requires ≥1 figure).
|
||||
Do not confuse this with `feat-*` tags. Call 1 free-text "character / inclusions / voids / checks" map to `feat-*` tags, not to this title slot.
|
||||
|
||||
All template math fields come from the deterministic pipeline, never from the LLM.
|
||||
|
||||
---
|
||||
|
||||
## Review and create flow
|
||||
|
||||
Mill-owner path after Photos-ready:
|
||||
|
||||
1. Upload calibrated / Photos-ready draft to the server (`POST /api/v1/slabs` multipart) when the M3 client reaches that step. Status becomes `calibrated`. If inference is on, Call 1 may run.
|
||||
2. **Review.** Owner fills or overrides every ready-set field. Pickers show only synced Woo attributes.
|
||||
3. When the ready set is complete, status becomes `ready` (submission-ready).
|
||||
4. Owner taps create/publish → `POST /api/v1/slabs/{id}/publish`.
|
||||
5. Server runs the create sequence below. Client polls `GET /api/v1/slabs/{id}`.
|
||||
|
||||
### Create sequence (server)
|
||||
|
||||
1. Check the listing against the ready-set rules.
|
||||
2. Look up SKU in the store (`GET /products?sku={sku}`, any status). Stop if found.
|
||||
3. Refresh taxonomy if last successful pull is older than 1 hour.
|
||||
4. Resolve categories, tags, and attributes from the synced lists.
|
||||
5. Upload inventory PNGs via `POST /media`, keep alpha. Reference `source_url` values on the product. Cap at 5 images. **Woo image order (locked):** (1) clean calibration inventory PNG, (2) dimension-overlay copy of that calibration PNG, (3+) remaining batch inventory PNGs in the order they were added in the web app. The overlay is part of the submission. Do not upload non-inventory artifacts outside that lineup.
|
||||
6. `POST /products` with status from Settings `woo_create_status` (`draft` or `publish`, default `draft`). **`SLAB-UAT-*` always forces draft.**
|
||||
7. On success: log, set `woo_product_id`, mark published locally, delete original and processed photos, clear device draft.
|
||||
8. On failure: log, mark failed, keep photos, put a field-level error on the phone. No raw status codes for the mill owner.
|
||||
|
||||
### Create payload (shape)
|
||||
|
||||
`POST /products` sends only non-null fields. Conceptual body:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "{title}",
|
||||
"type": "simple",
|
||||
"status": "{woo_create_status}",
|
||||
"sku": "{sku}",
|
||||
"regular_price": "{price}",
|
||||
"description": "{description}",
|
||||
"short_description": "{short_title}",
|
||||
"categories": [ { "id": <species leaf id> }, { "id": <wood-category leaf id> }, ... ],
|
||||
"tags": [ { "id": <fig-* and feat-* ids> } ],
|
||||
"images": [ { "src": "<media source_url>", "alt": "{species} {character} slab" } ],
|
||||
"attributes": [ /* Edge, Figure, Grade, Thickness, Moisture term ids when present */ ],
|
||||
"virtual": false,
|
||||
"downloadable": false,
|
||||
"catalog_visibility": "visible"
|
||||
}
|
||||
```
|
||||
|
||||
App "published" means the store create succeeded. That is not the same as the Woo product being live when status is draft.
|
||||
|
||||
### Duplicate SKU
|
||||
|
||||
Before create, if the SKU exists in any store status: block. Show Edit SKU and Open existing listing. This version does not update the other product. API: `409 duplicate_sku`.
|
||||
|
||||
### Stale taxonomy on submit
|
||||
|
||||
If a category, attribute, or price is no longer valid after refresh: under that field, tell the owner to pick from the current list and supply current options (`stale_field`).
|
||||
|
||||
---
|
||||
|
||||
## Settings keys (M3)
|
||||
|
||||
| Key | Where | Notes |
|
||||
|---|---|---|
|
||||
| `WOO_BASE_URL` | compose / `.env` | HTTPS store URL. Read-only in UI. |
|
||||
| `SLAB_AES_KEY` | compose / `.env` | AES master key. Never in DB or repo. |
|
||||
| `woo_wp_username` | Settings (encrypted) | Dedicated WP user. |
|
||||
| `woo_app_password` | Settings (encrypted) | Application password. |
|
||||
| `woo_create_status` | Settings | `draft` or `publish`. Default `draft`. |
|
||||
| `inference_base_url` | Settings (encrypted) | OpenAI-compatible base URL. |
|
||||
| `inference_api_key` | Settings (encrypted) | |
|
||||
| `inference_model` | Settings | |
|
||||
| `inference_enabled` | Settings | |
|
||||
| `content_llm_enabled` | Settings | Gates Generate text. |
|
||||
| `taxonomy_last_sync_at` | Settings / derived | |
|
||||
| `taxonomy_anchor` | Settings / derived | |
|
||||
| `brand_voice`, `geo_context` | Settings | Copy helpers. |
|
||||
|
||||
Relevant Settings API: `GET/PUT /api/v1/settings`, `POST /api/v1/settings/test-woo`, `POST /api/v1/settings/test-inference`.
|
||||
|
||||
---
|
||||
|
||||
## Data shape (submission-ready fields)
|
||||
|
||||
Core slab fields M3 must persist (from `main` DATA-MODEL, adopted here):
|
||||
|
||||
| Field | Rule |
|
||||
|---|---|
|
||||
| `sku` | Required. Unique. |
|
||||
| `status` | `draft` → `calibrated` → `ready` → `publishing` → `published`, or `failed` |
|
||||
| `length_in`, `thickness_in` | Required. 1/8" step. |
|
||||
| `sqft`, `bdft`, widths | From client math. |
|
||||
| `species_id` | 1 species leaf. |
|
||||
| `wood_category_ids` | 1 or more. |
|
||||
| `edge_type_term_id` | 1. |
|
||||
| `figure_term_ids` | 1 or more. |
|
||||
| `grade_term_ids` | 1 or more. |
|
||||
| `thickness_term_id` | Round-up band. |
|
||||
| `moisture_term_id` | 1. Default kiln-dried. |
|
||||
| `fig_tag_ids` | 1 or more. |
|
||||
| `feat_tag_ids` | 0 or more. |
|
||||
| `price`, `price_per_bdft`, `price_source` | See Pricing. |
|
||||
| `title`, `short_title`, `description` | Optional for ready. |
|
||||
|
||||
Cache tables: `woo_categories`, `woo_attributes`, `woo_attribute_terms`, `woo_tags`, `pricing_rules`.
|
||||
|
||||
Device draft (IndexedDB on the phone) is not a WooCommerce draft. Clear the device draft after successful create.
|
||||
|
||||
---
|
||||
|
||||
## Practice / UAT rules
|
||||
|
||||
- Every practice listing uses SKU prefix `SLAB-UAT-*`.
|
||||
- `SLAB-UAT-*` always creates a WooCommerce draft, even if Settings say publish.
|
||||
- Customers must not see practice listings on the storefront.
|
||||
- Inference is **off** for the claimable exit walk. Optional ON path only after inference Settings pass `test-inference`.
|
||||
- UAT deploy for this branch: ubudual5060 `http://100.64.0.9:10881`. Do not use `:10880`.
|
||||
|
||||
---
|
||||
|
||||
## API surface M3 must implement (summary)
|
||||
|
||||
Base path `/api/v1` (health may also be `/api/health`).
|
||||
|
||||
| Area | Methods |
|
||||
|---|---|
|
||||
| Health | `GET /api/health` |
|
||||
| Slabs | `POST/GET/PUT/DELETE /api/v1/slabs`, `GET /api/v1/slabs/{id}` |
|
||||
| Photos | original/processed GET while retained |
|
||||
| Inference | `POST .../infer-taxon`, `POST .../infer-content` |
|
||||
| Publish | `POST .../publish` → 202, poll slab |
|
||||
| Taxonomy | `GET/POST /api/v1/admin/taxonomy`, `.../sync` |
|
||||
| Pricing | CRUD `/api/v1/admin/pricing` |
|
||||
| Settings | `GET/PUT /api/v1/settings`, `test-woo`, `test-inference` |
|
||||
|
||||
Full request and response field lists on historical `main` `docs/OPENAPI.md` are the detail reference if a field is missing here. Prefer extending this file over coding from `main` directly.
|
||||
|
||||
---
|
||||
|
||||
## Done when (exit criteria)
|
||||
|
||||
**Product state induced.** Overnight UAT app on ubudual5060 `http://100.64.0.9:10881` (or the current M3 image of this branch), with `WOO_BASE_URL` and Woo application password pointed at the intended store.
|
||||
|
||||
**Evidence from that state.**
|
||||
|
||||
1. `test-woo` succeeds and taxonomy sync fills Woo attributes into the app cache. Pickers offer only synced ids.
|
||||
2. A Photos-ready slab can be filled to the **full ready set** (submission-ready) using those lists and pricing rules, with **inference off**.
|
||||
3. Create with `SLAB-UAT-*` yields a WooCommerce **draft** whose categories, attributes, tags, price, and inventory images match the reviewed ready set.
|
||||
4. Duplicate SKU is blocked. Device draft clears after success.
|
||||
5. Inference Settings accept an OpenAI-compatible base URL, key, and model. `test-inference` runs. Create still succeeds with inference disabled.
|
||||
|
||||
## Dishonest proxies (do not count as pass)
|
||||
|
||||
- Hardcoded or fixture taxonomy lists treated as synced.
|
||||
- Mock Woo client that never hits the real store for the exit walk.
|
||||
- Unit tests alone without a real `SLAB-UAT-*` draft in the store.
|
||||
- Consumer-key auth instead of application password.
|
||||
- Inference "wired" as dead config with no server proxy, or create that requires inference on.
|
||||
- Claiming submission-ready when required ready-set fields are still empty.
|
||||
- Touching `:10880` or parking work on `m2-ds` / doc-simplification.
|
||||
|
||||
## Open decisions (must lock before task breakdown)
|
||||
|
||||
Locked from Ty conversation or from `docs/CONTENT-WOO.md`:
|
||||
|
||||
1. **`categories` on `POST /products`.** **Locked from CONTENT-WOO field rules:** send every ready-set category id (species leaf + each wood-category leaf) as `[{"id": n}, ...]`. The single-id example in CONTENT-WOO §2.4 is incomplete relative to those field rules.
|
||||
|
||||
2. **Template `{character}`.** **Locked from CONTENT-WOO example (`… Wood Slab - Cathedral`):** use the primary Figure term **name**. `feat-*` tags are separate (Call 1 free-text character/inclusions/voids/checks map there).
|
||||
|
||||
3. **Dimension-overlay PNG.** **Locked (Ty 2026-09-16):** Woo image order is (1) clean calibration PNG, (2) dimension-overlay copy, (3+) other inventory PNGs in add order. Cap ≤5.
|
||||
|
||||
Still open (CONTENT-WOO does not fully settle these):
|
||||
|
||||
4. **`attributes` element shape on `POST /products`.** CONTENT-WOO still has `"attributes": [ … if present … ]` with no fields. Need a live Whidbey product JSON sample, or Ty pointing at the official Woo REST fields to use. Do not invent.
|
||||
|
||||
5. **Server persistence.** CONTENT-WOO describes create sequence and payload. It does not choose full OpenAPI slab CRUD vs a thinner FastAPI path on this branch. Still need a lock before coding the server store.
|
||||
|
||||
Non-blocking notes (shipped M1/M2 win on this branch): fill target 90% and 800px floor (not main's 80%/1600); Vite + Svelte SPA (not SvelteKit docs); overlay draw spacing may differ from measure spacing by design.
|
||||
|
||||
If store-specific term labels differ from examples in this file, the synced store lists win.
|
||||
+2
-2
@@ -70,6 +70,6 @@ On a phone UAT build:
|
||||
## Out of scope
|
||||
|
||||
- Complete-listing fields (SKU, species, category, edge, figure, grade, thickness band, moisture, `fig-*` / `feat-*`, title, short title, description, price)
|
||||
- WooCommerce create and submission-ready taxonomy (Milestone 3: `docs/CREATE.md`)
|
||||
- Work beyond `docs/CREATE.md` unless Ty expands M3 in chat
|
||||
- WooCommerce create (Milestone 3)
|
||||
- Inference / Assist (Milestone 4)
|
||||
- Changing Milestone 1 mask/axis/measure math except to reuse its outputs for batch apply and overlays
|
||||
|
||||
+36
-27
@@ -1,4 +1,4 @@
|
||||
# Product overview
|
||||
# Product requirements
|
||||
|
||||
SlabUploader creates one WooCommerce listing per wood slab for Whidbey Wood Store.
|
||||
|
||||
@@ -6,12 +6,8 @@ Each slab is one unit and one listing. Photos and metadata are the inventory rec
|
||||
|
||||
Photographs are taken of the slab on a green or black sheet. That is the capture starting state.
|
||||
|
||||
Owner: Ty. Store: https://www.whidbeywoodstore.com/
|
||||
|
||||
## Complete listing
|
||||
|
||||
A finished listing has:
|
||||
|
||||
- Inventory photo(s), uniform 3:4 transparent PNG
|
||||
- SKU, length, thickness, price
|
||||
- Species, wood category, edge type, figure, grade
|
||||
@@ -20,9 +16,7 @@ A finished listing has:
|
||||
- Title, short title, description
|
||||
- Zero or more `feat-*` tags
|
||||
|
||||
Store categories, attributes, and `fig-*` / `feat-*` tags are the filters shoppers use. Measured values feed title and description templates.
|
||||
|
||||
The locked name for that synced taxonomy package is **Woo attributes**. Details live in `docs/CREATE.md`.
|
||||
Store categories, attributes, and `fig-*` / `feat-*` tags are the filters. Measured values feed title and description templates.
|
||||
|
||||
## Decision rights
|
||||
|
||||
@@ -36,33 +30,48 @@ The locked name for that synced taxonomy package is **Woo attributes**. Details
|
||||
| Price | App proposal `bdft × species $/bdft`; user may replace |
|
||||
| Title, short title, description | Templates from listing facts; user may edit |
|
||||
|
||||
User-entered length, thickness, SKU, price, and taxonomy override computed or inferred values.
|
||||
|
||||
## System seams
|
||||
|
||||
Milestones 1 and 2 run in the browser. No backend and no inference URL on the shipped path.
|
||||
|
||||
Milestones 1 and 2 run in the browser. No backend and no inference URL.
|
||||
Milestone 1 handoff is the device draft: source photo, approved mask, inventory PNG, length, thickness, widths, sqft, bdft.
|
||||
|
||||
Milestone 2 extends the draft with the photo batch, calibration choice, per-photo inventory PNGs, and the dimension-overlay calibration copy.
|
||||
Milestone 3 uses FastAPI as the Woo client. Store credentials stay on the server.
|
||||
|
||||
Milestone 3 introduces FastAPI as the Woo client and inference proxy. Store credentials and inference secrets stay on the server. Spec: `docs/CREATE.md`.
|
||||
## Create
|
||||
|
||||
## Create rules (summary)
|
||||
`SLAB-UAT-*` creates as a WooCommerce draft (store status).
|
||||
Duplicate SKU blocks create.
|
||||
A listing can be created with inference off.
|
||||
A device draft in the app is separate from a WooCommerce draft.
|
||||
|
||||
Full behavior is in `docs/CREATE.md`.
|
||||
## Milestones
|
||||
|
||||
- `SLAB-UAT-*` creates as a WooCommerce draft (store status).
|
||||
- Duplicate SKU blocks create.
|
||||
- A listing can be created with inference off.
|
||||
- A device draft in the app is separate from a WooCommerce draft.
|
||||
Work is split into four milestones. Each milestone is done when its acceptance can be demonstrated.
|
||||
|
||||
## Milestone status on `m3-ds`
|
||||
### Milestone 1 — Capture
|
||||
|
||||
| Milestone | Spec | Status |
|
||||
|---|---|---|
|
||||
| 1 Capture | `docs/CAPTURE.md` | Shipped |
|
||||
| 2 Photos | `docs/PHOTOS.md` | Shipped (UAT `:10881`) |
|
||||
| 3 Create | `docs/CREATE.md` | Current work. Exit: submission-ready for WooCommerce (`CREATE.md` Done when). Scope from Ty conversations; behavior for sync/metadata/pricing/inference taken from `main` into `CREATE.md`. |
|
||||
Spec: `docs/CAPTURE.md`. Done historically; handoff into Milestone 2.
|
||||
|
||||
Anything beyond `docs/CREATE.md` is out of M3 unless Ty expands scope in chat.
|
||||
User stories:
|
||||
- Photograph a slab on a green or black sheet and remove the background.
|
||||
- Set length from the bounding rectangle and get widths, sqft, and bdft.
|
||||
- Leave and later Continue or Start fresh.
|
||||
|
||||
Acceptance: inventory PNG and measurements visible on a phone. No Woo create.
|
||||
|
||||
### Milestone 2 — Photos
|
||||
|
||||
Spec: `docs/PHOTOS.md`. Current work.
|
||||
|
||||
Batch photos of one slab, pick calibration, run Milestone 1 on that reference, apply the same mask params and crop to the rest, preview all inventory PNGs lined up, plus a dimension-overlay copy of the calibration PNG.
|
||||
Acceptance: on a phone, 1–N store-ready inventory PNGs for the slab (including dimension overlay on the calibration copy) with Milestone 1 measures visible. No Woo create. No Complete-listing taxonomy in this milestone.
|
||||
|
||||
### Milestone 3 — Create
|
||||
|
||||
WooCommerce draft for a practice SKU. Then clear the device draft.
|
||||
Acceptance: `SLAB-UAT-*` exists in the store as draft.
|
||||
|
||||
### Milestone 4 — Assist
|
||||
|
||||
Optional vision and generated copy on the manual path from Milestones 1–3.
|
||||
Acceptance: create still works with inference off.
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
# Docs map (branch `m3-ds`)
|
||||
|
||||
Read this file first. It tells you what SlabUploader is for, what already ships, and what Milestone 3 must build.
|
||||
|
||||
**Who this is for.** Mill owner (Ty), humans reviewing scope, and agents implementing on this branch.
|
||||
|
||||
**Source of truth for Milestone 3.** `docs/CREATE.md` plus the Ty conversations that wrote it. Older milestone and task splits on `main` are obsolete for M3. Do not open `main` for scope once you are coding on `m3-ds`.
|
||||
|
||||
## High-level project goal
|
||||
|
||||
SlabUploader lists one-of-a-kind wood slabs on Whidbey Wood Store (WooCommerce).
|
||||
|
||||
One slab is one unit and one listing. Photos and metadata are the inventory record. The mill owner photographs a slab on a green or black sheet, gets store-ready transparent PNGs and measurements on the phone, fills the store's taxonomy and price, then creates a WooCommerce product.
|
||||
|
||||
Store: https://www.whidbeywoodstore.com/
|
||||
|
||||
## What is already implemented (pre-M3)
|
||||
|
||||
| Milestone | Spec | Status on this branch |
|
||||
|---|---|---|
|
||||
| 1 Capture | `docs/CAPTURE.md` | Shipped. Browser-only mask, length axis, measure, inventory PNG, device draft. |
|
||||
| 2 Photos | `docs/PHOTOS.md` | Shipped. Batch photos, pick calibration, apply mask/crop to the batch, dimension overlay, Photos-ready lineup. UAT on `:10881`. |
|
||||
|
||||
There is no Woo create yet. There is no Complete-listing taxonomy UI yet. There is no FastAPI Woo client on this branch (`backend/` is a stub). Inference is not wired.
|
||||
|
||||
## What Milestone 3 builds
|
||||
|
||||
Spec: `docs/CREATE.md`.
|
||||
|
||||
**Exit criteria.** A Photos-ready slab becomes **submission-ready for WooCommerce**, and a practice `SLAB-UAT-*` draft exists in the store that matches the reviewed ready set. Create must still work with inference off.
|
||||
|
||||
In plain terms, M3 connects the Photos-ready device draft to the real store:
|
||||
|
||||
1. Talk to WooCommerce with a WordPress application password (server-side only).
|
||||
2. Sync and use **Woo attributes** (categories, five locked attributes, `fig-*` / `feat-*` tags) per the requirements taken from `main` and written into `CREATE.md`.
|
||||
3. Pricing from species `$/bdft` rules, with user override.
|
||||
4. Inference Settings (OpenAI-compatible URL, key, model) and Call 1 / Call 2 rules when enabled.
|
||||
5. Review to the full ready set, then create the Woo product (force draft for `SLAB-UAT-*`).
|
||||
|
||||
Technical detail for Woo sync, metadata, pricing, and inference is copied into `docs/CREATE.md` from `main` so you do not need to open `main` while coding. Older milestone splits on `main` are still not the scope authority for what M3 includes.
|
||||
|
||||
## How to read the folder
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `README.md` (this file) | Map: goals, shipped, M3. |
|
||||
| `PRODUCT.md` | Product overview: complete listing, decision rights, seams. |
|
||||
| `CAPTURE.md` | Milestone 1 Capture (shipped). |
|
||||
| `PHOTOS.md` | Milestone 2 Photos (shipped). |
|
||||
| `CREATE.md` | Milestone 3 Create (current work). Read this before task breakdown or coding. |
|
||||
| `CONTENT-WOO.md` | Listing copy templates and Woo create/payload detail (from `main`). |
|
||||
| `UAT-PRACTICE.md` | Human practice walk for a `SLAB-UAT-*` listing. Defers to `CREATE.md` on conflict. |
|
||||
| `archive/` | Historical only. Not M3 scope. |
|
||||
|
||||
## Deploy reminders for this branch
|
||||
|
||||
- UAT overnight Docker: ubudual5060 `http://100.64.0.9:10881`
|
||||
- Do not touch `:10880` (Ty Node/SvelteKit)
|
||||
- Store URL (`WOO_BASE_URL`) lives in compose / host `.env`
|
||||
- Woo application password and inference URL live in encrypted Settings, not in compose
|
||||
@@ -1,230 +0,0 @@
|
||||
# UAT Practice Walk: one `SLAB-UAT-*` listing
|
||||
|
||||
Status: WALK · for Ty to run · SlabUploader
|
||||
|
||||
This is the walkable practice plan. It proves one listing end to end without touching a live product.
|
||||
Follow the sections in order. Each one names what you do, what you should see, and where the rule
|
||||
lives.
|
||||
|
||||
**Inference is OFF for this walk.** The optional ON path is only valid after Gate C is green and
|
||||
the inference endpoint is already configured. Inference stays off here.
|
||||
|
||||
---
|
||||
|
||||
> On branch `m3-ds`, Milestone 3 exit criteria and field rules live in `docs/CREATE.md`. This walk is the human practice path for a `SLAB-UAT-*` listing. Prefer `CREATE.md` if anything conflicts.
|
||||
|
||||
|
||||
## 1. Host and deploy preconditions
|
||||
|
||||
Before this walk, the app must already be on the inventoried host. This section only checks that.
|
||||
Host IPs and hostnames come from `deploy/INVENTORY.md`.
|
||||
|
||||
1. **Inventory is filled.** `deploy/INVENTORY.md` has `APP_HOST`, `APP_HOSTNAME`, ports, Caddy
|
||||
location, and the smoke results. It is the only source for what runs where on this host.
|
||||
See `docs/DEPLOYMENT.md` §1 for the worksheet rules.
|
||||
2. **Reachable over LAN/Tailscale.** Your phone reaches `https://${APP_HOSTNAME}` on the same
|
||||
network (LAN or Tailscale). No public exposure. Verify with a browser, not `curl`.
|
||||
3. **HTTPS only.** The URL is `https://`. The browser shows a valid certificate. If the phone
|
||||
warns, the Caddy TLS trust is not set up yet. That is a deploy problem, not a listing problem.
|
||||
4. **Health passes.** In a browser on the phone, open `https://${APP_HOSTNAME}/api/health`.
|
||||
You should see an ok response. This is the same check in `docs/DEPLOYMENT.md` §8 step 1.
|
||||
5. **Store URL is set.** `WOO_BASE_URL` is in the host `.env` and points at the store over
|
||||
`https://`. It is not something you type in the app. See `docs/DEPLOYMENT.md` §3 and §5.
|
||||
|
||||
If any of these fail, fix the deploy before continuing. This walk does not cover Gate B host work.
|
||||
It assumes the app is up and reachable.
|
||||
|
||||
---
|
||||
|
||||
## 2. WordPress Application Password setup
|
||||
|
||||
You do this once per store, in the store's WordPress admin. Do it before you open the app's
|
||||
Settings.
|
||||
|
||||
1. **Store is on HTTPS.** WordPress only offers Application Passwords over HTTPS. The store must
|
||||
already be on TLS. If it is not, stop and fix the store first.
|
||||
2. **Dedicated low-privilege user.** Create or use a WordPress user that can create and edit
|
||||
products and upload media. The store admin login is not reused. A shop manager role or a custom
|
||||
role with product create/edit plus media upload is enough.
|
||||
3. **Generate the password.** Sign in as that user. Go to **Users → Profile → Application
|
||||
Passwords**. Generate a password and label it "SlabUploader". Copy the password. WordPress
|
||||
shows it once.
|
||||
4. **Enter it in the app.** Open the app's Settings on the phone. Enter the WordPress username and
|
||||
the application password. The server stores them encrypted. The browser does not keep them.
|
||||
5. **Run `test-woo`.** In Settings, tap the test. It should report ok. This also resyncs the
|
||||
store's species, categories, tags, and attributes into the app. If it fails, the username or
|
||||
password is wrong, or the store is unreachable.
|
||||
|
||||
Rules: this is a WordPress Application Password, not WooCommerce consumer keys (`ck_`/`cs_`).
|
||||
The app never calls the store from the browser. FastAPI is the only caller. Rotate by revoking
|
||||
the old password in the WP profile and entering a new one in Settings, then re-running
|
||||
`test-woo`. Full lock: `AGENTS.md` §4 and §5, `docs/DEPLOYMENT.md` §5.
|
||||
|
||||
---
|
||||
|
||||
## 3. Practice build rules
|
||||
|
||||
These rules keep practice runs invisible to customers. They are locked. Changes are not made for
|
||||
the walk.
|
||||
|
||||
- **SKU prefix `SLAB-UAT-*`.** Every practice listing uses a SKU that starts with `SLAB-UAT-`.
|
||||
Pick a unique one each time, for example `SLAB-UAT-001`.
|
||||
- **Always a Woo draft.** A `SLAB-UAT-*` SKU creates a WooCommerce draft even if Settings say
|
||||
publish. Customers cannot open it. This is hard rule in `AGENTS.md` §5.
|
||||
- **`woo_create_status` defaults to draft.** The Settings field that controls create status
|
||||
defaults to `draft`. Practice uses the default. If it is `publish`, the
|
||||
`SLAB-UAT-*` rule still forces a draft.
|
||||
- **Customers never see practice listings.** A Woo draft is hidden from the storefront. If you
|
||||
can see the product on the live store, something is wrong.
|
||||
|
||||
If a practice SKU is not `SLAB-UAT-*`, it is not a practice run. Real SKUs are not published in
|
||||
this walk.
|
||||
|
||||
---
|
||||
|
||||
## 4. Prove one listing end to end
|
||||
|
||||
This is the main walk. Run it on a phone browser first, then repeat on a PC browser. Both are in
|
||||
the signed compatibility matrix (`docs/UX.md`, signed 2026-09-06).
|
||||
|
||||
Supported browsers: iOS Safari, Android Chrome on phones; desktop Chrome, Edge, Firefox on PC.
|
||||
Exact version pins are tracked under issue #4. If the browser is not supported, the app shows a
|
||||
hard stop. Switch to a supported browser.
|
||||
|
||||
### 4.1 Capture
|
||||
|
||||
1. Open the app on the phone. Start a new slab.
|
||||
2. Take 1 to 5 photos of the slab. The first should be top-down, lens parallel to the broad
|
||||
face. A tilted shot makes the numbers wrong. Retake if it is tilted.
|
||||
3. **Mask overlay and four knobs.** The app draws a mask over the photo. You see which pixels are
|
||||
slab and which are not. Tune the four knobs: **Sheet** (auto/green/black), **Sensitivity**,
|
||||
**Edge offset**, **Feather**. Each change re-runs the mask immediately. Look at the edge
|
||||
against the real wood. If it is still off, retake the photo.
|
||||
4. **Confirm the mask.** Tap confirm.
|
||||
5. **Length axis.** The app draws the bounding rectangle and the length axis. Confirm it follows
|
||||
the slab's long direction. Rotate if it is skewed. On a nearly square slab, you say which way
|
||||
is length.
|
||||
6. **Enter numbers by hand.** Length in 1/8" steps. Thickness as a 1/8" pick. SKU: type
|
||||
`SLAB-UAT-001` (or your chosen practice SKU).
|
||||
7. **Check the computed numbers.** The browser shows square feet, board feet, and width samples.
|
||||
Board feet = square feet × thickness in inches. No divide-by-12. If the numbers look wrong,
|
||||
go back and fix the mask or the length.
|
||||
8. **Continue.** This uploads the originals, the processed PNGs, and the numbers to the server.
|
||||
The slab status becomes `calibrated`.
|
||||
|
||||
### 4.2 Review
|
||||
|
||||
1. You are on the review screen. Fill every required field. Inference is OFF, so everything starts
|
||||
empty.
|
||||
2. **Species.** Pick exactly one from the synced store list. The list came from `test-woo`.
|
||||
Only species in the list may be typed.
|
||||
3. **Wood categories.** Pick at least one from the synced list.
|
||||
4. **Edge type, figure, grade.** Pick from the synced lists. At least one figure, at least one
|
||||
grade.
|
||||
5. **Thickness band.** Pick the Woo band that the measured thickness rounds up into.
|
||||
6. **Moisture.** Keep the default (kiln-dried) or change it.
|
||||
7. **fig-* tags.** Pick at least one.
|
||||
8. **feat-* tags.** Zero or more. Only pick existing `feat-*` tags.
|
||||
9. **Price.** A recommendation fills from the species $/bdft rule times board feet. You can
|
||||
override it. If there is no rule for the species, the price starts empty. Type it. Publish
|
||||
is still allowed with a manual price.
|
||||
10. **Title and description.** You can type them, or leave them for templates at publish.
|
||||
Templates inject the deterministic numbers. Generate text is OFF for this walk.
|
||||
|
||||
Every empty required field shows an inline nudge where it sits. The slab cannot reach `ready`
|
||||
until the full set is filled.
|
||||
|
||||
### 4.3 Publish
|
||||
|
||||
1. Tap **Publish**.
|
||||
2. The server checks the SKU against the store. If `SLAB-UAT-001` is new, it creates the
|
||||
product.
|
||||
3. **What you should see:** a success message on the screen. The local draft and its photos are
|
||||
gone from the app. The slab status in the app is `published`.
|
||||
4. **What that does NOT mean:** the product is not live in the store. It is a WooCommerce draft.
|
||||
The app's "published" means the store create succeeded and the local row was cleaned up.
|
||||
|
||||
### 4.4 Confirm in the store
|
||||
|
||||
1. Open the store's WooCommerce admin.
|
||||
2. Find the product by SKU `SLAB-UAT-001`.
|
||||
3. **It must be a draft.** Status is draft, not published.
|
||||
4. **Check the fields:** SKU, price, category (species), tags (fig-*, feat-*), attributes
|
||||
(Edge Type, Figure, Grade, Thickness, Moisture), and images (the processed transparent PNGs).
|
||||
5. **Open the live storefront.** The product must not appear. A draft is invisible to customers.
|
||||
|
||||
### 4.5 Repeat on a PC browser
|
||||
|
||||
Repeat 4.1 through 4.4 on a desktop browser (Chrome, Edge, or Firefox). Use a different
|
||||
practice SKU, for example `SLAB-UAT-002`. The flow must work the same way. This is the signed
|
||||
PC-browser compatibility check.
|
||||
|
||||
---
|
||||
|
||||
## 5. Pass and fail checks
|
||||
|
||||
You can see all of these without reading code.
|
||||
|
||||
| Check | Where | Pass looks like |
|
||||
|---|---|---|
|
||||
| Health endpoint | Phone browser, `https://${APP_HOSTNAME}/api/health` | ok response |
|
||||
| `test-woo` ok | App Settings | ok; species list is populated |
|
||||
| `woo_create_status` is draft | App Settings | shows `draft` (or `publish`, but `SLAB-UAT-*` forces draft anyway) |
|
||||
| Slab reaches `calibrated` | App, after capture | status is `calibrated` |
|
||||
| Slab reaches `ready` | App, after review | no empty required fields; inline nudges gone |
|
||||
| Publish succeeds | App, after Publish | success message; local draft is gone |
|
||||
| Woo product exists as draft | Store admin, products | SKU `SLAB-UAT-*`, status draft |
|
||||
| SKU prefix correct | Store admin | starts with `SLAB-UAT-` |
|
||||
| Store not live | Live storefront URL | product does not appear |
|
||||
| Board feet identity | App review screen, or a fixture | bdft = sqft × thickness in inches; no divide-by-12 |
|
||||
| Images are processed PNGs | Store admin, product images | transparent PNGs, not the original JPEGs |
|
||||
|
||||
### Duplicate SKU recovery
|
||||
|
||||
If you publish the same SKU twice, the second time the app stops before creating a second product.
|
||||
Under the SKU field you see: "This SKU already exists in the store (any status)." Two paths:
|
||||
edit the SKU to a new value, or open the existing listing. The draft stays in the app. Nothing is
|
||||
lost. This is the expected behavior, not a bug. See `docs/CONTENT-WOO.md` §2.5.
|
||||
|
||||
### What a fail looks like
|
||||
|
||||
- **No success message after Publish.** The app shows a plain message that the listing could not
|
||||
be created. No status number, no stack trace. The server kept a log. You can retry from the
|
||||
same screen without re-photographing.
|
||||
- **A value is no longer valid.** If a category, attribute, or price was removed from the store
|
||||
between sync and publish, that field shows "This value is no longer valid. Pick from the
|
||||
current list." Re-pick and retry.
|
||||
- **Store unreachable.** A plain message says the store could not be reached. Check the network
|
||||
and retry.
|
||||
|
||||
---
|
||||
|
||||
## 6. Cleanup
|
||||
|
||||
After the walk, decide what to do with the Woo draft UAT product.
|
||||
|
||||
- **Leave it or delete it.** Both are fine. It is a draft. Customers cannot see it. If you delete
|
||||
it, the store admin is the place to do it. The app does not delete store products.
|
||||
- **Never customer-visible.** If the product is visible on the live storefront, it is not a
|
||||
draft. That is a bug. Stop and report it.
|
||||
|
||||
**Real live publish is a separate step.** It is not part of this walk. A real product uses a
|
||||
non-UAT SKU (no `SLAB-UAT-` prefix) and is published only after you review it. That is a
|
||||
different decision. That step is not run here. The name of that step is **final POC publish**, and it
|
||||
is gated on your review of one real listing.
|
||||
|
||||
---
|
||||
|
||||
## Where each rule lives
|
||||
|
||||
| Rule | Source |
|
||||
|---|---|
|
||||
| Practice SKU forces draft | `AGENTS.md` §5 |
|
||||
| `woo_create_status` default draft | `AGENTS.md` §5, `docs/CONTENT-WOO.md` §2.4 |
|
||||
| App Password setup, not consumer keys | `AGENTS.md` §4, `docs/DEPLOYMENT.md` §5 |
|
||||
| Host inventory before deploy | `docs/DEPLOYMENT.md` §1, `deploy/INVENTORY.md` |
|
||||
| Capture → review → publish flow | `docs/UX.md` (SIGNED 2026-09-06) |
|
||||
| Board feet identity | `docs/TECH-SPEC-PIPELINE.md` |
|
||||
| Duplicate SKU recovery | `docs/CONTENT-WOO.md` §2.5, `docs/UX.md` Publish |
|
||||
| PC browser compatibility | `docs/UX.md` Browser compatibility |
|
||||
| Gate C = early Phase 1 | `docs/IMPL-PLAN.md` Gates map and Phase 1 |
|
||||
@@ -15,11 +15,3 @@ Live spec on this branch:
|
||||
- `docs/PRODUCT.md`
|
||||
- `docs/CAPTURE.md`
|
||||
- `deploy/` for hosting only
|
||||
|
||||
## M3 note (2026-09-16)
|
||||
|
||||
For Milestone 3 on branch `m3-ds`, do **not** treat docs frozen on `main` as scope SoT. Use `docs/CREATE.md` on `m3-ds` (conversation-locked). Archive / `main` files remain historical reference only.
|
||||
|
||||
## M3 note (updated 2026-09-16)
|
||||
|
||||
M3 exit on `m3-ds` is submission-ready for WooCommerce (`docs/CREATE.md`). Technical requirements for Woo sync, metadata, pricing, and inference were taken from historical `main` docs and written into `CREATE.md`. Do not use `main` milestone splits as M3 scope.
|
||||
|
||||
Reference in New Issue
Block a user