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
+191
View File
@@ -0,0 +1,191 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { apiFetch } from '../lib/api';
const dispatch = createEventDispatcher<{ close: void }>();
interface SecretState {
configured: boolean;
}
interface Settings {
woo_base_url: string;
woo_wp_username: string | null;
woo_app_password: SecretState | string;
inference_base_url: SecretState | string;
inference_api_key: SecretState | string;
inference_model: string | null;
inference_enabled: boolean;
content_llm_enabled: boolean;
woo_create_status: string;
brand_voice: string | null;
geo_context: string | null;
crop_fill: number;
taxonomy_anchor: string | null;
taxonomy_last_sync_at: string | null;
[key: string]: unknown;
}
let settings: Settings | null = null;
let saveMsg = '';
let saveError = '';
let saving = false;
let testMsg = '';
let testErr = '';
let testing = '' as '' | 'woo' | 'inference';
let wooUser = '';
let wooPass = '';
let infBase = '';
let infKey = '';
let infModel = '';
let infEnabled = false;
let contentEnabled = false;
let createStatus = 'draft';
let brandVoice = '';
let geoContext = '';
let cropFill = 90;
$: configured = (v: SecretState | string | null | undefined): boolean =>
typeof v === 'object' && v !== null ? v.configured : Boolean(v);
async function load() {
try {
const s = await apiFetch<Settings>('/settings');
settings = s;
wooUser = s.woo_wp_username ?? '';
infModel = s.inference_model ?? '';
infEnabled = s.inference_enabled;
contentEnabled = s.content_llm_enabled;
createStatus = s.woo_create_status || 'draft';
brandVoice = s.brand_voice ?? '';
geoContext = s.geo_context ?? '';
cropFill = s.crop_fill ?? 90;
} catch (err) {
saveError = err instanceof Error ? err.message : 'Failed to load settings';
}
}
async function save() {
saving = true;
saveMsg = '';
saveError = '';
const payload: Record<string, unknown> = {
woo_wp_username: wooUser,
inference_model: infModel,
inference_enabled: infEnabled,
content_llm_enabled: contentEnabled,
woo_create_status: createStatus,
brand_voice: brandVoice,
geo_context: geoContext,
crop_fill: cropFill,
};
if (wooPass) payload.woo_app_password = wooPass;
if (infBase) payload.inference_base_url = infBase;
if (infKey) payload.inference_api_key = infKey;
try {
settings = await apiFetch<Settings>('/settings', { method: 'PUT', body: JSON.stringify(payload) });
wooPass = '';
infBase = '';
infKey = '';
saveMsg = 'Saved.';
} catch (err) {
saveError = err instanceof Error ? err.message : 'Save failed';
} finally {
saving = false;
}
}
async function runTest(which: 'woo' | 'inference') {
testing = which;
testMsg = '';
testErr = '';
const path = which === 'woo' ? '/settings/test-woo' : '/admin/test-inference';
try {
const res = await apiFetch<Record<string, unknown>>(path, { method: 'POST' });
testMsg = which === 'woo'
? `Woo: ok=${String(res.ok)} — ${String(res.detail ?? '')}`
: `Inference: ok=${String(res.ok)} — ${String(res.detail ?? '')}`;
} catch (err) {
testErr = err instanceof Error ? err.message : 'Test failed';
} finally {
testing = '';
}
}
onMount(load);
</script>
<section>
<h2>Settings (SETTINGS)</h2>
{#if !settings}
<p>{saveError || 'Loading…'}</p>
{:else}
<div class="grid">
<fieldset>
<legend>Woo (SETTINGS-10/25/75)</legend>
<p>Base URL (env-only, SETTINGS-85): <code>{settings.woo_base_url || 'not set'}</code></p>
<label>WP username <input bind:value={wooUser} /></label>
<label>App password {configured(settings.woo_app_password) ? '(set — leave blank to keep)' : '(not set)'}
<input type="password" bind:value={wooPass} autocomplete="new-password" />
</label>
<label>Create status
<select bind:value={createStatus}>
<option value="draft">draft</option>
<option value="publish">publish</option>
</select>
</label>
</fieldset>
<fieldset>
<legend>Inference (SETTINGS-30/45/90)</legend>
<label>Base URL {configured(settings.inference_base_url) ? '(set — leave blank to keep)' : '(not set)'}
<input bind:value={infBase} placeholder="https://…" />
</label>
<label>API key {configured(settings.inference_api_key) ? '(set — leave blank to keep)' : '(not set)'}
<input type="password" bind:value={infKey} autocomplete="new-password" />
</label>
<label>Model <input bind:value={infModel} placeholder="gpt-4o-mini" /></label>
<label><input type="checkbox" bind:checked={infEnabled} /> Enable Call 1 (vision classification)</label>
<label><input type="checkbox" bind:checked={contentEnabled} /> Enable Call 2 (long description)</label>
</fieldset>
<fieldset>
<legend>Store copy & crop</legend>
<label>Brand voice <input bind:value={brandVoice} /></label>
<label>Geo context <input bind:value={geoContext} placeholder="Whidbey Island, WA" /></label>
<label>Crop fill % (1–100) <input type="number" min="1" max="100" bind:value={cropFill} /></label>
</fieldset>
</div>
<div class="meta">
<small>Taxonomy: {settings.taxonomy_last_sync_at ?? 'never synced'}{settings.taxonomy_anchor ? ` (anchor ${String(settings.taxonomy_anchor).slice(0, 8)}…)` : ''}</small>
</div>
<div class="actions">
<button on:click={save} disabled={saving}>{saving ? 'Saving…' : 'Save'}</button>
<button on:click={() => runTest('woo')} disabled={testing !== ''}>Test Woo {testing === 'woo' ? '…' : ''}</button>
<button on:click={() => runTest('inference')} disabled={testing !== ''}>Test inference {testing === 'inference' ? '…' : ''}</button>
<button on:click={() => dispatch('close')}>Close</button>
</div>
{#if saveMsg}<p class="ok">{saveMsg}</p>{/if}
{#if saveError}<p class="err">{saveError}</p>{/if}
{#if testMsg}<p class="ok">{testMsg}</p>{/if}
{#if testErr}<p class="err">{testErr}</p>{/if}
<small>Secrets are write-only: GET shows a configured flag only (SETTINGS-75). Re-enter a secret to replace it.</small>
{/if}
</section>
<style>
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:1rem; }
fieldset { border:1px solid #333; border-radius:4px; padding:0.75rem; }
legend { color:#888; font-size:0.85rem; }
label { display:block; font-size:0.9rem; margin:0.4rem 0; }
input,select { width:100%; padding:0.35rem; background:#222; color:#eee; border:1px solid #555; margin-top:0.2rem; box-sizing:border-box; }
code { color:#0af; }
.actions { display:flex; gap:0.5rem; margin-top:1rem; flex-wrap:wrap; }
.ok { color:#0a7; }
.err { color:#f88; }
.meta { margin-top:0.75rem; }
</style>