Prototype routing so 2–3 owners can share one store URL with different WP
application passwords. Device creds live in localStorage; server uses
request override → Settings fallback and never writes overrides into the
Settings row. Inference stays on shared Settings.
See PROTOTYPE.md for UAT with two browsers and known LAN forge risk.
Pinch/drag were applying zoom and tilt together on Measure. Add an
explicit Zoom/Pan | Rotate segmented toggle (default Zoom/Pan). Stage
gestures are exclusive per mode; tilt/zoom sliders stay available.
Remove the bottom-center ↻ handle that sat on tick B (hitTick won, so
the icon looked live but only moved the tick).
After Review → Continue to Publish, show a glanceable confirmation:
ordered photo thumbs, SKU/title/price, taxonomy + dims, short and long
descriptions (plain text). Back to Review edits; Publish posts the draft.
Reuses slab/taxonomy/photos only — no invented fields.
Post-publish: hide Back while publishing; drop Settings/Slabs chrome on
done (remove unused white Slabs control). Review/App horizontal padding.
Unify Create desc / View prompt / Create title|short as .btn-sec.
Moisture kd default: sync select after form mount; persist via commit.
Ready/missing update on input (debounced) without blur.
Call 2 seed now requires benefit-first short + four-part long HTML, returned
as JSON. POST parses and persists both fields; Create-desc gate no longer
requires short (Call 2 writes it). Brand voice/geo stay Settings-injected.
- Taxonomy sync keeps fig-/feat- by slug (hyphen), not name; HTML-unescape names
- Review: remove AI assist labels; Features picker for feat-*; derive fig_tag_ids
from Figure term names (casefold match, no bridges); decodeName on display
- Unit tests for scoped_tags, fig match, Review UI contract; docs slug wording
Live overnight slabs after removing the Review species picker had
species_id null, so Create title stayed gated off. Require species on
Measure Calculate, stop the select sync from wiping choices before
options load, and have Review gate/PUT from slab.species_id with a
Capture handoff repair so deterministic title/short-desc always see it.
Species is chosen on the merged Measure screen; the Review select was a
duplicate. Keep species_id on the slab from createSlab (Capture→draft→Woo)
and gate Create title from the hydrated value without offering edit.
Sheet gaps after black/green removal left stray chunks in the bitset.
After keep predicates, zero all but the largest 4-connected component
and recompute slabPx before bbox / slabBounds.
After mask approve, land on one Measure screen: interactive tape
PhotoStage plus Length L, Thickness, and Species (Woo taxonomy leaves).
Approve tape is folded into Calculate. Persist species_id on the device
draft and pass it through createSlab so Review pre-fills.
After rotateCalibration, map tape tick A→B through bakeTiltTransform into
baked cal coords, orient overlay bbox with orientedRect(..., tapeAngle), and
pass optional axis into buildOverlayPlan so length || tape and widths ⊥ tape.
Review was POSTing price/sku to /review before each save; that endpoint
persists those overrides and bumps server_rev, so the following PUT sent
a stale client_rev and hit 409 — Continue to Publish stayed disabled.
Stop sending price/sku on review probes (PUT already persists them) and
have updateSlab refetch + retry once on revision_conflict.
Pointer-Events pan/zoom/tilt under a fixed black ~2pt line with
rifle-scope end ticks (≥44×44 hit). AxisPanel becomes Tape line step;
session persists tape transform; scale = tick distance / L.
sveltedrag unavailable for Svelte 4 — Pointer Events path used.
Morning merge kept Review call sites (nextPrimary, keptInOrder, roleLabel, …)
and dropped the imports, so hydrate threw ReferenceError and the UI showed
"Could not load the slab." Live UAT redeployed; sample data preserved.
The merge auto-kept the branch's canCreateTitle reactive (speciesId) which is
undeclared in the merged file (main's canonical var is speciesSel) → ReferenceError
on Review mount → black screen. Also dropped the branch's duplicate gradeId (main
uses gradeSel) and repointed hydrate/patch to gradeSel. Live UAT verified: Review
now mounts and renders 'Review to Ready' with photo strip + Create buttons.
Reconciles two independent lines of work that both touched Review.svelte,
MultiSelect.svelte, and the thickness band:
- main's PR #97 (client-side thicknessBand.ts + MultiSelect names/commit +
syncSelects DOM race fix + box-sizing CSS)
- work/merge-89-90 (server-side thickness band in thickness_band.py,
Create-string #89, metadata_locked dropped, photo strip, wood primary)
Canonical resolution: the band is SERVER-side (hidden from UI, sent to Woo
on publish), so the client-side thicknessBand.ts + read-only band display
are removed from Review.svelte; PR #97's syncSelects() and box-sizing CSS
are preserved (orthogonal fixes). MultiSelect takes selectedSummary from
woodPrimary.ts (names in closed state) + the ms-count testid + onDestroy
timer cleanup; duplicate close()/summary definitions dropped.
Gates: backend 117/117, unit 88/88, tsc clean, e2e 6/6 (m3-create +
photo-strip).
Drop the metadata_locked column + "Save and lock" UI end-to-end. The
review endpoint now builds the deterministic title/short_description on
every call; the owner's Create title / Create short desc buttons fire
the build and write the result (force-overwrite contract). commitReview
auto-fills only empty fields (no owner-clobber) and folds the
deterministic copy into its single PUT (kills the client_rev 409
cascade).
Taken wholesale from the branch: photo strip, wood primary/secondary,
server-side thickness band (hidden from UI, passed to Woo on publish),
and fixes #80/#82/#83/#84.
- backend: remove metadata_locked from schema.sql, db sentinels,
slabs.py client fields/exposure, review.py lock action; review
always computes title/short_desc
- frontend: slabs.ts + Review.svelte drop the lock model, add
Create-string workflow + wood primary + photo strip
- e2e: m3-create + review-photo-strip rewritten for the Create-string
contract; thickness-band test asserts absence (per #90)
- deploy: recreate LAN binds (override-m3.yml was absent); verify
metadata_locked gone, review builds title, PUT persists
Gates: backend 117/117, unit 88/88, tsc clean, e2e 6/6 (m3-create +
photo-strip). Live UAT verified at 192.168.1.202:10881.
#88 — thickness band is now display-only, derived deterministically from
thickness_in against the synced Woo thickness terms (TV-9 / QUALITY-95).
Replaces the <select> with a read-only div. New pure function
deriveThicknessTermId + 6 unit tests. Docs (UAT-PRACTICE, contracts/UX)
updated to reflect the band is not an owner control.
#90 — MultiSelect closed state now shows the selected option names
(truncated to 2 + +N) instead of just a count. onCommit prop wired so
multi-select changes persist + re-review immediately (250ms debounce,
flush on Done, cleanup on destroy). Single-select binds use string vars
(speciesSel/edgeSel/gradeSel) to fix the number/string mismatch with
<option value={id}>; a tick()-deferred syncSelects() keeps the DOM
option in sync after programmatic writes (AI prefill, reload hydrate)
that Svelte's own select_option would otherwise reset.
The mandatory clean-cal and dimension-overlay photos now show a padlock glyph
in their keep box and an amber REQUIRED tag at the top of the card, so the
owner can see at a glance which photos cannot be removed (vs. optional batch
photos, which keep a plain checkmark).
REVIEW-5 requires both the clean calibration photo and its dimension-overlay
copy to ship. Their keep checkbox is now checked AND disabled (aria-disabled,
a lock glyph, cursor:default, tap is a no-op) so the owner can't remove them.
Only batch photos are removable. The submission minimum can no longer be
broken by the owner, so publish is never lineup-blocked by a missing required
photo.
The Capture→Review upload filtered out the overlay (role !== 'overlay') and
sent no role field, so the dimension-overlay copy was never stored and every
photo defaulted to role=batch. Review then fell back to a stored /processed
URL that had no dimensions drawn — the owner saw the overlay thumbnail
without its measurements. Now upload the full lineup with the correct role,
keeping the overlay's dimension-drawn blob as both original and processed.
Locks in the two live-bug fixes: the Primary (leftmost kept) photo's <img src>
must be a real URL (blob: for an in-memory capture blob) and never empty/
"object Blob"; and each strip card must carry the gray checkerboard
background matching the Capture lineup.
Bug #1: the first/Primary (calibration) photo rendered broken because its
in-memory capture Blob was assigned to <img src> directly (String(Blob)=
"object Blob"). resolvePhotoUrl() now mints a blob: object URL (revoked on
unload) and falls back to the stored photo endpoint when there is no blob.
Bug #2: strip thumbnails used a flat dark bg; now carry the gray checkerboard
from the Capture lineup (MeasurePanel) so transparent inventory PNGs read the
same on both screens.
Also harden missingMinimums to filter kept internally (pre-existing test 7
failed on the clean base because it passed un-kept items).
REVIEW-5 / PUBLISH-45: owner edits the stored photo lineup in Review — tap a
card to keep/remove, drag to reorder, leftmost kept photo = primary (Woo
cover). The kept set must contain a clean calibration + its dimension-overlay
copy; publish is blocked until it does.
- backend: role column on slab_photos (schema + idempotent _migrate ALTER),
role in photo CRUD + new DELETE endpoint, Woo image order by role-rank
(clean→overlay→batch) per PUBLISH-45
- frontend: Review photo strip (render from listPhotos, keep/remove,
HTML5 drag-reorder, dynamic PRIMARY badge from .primary class),
persistLineup() re-seq kept + drop removed before entering Publish
- tests: lineup.ts unit, e2e review-photo-strip.spec, backend role + order
Refs: #90, REVIEW-5, PUBLISH-45
The Review screen now has an explicit 'Lock & Build Title' step. Until the
owner locks, title/short_description stay empty (no 'ready' copy from an
unlocked AI draft). On lock the server validates the taxonomy, sets
metadata_locked=1, builds + persists the deterministic title/short_description
(owner prose wins), and returns the ready state.
- schema.sql: add metadata_locked INTEGER DEFAULT 0 to slabs
- db/__init__.py: bump _SCHEMA_SENTINELS for metadata_locked
- slabs.py: expose metadata_locked in GET; reject it in PUT (server-managed)
- review.py: POST /{id}/review action=lock path; regular review reports
stored (empty) copy until locked
- test_review.py: 8 tests covering lock gate, owner-prose-wins, stale taxonomy
- frontend/api/slabs.ts: Slab.metadata_locked, ReviewResult.locked,
ReviewInput.action, lockSlab() helper
- Review.svelte: lockMetadata() with correct commit-token ordering (bump AFTER
inner commit so the inner PUT isn't superseded), Lock button, locked banner
- e2e/m3-create.spec.ts: pare to single golden path (capture→review→lock→
ready→publish→done); mock respects optimistic concurrency (client_rev) so
stale PUTs 409 like the real backend
Ship locked expert Call 1 seed: order cats before edge, species=best
confidence, mill-count edge rules, Wild for cookies/burl, multi figure,
feature_tag_ids only (no free-text bags, no grade). LISTS keys match the
seed; feat- prefix stripped for the model. Gate and review UI apply
feature_tag_ids. Seed auto-activates on deploy when still seed-authored.
- frontend: uuid() helper (crypto.randomUUID -> getRandomValues) for plain-HTTP LAN UAT
- slabs.py: empty SKU at create -> NULL (no-SKU-yet is transient, filled in Review)
- review.py: ready-gate enforces REVIEW-45 (SKU 3-24 alnum+hyphen) in the middle tier
- DB CHECK stays as backstop only (referential/structural), per layering principle
Owner-directed, LAN/HTTP dev-test only. When the env flag is set,
GET /api/v1/settings returns decrypted secret values (string) instead of
{configured: bool}; the UI prefills and shows them in clear text. Default
off -> SETTINGS-75 masking and the existing tests stay intact. Revert
before any non-UAT deployment.
Scope .settings styles to the panel: light #eee text on the dark
#121212 surface for labels, and white inputs/selects/options with dark
text. Both pairs exceed WCAG 4.5:1. Add settings-contrast e2e as the
regression guard for the Settings form plus the sibling dev panels.
- test_exit_inference_off.py (QUALITY-20/120): full create->photo->review->publish
happy path with inference OFF, asserts zero inference_log rows and no raw codes/
tracebacks reach the owner; not-ready publish 422 with plain field-level message.
- test_integration_woo_mock.py (QUALITY-35): Woo mock covering success, error,
duplicate SKU, stale field (no raw code), and taxonomy anchor bump on change.
- m3-create.spec.ts (QUALITY-50): Playwright E2E — all M3 dev screens render,
Review/Publish controls load, Generate control visible only when enabled+reachable.
- deploy/uat_walk.sh (QUALITY-110): ready-to-run human walk for the real SLAB-UAT-*
draft (inference off) against the live store — NOT a mock.
Verified: pytest 61 passed; frontend unit 88 passed; e2e 4 passed; gates exit 0.