b3380f3848
- backend/crypto.py: AES-GCM (cryptography.AESGCM); key=base64(SLAB_AES_KEY)
must be exactly 32 bytes; value_enc=base64(nonce(12)||ct); DecryptError on tamper
- backend/settings.py: get_settings (secrets->{configured:bool}, woo_base_url env
read-only, flags default false), update_settings (rejects woo_base_url SETTINGS-85,
encrypts secrets, partial plain writes)
- backend/app.py: wire GET/PUT /api/v1/settings
- frontend/src/create/settings/Settings.svelte: masked form + partial PUT
- tests: crypto roundtrip/tamper, masked GET, encrypted PUT, reject woo_base_url,
flags default, decrypt-fail->configured:false (TDD)
Verified live: secrets round-trip configured:true with valid key; woo_base_url write
rejected 400; no secret leak in GET. Gates: check_requirements + check_coverage exit 0;
pytest 13 passed. Deploy+build on ubudual5060 (never Windows).
111 lines
3.3 KiB
Svelte
111 lines
3.3 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import { supported } from './browser';
|
|
import { createCaptureSession, type CaptureSession } from './capture/session';
|
|
import type { Capture } from './capture/capture.types';
|
|
import Resume from './capture/Resume.svelte';
|
|
import BatchPhotos from './capture/BatchPhotos.svelte';
|
|
import PickCalibration from './capture/PickCalibration.svelte';
|
|
import MaskPanel from './capture/MaskPanel.svelte';
|
|
import AxisPanel from './capture/AxisPanel.svelte';
|
|
import MeasurePanel from './capture/MeasurePanel.svelte';
|
|
import Settings from './create/settings/Settings.svelte';
|
|
|
|
const gate = typeof window !== 'undefined' ? supported() : 'window unavailable';
|
|
const session: CaptureSession = createCaptureSession();
|
|
|
|
let cap: Capture = { k: 'batch', photos: [] };
|
|
let busy = false;
|
|
let pngUrl: string | null = null;
|
|
let showSettings = false;
|
|
|
|
const unsubCap = session.capture.subscribe((c) => {
|
|
cap = c;
|
|
pngUrl = c.k === 'measure' ? session.getInventoryUrl() : null;
|
|
});
|
|
const unsubBusy = session.busy.subscribe((b) => {
|
|
busy = b;
|
|
});
|
|
|
|
onMount(() => {
|
|
void session.resumeProbe();
|
|
});
|
|
onDestroy(() => {
|
|
unsubCap();
|
|
unsubBusy();
|
|
session.dispose();
|
|
});
|
|
</script>
|
|
|
|
{#if gate}
|
|
<main class="hardstop">
|
|
<h1>Unsupported browser</h1>
|
|
<p>
|
|
SlabUploader needs a modern browser with canvas, workers, and IndexedDB support:
|
|
iOS Safari, Android Chrome, or desktop Chrome, Edge, or Firefox.
|
|
</p>
|
|
<p class="detail">Missing: {gate}</p>
|
|
</main>
|
|
{:else}
|
|
<main class="app">
|
|
{#if cap.k === 'resume'}
|
|
<Resume onContinue={() => session.continueDraft()} onFresh={() => void session.startFresh()} />
|
|
{:else if cap.k === 'batch'}
|
|
<BatchPhotos
|
|
photos={cap.photos}
|
|
onAdd={(f) => void session.addToBatch(f)}
|
|
onRemove={session.removeFromBatch}
|
|
onContinue={session.continueBatch}
|
|
/>
|
|
{:else if cap.k === 'pickCalibration'}
|
|
<PickCalibration
|
|
photos={cap.photos}
|
|
onPick={session.pickCalibration}
|
|
onBack={session.back}
|
|
/>
|
|
{:else if cap.k === 'mask'}
|
|
<MaskPanel
|
|
state={cap}
|
|
{busy}
|
|
setSensitivity={session.setSensitivity}
|
|
setSheet={session.setSheet}
|
|
approveMask={session.approveMask}
|
|
retakes={session.retakes}
|
|
back={session.back}
|
|
/>
|
|
{:else if cap.k === 'axis'}
|
|
<AxisPanel
|
|
state={cap}
|
|
setAxisAngle={session.setAxisAngle}
|
|
approveAxis={session.approveAxis}
|
|
back={session.back}
|
|
/>
|
|
{:else}
|
|
<MeasurePanel
|
|
state={cap}
|
|
setLength={session.setLength}
|
|
setThickness={session.setThickness}
|
|
calculate={session.calculate}
|
|
back={session.back}
|
|
{pngUrl}
|
|
/>
|
|
{/if}
|
|
<button on:click={() => (showSettings = !showSettings)} style="margin-top:1rem">Settings (dev)</button>
|
|
{#if showSettings}
|
|
<Settings />
|
|
{/if}
|
|
</main>
|
|
{/if}
|
|
|
|
<style>
|
|
:global(body) {
|
|
margin: 0;
|
|
background: #121212;
|
|
font-family: system-ui, -apple-system, Segoe UI, sans-serif;
|
|
}
|
|
.app, .hardstop { max-width: 720px; margin: 0 auto; min-height: 100vh; }
|
|
.hardstop { padding: 48px 24px; color: #eee; text-align: center; }
|
|
.hardstop h1 { font-size: 1.6rem; }
|
|
.detail { color: #888; font-size: 0.85rem; }
|
|
</style>
|