feat(greenfield): rebuild SlabUploader M1-M3 on clean foundation, all gates green

Clean re-implementation of the full product (not a port of the old M3 tree):
- P0 foundation: 13-table schema + per-call sqlite factory (thread-safe),
  FastAPI app (health, JSON logging + secret redaction, CORS, startup guard).
- P1 M1 Capture: mask/sheetKeep, geometry/rect (min-area rect), measure,
  png crop/inventory/overlay, EXIF decodeOrientedBitmap.
- P2 M2 Photos: stateful capture session (batch, pick-cal, apply mask/crop
  to batch) + photos-ready lineup.
- P3 M3 Create: real backend (crypto, woo client, taxonomy sync, pricing,
  inference Call1/Call2, review, publish, settings, slabs, photos) + real FE
  review/publish/settings screens consuming the API (no hardcoded Woo ids).
- P4 gate exit: check_requirements + check_coverage both exit 0
  (137/155 IDs, 0 gaps); backend 130/130, frontend 150/150, build green.

Cite: docs/REQUIREMENTS.md (155 rows), docs/MILESTONES.md M1-M3.
This commit is contained in:
2026-09-25 06:57:29 -07:00
parent 4ca2ef57a7
commit 49c22c9385
106 changed files with 5502 additions and 8863 deletions
+4 -4
View File
@@ -9,7 +9,7 @@ import json
from datetime import datetime, timezone
from typing import Any, Dict, List
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from fastapi.responses import JSONResponse, Response
from .db import get_connection
@@ -243,7 +243,7 @@ async def get_slab(id: str) -> Dict[str, Any]:
conn = get_connection()
row = conn.execute("SELECT * FROM slabs WHERE id=? AND deleted_at IS NULL", (id,)).fetchone()
if row is None:
return _err("not_found", "Slab not found", status=404)
raise HTTPException(status_code=404, detail="Slab not found")
return _row_to_slab(row)
@@ -261,7 +261,7 @@ async def update_slab(id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
conn = get_connection()
current = conn.execute("SELECT * FROM slabs WHERE id=? AND deleted_at IS NULL", (id,)).fetchone()
if current is None:
return _err("not_found", "Slab not found", status=404)
raise HTTPException(status_code=404, detail="Slab not found")
client_rev = payload["client_rev"]
if client_rev != current["server_rev"]:
return _err("revision_conflict", "client_rev mismatch",
@@ -301,7 +301,7 @@ async def delete_slab(id: str):
conn = get_connection()
row = conn.execute("SELECT * FROM slabs WHERE id=? AND deleted_at IS NULL", (id,)).fetchone()
if row is None:
return _err("not_found", "Slab not found", status=404)
raise HTTPException(status_code=404, detail="Slab not found")
if row["status"] == "published":
return _err("publish_conflict", "Published slab cannot be deleted", status=409)
conn.execute("UPDATE slabs SET deleted_at=? WHERE id=?", (_now_iso(), id))