merge issue-89-metadata-lock → main (Create-string model, #89/#90)
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.
This commit is contained in:
+15
-68
@@ -155,65 +155,15 @@ async def review_slab(slab_id: str, payload: dict[str, Any] | None = None) -> di
|
||||
if row is None:
|
||||
return JSONResponse(status_code=404, content=_error("not_found", "Slab not found"))
|
||||
slab = _slab_dict(row)
|
||||
locked = bool(slab.get("metadata_locked"))
|
||||
|
||||
# --- Explicit lock (#89): owner confirms the taxonomy set; server then
|
||||
# builds AND PERSISTS the deterministic title + short_description. Until
|
||||
# this action runs, the prose fields stay unset (no "ready" copy from an
|
||||
# unlocked AI draft). Owner prose (title/short_description) always wins.
|
||||
if payload.get("action") == "lock":
|
||||
if not slab.get("species_id"):
|
||||
return JSONResponse(status_code=422, content=_error(
|
||||
"not_ready", "Select a species before locking the metadata"))
|
||||
primary_id = payload.get("primary_wood_category_id")
|
||||
wood_cats = slab.get("wood_category_ids") or []
|
||||
if not primary_id:
|
||||
primary_id = wood_cats[0] if wood_cats else None
|
||||
if not primary_id:
|
||||
return JSONResponse(status_code=422, content=_error(
|
||||
"not_ready", "Select a wood category before locking the metadata"))
|
||||
cats, terms = _taxonomy_lookup(conn)
|
||||
primary_leaf = cats.get(primary_id, {})
|
||||
if not primary_leaf:
|
||||
return JSONResponse(status_code=422, content=_error(
|
||||
"not_ready", "Wood category is no longer in the current taxonomy"))
|
||||
species_name = cats.get(slab.get("species_id"), {}).get("name", "")
|
||||
figure_term_ids = slab.get("figure_term_ids") or []
|
||||
figure = terms.get(figure_term_ids[0], {}).get("name") if figure_term_ids else None
|
||||
|
||||
owner_title = (payload.get("title") or slab.get("title") or "").strip()
|
||||
owner_short = (payload.get("short_description") or slab.get("short_description") or "").strip()
|
||||
title = owner_title or build_title(slab, figure, species_name, primary_leaf)
|
||||
short_desc = owner_short or build_short_description(slab, figure, species_name, primary_leaf)
|
||||
|
||||
now = _now()
|
||||
cur = conn.execute(
|
||||
"UPDATE slabs SET title=?, short_description=?, metadata_locked=1, server_rev=server_rev+1, updated_at=? WHERE id=?",
|
||||
(title, short_desc, now, slab_id),
|
||||
)
|
||||
conn.commit()
|
||||
if cur.rowcount == 0:
|
||||
return JSONResponse(status_code=409, content=_error("not_found", "Slab not found"))
|
||||
slab["title"] = title
|
||||
slab["short_description"] = short_desc
|
||||
slab["metadata_locked"] = 1
|
||||
|
||||
price = _resolve_price(conn, slab, payload)
|
||||
ready, missing = compute_ready({**slab, "price": price})
|
||||
return {
|
||||
"ready": ready, "missing": missing, "title": title,
|
||||
"short_description": short_desc, "price": price,
|
||||
"price_suggestion": price_suggestion(conn, slab),
|
||||
"primary_wood_category_id": primary_id, "locked": True,
|
||||
}
|
||||
|
||||
# --- Regular review (no lock): report ready/missing + suggested prose.
|
||||
# #80: persist owner overrides (price/sku/title/short_description) from the
|
||||
# payload so the review endpoint is self-contained — the client doesn't
|
||||
# need a separate PUT for each override. #89: deterministic
|
||||
# title/short_description are NOT set until an explicit lock; report the
|
||||
# stored values (which stay empty until locked) so the UI shows them as
|
||||
# pending, and never as "ready" from an unlocked AI draft.
|
||||
# --- Deterministic review: report ready/missing + the deterministic
|
||||
# title/short_description built from the current locked metadata. The client's
|
||||
# "Create title" / "Create short desc" buttons call this endpoint and write the
|
||||
# returned strings into the fields (item 1: no lock action, no metadata_locked).
|
||||
# #80: persist owner overrides (price/sku) from the payload so the review
|
||||
# endpoint is self-contained. title/short_description are NOT overridden here —
|
||||
# they are produced by the deterministic build below, not persisted from the
|
||||
# payload (so the review POST can't clobber the owner's Create workflow).
|
||||
cats, terms = _taxonomy_lookup(conn)
|
||||
primary_id = payload.get("primary_wood_category_id")
|
||||
wood_cats = slab.get("wood_category_ids") or []
|
||||
@@ -224,9 +174,9 @@ async def review_slab(slab_id: str, payload: dict[str, Any] | None = None) -> di
|
||||
figure_term_ids = slab.get("figure_term_ids") or []
|
||||
figure = terms.get(figure_term_ids[0], {}).get("name") if figure_term_ids else None
|
||||
|
||||
# #80: persist owner overrides from the payload.
|
||||
# #80: persist owner overrides from the payload (price/sku only).
|
||||
overrides: dict[str, Any] = {}
|
||||
for k in ("price", "sku", "title", "short_description"):
|
||||
for k in ("price", "sku"):
|
||||
if k in payload and payload[k] is not None:
|
||||
overrides[k] = payload[k]
|
||||
if overrides:
|
||||
@@ -238,20 +188,17 @@ async def review_slab(slab_id: str, payload: dict[str, Any] | None = None) -> di
|
||||
conn.commit()
|
||||
slab.update(overrides)
|
||||
|
||||
if not locked:
|
||||
title = slab.get("title") or None
|
||||
short_desc = slab.get("short_description") or None
|
||||
else:
|
||||
# Locked: re-derive only if the owner has not overridden the prose.
|
||||
title = slab.get("title") or build_title(slab, figure, species_name, primary_leaf)
|
||||
short_desc = slab.get("short_description") or build_short_description(slab, figure, species_name, primary_leaf)
|
||||
# Deterministic title / short_description, re-derived from the current
|
||||
# metadata on every review (the Create button's source of truth).
|
||||
title = build_title(slab, figure, species_name, primary_leaf) if (primary_leaf and species_name) else slab.get("title") or None
|
||||
short_desc = build_short_description(slab, figure, species_name, primary_leaf) if (primary_leaf and species_name) else slab.get("short_description") or None
|
||||
|
||||
price = _resolve_price(conn, slab, payload)
|
||||
ready, missing = compute_ready({**slab, "price": price, "title": title, "short_description": short_desc})
|
||||
return {
|
||||
"ready": ready, "missing": missing, "title": title, "short_description": short_desc,
|
||||
"price": price, "price_suggestion": price_suggestion(conn, slab),
|
||||
"primary_wood_category_id": primary_id, "locked": locked,
|
||||
"primary_wood_category_id": primary_id,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user