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
+81
View File
@@ -0,0 +1,81 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { apiFetch, ApiError, isRecoverable } from '../lib/api';
import type { SlabDraft } from '../lib/types';
export let slab: SlabDraft;
const dispatch = createEventDispatcher<{
success: { id: number | null; draft: boolean };
back: void;
}>();
interface PublishOk {
ok: boolean;
status: string;
woo_product_id: number;
draft: boolean;
}
let status = '';
let result: PublishOk | null = null;
let error = '';
let recoverable = false;
async function attemptPublish() {
status = 'publishing…';
error = '';
recoverable = false;
try {
const body = await apiFetch<PublishOk>(`/slabs/${slab.id}/publish`, { method: 'POST' });
result = body;
status = body.draft ? 'published as DRAFT (SLAB-UAT-* — PUBLISH-10)' : 'published';
dispatch('success', { id: body.woo_product_id, draft: body.draft });
} catch (err) {
recoverable = isRecoverable(err);
status = 'failed';
if (err instanceof ApiError) {
error = `${err.message}${err.code === 'unpublishable' && Array.isArray((err.detail as { missing?: string[] })?.missing) ? ` (missing: ${(err.detail as { missing: string[] }).missing.join(', ')})` : ''}`;
} else {
error = 'The store is unreachable — you can retry.';
}
}
}
</script>
<section>
<h2>4. Publish (PUBLISH)</h2>
<p>Ready set validated → SKU lookup → taxonomy refresh → PNG upload → Woo product. SLAB-UAT-* SKUs are always created as drafts (PUBLISH-10).</p>
<div class="summary">
<p><strong>SKU:</strong> {slab.sku} | <strong>Price:</strong> ${slab.price} | <strong>Bdft:</strong> {slab.bdft}</p>
<p><strong>Title:</strong> {slab.title}</p>
<p>Photos: {slab.photos.length}</p>
</div>
{#if status}
<p class="status" class:ok={!!result}>{status}</p>
{/if}
{#if result}
<p class="ok">Woo product id: {result.woo_product_id}{result.draft ? ' (draft)' : ''}</p>
{/if}
{#if error}
<p class="err" role="alert">{error}{recoverable ? '' : ' Retrying will not help.'}</p>
{/if}
<div class="actions">
<button on:click={() => dispatch('back')}>← Back to review</button>
<button on:click={attemptPublish} disabled={!!result || (status === 'failed' && !recoverable)} class="primary">
{result ? 'Published' : status === 'failed' ? 'Retry publish' : 'Publish to Woo'}
</button>
</div>
</section>
<style>
.summary { background:#1a1a1a; padding:1rem; border-radius:4px; }
.status { color:#0af; }
.status.ok { color:#0a7; }
.err { color:#f88; }
.ok { color:#0a7; }
.primary { background:#0a7; color:#fff; border:none; }
.actions { display:flex; gap:0.75rem; margin-top:1rem; }
</style>