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).
Capture→Review uploads the calibrated lineup PNGs (clean cal + batch) as
multipart to /api/v1/slabs/{id}/photos. nginx's default 1MB
client_max_body_size 413s on real multi-MB photos, blocking the
Capture→Review transition. Allow 25m.
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
#84: /api/health woo_reachable is now a real check (GET /wp-json/ with a
3s timeout) instead of a hardcoded False stub. Version bumped to 0.5.0-m3-s1.
#83: POST /slabs rejects unknown (non-ALLOWED_FIELDS) keys with a 400
'unknown fields' error, instead of silently dropping them. The INSERT
binding count is now provably correct (no more assert needed).
#82: stale_field 422 error now returns the LIVE taxonomy ids
(SELECT id FROM table) as current_options, plus the field name and
submitted value, instead of echoing the submitted value back.
#80: regular review (no lock) persists owner overrides (price/sku/title/
short_description) from the payload to the DB via an UPDATE, so the
review endpoint is self-contained. server_rev is incremented on persist.
Tests: +3 (test_create_rejects_unknown_fields,
test_update_stale_field_returns_live_options,
test_review_persists_owner_overrides). 87/87 passing.
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
Log full text + image_ref (photo_id/seq/bytes) only; wire path still
sends data-URLs. Call 1/2 SELECT id+seq for refs. Cap is safety net
after redaction (METADATA-66).
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
create_slab required ALL ALLOWED_FIELDS present, but the Review screen fills
taxonomy/content after create (CREATE.md §1). Now requires only the capture
fields (id, sku, dims, widths); the rest stay nullable for Review.
Also fixed a pre-existing off-by-one: vals re-filtered cols by 'in data',
dropping the 6 server-managed columns and under-counting bindings (19 vs 18).
Split allowed_cols/server_cols so vals always matches cols.
Verified: 9/9 slabs tests pass; live POST /slabs with the app's capture-math
payload returns 201.
WordPress security plugins (Wordfence, WAF) block the default
Python-urllib User-Agent with 403. Send a descriptive User-Agent on
all Woo requests. Also add WooForbiddenError so 403 is classified
distinctly from 401 (auth) per SETTINGS-80, and surface a clear
message in test-woo.
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.
Live host had stale M2 slabs table (no deleted_at/server_rev) -> GET /slabs 500.
Drift check now verifies sentinel (table,column) pairs across settings/slabs/
slab_photos/sync_log; on drift drops all app-owned M3 tables and rebuilds, stamps
schema_version. Drift test now seeds M2 settings + slabs and asserts both rebuilt.
pytest 23 passed; gates exit 0.
- Add schema_version table; init_db detects a pre-M3/unknown DB (no version
row, or settings table missing the value column) and rebuilds to the clean
M3 schema, then stamps version 1
- _has_schema_version checks for a real row (empty table != versioned)
- _reset_for_tests checkpoints WAL before close (Windows handle release)
- TDD: test_schema_drift seeds the M2 settings shape, asserts rebuild + version
Fixes live 500 'no such column: value' on the UAT host (stale M2 slab_db volume).
Gates: check_requirements + check_coverage exit 0; pytest 14 passed.