Tested and working, added ability to use vite

This commit is contained in:
Craig
2026-08-02 18:46:23 +01:00
parent fc30b28921
commit 42c4deca75
12 changed files with 143 additions and 25 deletions
+2 -2
View File
@@ -2,8 +2,8 @@
name: be-implementer name: be-implementer
description: Backend implementer for FastAPI + SQLAlchemy + SQLite description: Backend implementer for FastAPI + SQLAlchemy + SQLite
tools: read, write, edit, grep, find, ls, bash tools: read, write, edit, grep, find, ls, bash
model: deepseek/deepseek-v4-pro model: deepseek/deepseek-v4-flash
thinking: xhigh thinking: high
--- ---
You are a backend implementer. Write code, run tests, iterate until green. You are a backend implementer. Write code, run tests, iterate until green.
+2 -2
View File
@@ -2,8 +2,8 @@
name: fe-implementer name: fe-implementer
description: Frontend implementer for Svelte 5 + Vite description: Frontend implementer for Svelte 5 + Vite
tools: read, write, edit, grep, find, ls, bash tools: read, write, edit, grep, find, ls, bash
model: deepseek/deepseek-v4-pro model: deepseek/deepseek-v4-flash
thinking: xhigh thinking: high
--- ---
You are a frontend implementer. Write code, run tests, iterate until green. You are a frontend implementer. Write code, run tests, iterate until green.
+15 -4
View File
@@ -155,15 +155,26 @@ detection in `services/nutrition.py`). Read the TICKET-007 section of
the camera+decode loop (native BarcodeDetector + lazy zxing-wasm, the camera+decode loop (native BarcodeDetector + lazy zxing-wasm,
~4fps, teardown in `stop()`). `BarcodeScanner.svelte` stops on first ~4fps, teardown in `stop()`). `BarcodeScanner.svelte` stops on first
detect + on destroy (§8.2 rule 6). detect + on destroy (§8.2 rule 6).
- **§8.4 phone checklist PENDING USER TESTING** (native BarcodeDetector on - **§8.4 phone checklist — tested** (2026-07-26):
Android Chrome, zxing-wasm on iOS Safari, EAN-13/UPC-A decode). Requires - ✅ Desktop webcam (native BarcodeDetector on Chrome): works with good
Caddy HTTPS. The manual-barcode fallback + OFF search flows are lighting. Scanner requests 640×480 min resolution. Added `[scanner]`/
playwright-verified; do not regress them. `[BarcodeScanner]`/`[App]` console logging; set
`localStorage.debugScanner = 'true'` for per-frame verbose logs.
- ✅ Phone camera (native BarcodeDetector on Android Chrome): **works**.
Tested via Vite dev server with `@vitejs/plugin-basic-ssl` (auto-generates
self-signed cert) + `npm run dev:host` (`vite --host`). Phone accepts the
browser security warning and camera works. Backend proxy unchanged.
- ✅ Manual barcode input + OFF search flows remain playwright-verified;
do not regress them.
- **Dev-only Vite proxy caching** observed by QA: empty OFF search - **Dev-only Vite proxy caching** observed by QA: empty OFF search
responses were cached within a Vite dev session; a fresh Vite restart responses were cached within a Vite dev session; a fresh Vite restart
cleared it. Not a code defect (production build unaffected). If it cleared it. Not a code defect (production build unaffected). If it
recurs, check the vite proxy config (`changeOrigin`, cache headers). recurs, check the vite proxy config (`changeOrigin`, cache headers).
- Fixed macro grid on FoodEditor and target form (App.svelte): was 3-column
grid that overflowed on mobile; changed to `flex-direction: column` so
Protein/Carbs/Fat inputs stack vertically on narrow screens.
## Loose ends / chores ## Loose ends / chores
- `.playwright-cli/` artifacts are polluting the repo (some were even tracked - `.playwright-cli/` artifacts are polluting the repo (some were even tracked
+3
View File
@@ -59,3 +59,6 @@ cd frontend && npm test # frontend suite (vitest)
```bash ```bash
cd frontend && npm run build # production build to frontend/dist/ cd frontend && npm run build # production build to frontend/dist/
``` ```
# Agentic dev
Defined subagents, idea is big strong agent tells the little ones what to do. I maybe have overkilled on the "small" agents, v4 pro is till pretty powerful. Play around with it and see what we can get away with.
Executable
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
BACKEND_DIR="$ROOT_DIR/backend"
FRONTEND_DIR="$ROOT_DIR/frontend"
echo "=== Starting CalCount dev servers ==="
# Kill background processes on exit
cleanup() {
echo ""
echo "Shutting down..."
kill "$BACKEND_PID" "$FRONTEND_PID" 2>/dev/null || true
wait "$BACKEND_PID" "$FRONTEND_PID" 2>/dev/null || true
echo "Done."
}
trap cleanup EXIT SIGINT SIGTERM
# Start backend (FastAPI via uvicorn)
echo "[backend] uvicorn main:app --reload --host 0.0.0.0"
cd "$BACKEND_DIR"
uv run uvicorn main:app --reload --host 0.0.0.0 &
BACKEND_PID=$!
# Start frontend (Vite dev server with --host)
echo "[frontend] vite --host"
cd "$FRONTEND_DIR"
npm run dev:host &
FRONTEND_PID=$!
echo ""
echo " Backend: http://localhost:8000"
echo " Frontend: http://localhost:5173"
echo " Press Ctrl+C to stop both."
echo ""
# Wait for either process to exit
wait
+14
View File
@@ -12,6 +12,7 @@
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.4", "@sveltejs/vite-plugin-svelte": "^6.2.4",
"@vitejs/plugin-basic-ssl": "^2.3.0",
"svelte": "^5.56.4", "svelte": "^5.56.4",
"vite": "^7.3.6", "vite": "^7.3.6",
"vitest": "^4.1.10" "vitest": "^4.1.10"
@@ -953,6 +954,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vitejs/plugin-basic-ssl": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz",
"integrity": "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"peerDependencies": {
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/@vitest/expect": { "node_modules/@vitest/expect": {
"version": "4.1.10", "version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
+2
View File
@@ -5,12 +5,14 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"dev:host": "vite --host",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest run" "test": "vitest run"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.4", "@sveltejs/vite-plugin-svelte": "^6.2.4",
"@vitejs/plugin-basic-ssl": "^2.3.0",
"svelte": "^5.56.4", "svelte": "^5.56.4",
"vite": "^7.3.6", "vite": "^7.3.6",
"vitest": "^4.1.10" "vitest": "^4.1.10"
+13 -2
View File
@@ -107,37 +107,48 @@
} }
async function handleBarcode(barcode) { async function handleBarcode(barcode) {
console.log('[App] handleBarcode:', barcode)
resetScanFlow() resetScanFlow()
scanBarcode = barcode scanBarcode = barcode
scanLoading = true scanLoading = true
// Step 1: Check local DB by barcode // Step 1: Check local DB by barcode
try { try {
console.log('[App] searching local DB for barcode:', barcode)
const results = await api.searchFoodsByBarcode(barcode) const results = await api.searchFoodsByBarcode(barcode)
console.log('[App] local search results:', results?.length ?? 0)
if (results && results.length > 0) { if (results && results.length > 0) {
localFood = results[0] localFood = results[0]
scanLogQuantity = defaultQuantity(localFood) scanLogQuantity = defaultQuantity(localFood)
scanLoading = false scanLoading = false
scanPhase = 'localFound' scanPhase = 'localFound'
console.log('[App] local food found:', localFood.name)
return return
} }
console.log('[App] not found locally, trying OpenFoodFacts…')
} catch (e) { } catch (e) {
console.warn('[App] local search failed:', e)
// Local lookup failed — try OFF anyway // Local lookup failed — try OFF anyway
} }
// Step 2: Not found locally → try OFF // Step 2: Not found locally → try OFF
try { try {
console.log('[App] fetching OFF product:', barcode)
offFood = await api.offProduct(barcode) offFood = await api.offProduct(barcode)
scanLoading = false scanLoading = false
scanPhase = 'offFound' scanPhase = 'offFound'
console.log('[App] OFF product found:', offFood?.product_name ?? offFood?.name)
} catch (e) { } catch (e) {
console.warn('[App] OFF lookup failed:', e.message)
// OFF miss (404) or network error // OFF miss (404) or network error
scanLoading = false scanLoading = false
if (e.message?.includes('404') || e.message?.includes('not found')) { if (e.message?.includes('404') || e.message?.includes('not found')) {
scanPhase = 'notFound' scanPhase = 'notFound'
console.log('[App] OFF product not found (404)')
} else { } else {
scanError = e.message scanError = e.message
scanPhase = 'error' scanPhase = 'error'
console.error('[App] OFF lookup error:', e.message)
} }
} }
} }
@@ -572,8 +583,8 @@
padding: 0 0.3rem; padding: 0 0.3rem;
} }
.macro-grid { .macro-grid {
display: grid; display: flex;
grid-template-columns: 1fr 1fr 1fr; flex-direction: column;
gap: 0.5rem; gap: 0.5rem;
} }
.form-actions { .form-actions {
@@ -19,6 +19,7 @@
let stopHandle = null let stopHandle = null
function handleDetected(barcode) { function handleDetected(barcode) {
console.log('[BarcodeScanner] 🎯 barcode detected:', barcode)
// Stop scanning once we have a barcode // Stop scanning once we have a barcode
if (stopHandle) { if (stopHandle) {
stopHandle.stop() stopHandle.stop()
@@ -28,6 +29,7 @@
} }
function handleError(err) { function handleError(err) {
console.error('[BarcodeScanner] camera error:', err)
loading = false loading = false
if (isPermissionDeniedError(err)) { if (isPermissionDeniedError(err)) {
cameraUnavailable = true cameraUnavailable = true
@@ -45,6 +47,7 @@
e.preventDefault() e.preventDefault()
const code = manualBarcode.trim() const code = manualBarcode.trim()
if (!code) return if (!code) return
console.log('[BarcodeScanner] manual barcode submitted:', code)
handleDetected(code) handleDetected(code)
} }
+2 -2
View File
@@ -538,8 +538,8 @@
} }
.macro-grid { .macro-grid {
display: grid; display: flex;
grid-template-columns: 1fr 1fr 1fr; flex-direction: column;
gap: 0.5rem; gap: 0.5rem;
} }
+45 -11
View File
@@ -8,8 +8,13 @@
* *
* NOTE: getUserMedia requires a secure context — HTTPS via the Caddy * NOTE: getUserMedia requires a secure context — HTTPS via the Caddy
* reverse proxy must be in place before phone testing (spec §5). * reverse proxy must be in place before phone testing (spec §5).
*
* DEBUGGING: Open browser DevTools (F12) → Console to see scanner logs.
* Set localStorage.debugScanner = 'true' for verbose per-frame logging.
*/ */
const DEBUG = typeof globalThis !== 'undefined' && globalThis.localStorage?.getItem('debugScanner') === 'true'
// zxing-wasm is the fallback decoder; imported lazily so Chromium users // zxing-wasm is the fallback decoder; imported lazily so Chromium users
// on the native path never pay the WASM download cost. // on the native path never pay the WASM download cost.
// The module exposes readBarcodesFromImageData for ImageData input. // The module exposes readBarcodesFromImageData for ImageData input.
@@ -82,57 +87,80 @@ export function startScanner(videoEl, { onDetect, onError }) {
// Extract the current video frame into our canvas // Extract the current video frame into our canvas
const vw = videoEl.videoWidth const vw = videoEl.videoWidth
const vh = videoEl.videoHeight const vh = videoEl.videoHeight
if (vw === 0 || vh === 0) return // not playing yet if (vw === 0 || vh === 0) {
if (DEBUG) console.warn('[scanner] video dimensions are 0 — not playing yet?')
return
}
canvas.width = vw canvas.width = vw
canvas.height = vh canvas.height = vh
ctx.drawImage(videoEl, 0, 0, vw, vh) ctx.drawImage(videoEl, 0, 0, vw, vh)
if (DEBUG) console.log('[scanner] frame extracted', vw, '×', vh)
let barcode = null let barcode = null
if (detector) { if (detector) {
// Native BarcodeDetector path // Native BarcodeDetector path
try { try {
const detections = await detector.detect(canvas) const detections = await detector.detect(canvas)
if (DEBUG) console.log('[scanner] BarcodeDetector returned', detections.length, 'results')
if (detections.length > 0 && !stopped) { if (detections.length > 0 && !stopped) {
barcode = detections[0].rawValue barcode = detections[0].rawValue
if (DEBUG) console.log('[scanner] 🎯 detected via BarcodeDetector:', barcode)
} }
} catch { } catch (e) {
// Native detector can throw on some frames; ignore and try next console.warn('[scanner] BarcodeDetector error on frame:', e)
} }
} else { } else {
// zxing-wasm fallback — load once // zxing-wasm fallback — load once
if (!zxingReader) { if (!zxingReader) {
const mod = await import('zxing-wasm') try {
// zxing-wasm v3 exports readBarcodesFromImageData if (DEBUG) console.log('[scanner] loading zxing-wasm…')
zxingReader = mod.readBarcodesFromImageData const mod = await import('zxing-wasm')
// zxing-wasm v3 exports readBarcodesFromImageData
zxingReader = mod.readBarcodesFromImageData
if (DEBUG) console.log('[scanner] zxing-wasm loaded')
} catch (e) {
console.error('[scanner] failed to load zxing-wasm:', e)
return
}
} }
try { try {
const imageData = ctx.getImageData(0, 0, vw, vh) const imageData = ctx.getImageData(0, 0, vw, vh)
const results = await zxingReader(imageData, { const results = await zxingReader(imageData, {
formats: BARCODE_FORMATS, formats: BARCODE_FORMATS,
}) })
if (DEBUG) console.log('[scanner] zxing returned', results.length, 'results')
if (results.length > 0 && !stopped) { if (results.length > 0 && !stopped) {
barcode = results[0].text barcode = results[0].text
if (DEBUG) console.log('[scanner] 🎯 detected via zxing:', barcode)
} }
} catch { } catch (e) {
// zxing decode errors on non-barcode frames — ignore console.warn('[scanner] zxing decode error on frame:', e)
} }
} }
if (barcode && !stopped) { if (barcode && !stopped) {
onDetect(barcode) onDetect(barcode)
} }
} catch { } catch (e) {
// Frame extraction can fail if video isn't ready yet — ignore console.warn('[scanner] frame extraction error:', e)
} }
} }
// ── Start camera ────────────────────────────────────────────────────── // ── Start camera ──────────────────────────────────────────────────────
;(async () => { ;(async () => {
try { try {
// Request a minimum resolution for better barcode detection.
// Desktop webcams default to very low res (e.g. 320×240) which makes
// fine barcode lines hard to read. 640×480 is a good baseline.
stream = await navigator.mediaDevices.getUserMedia({ stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' }, video: {
facingMode: 'environment',
width: { min: 640, ideal: 1280 },
height: { min: 480, ideal: 720 },
},
audio: false, audio: false,
}) })
if (stopped) { if (stopped) {
@@ -143,9 +171,15 @@ export function startScanner(videoEl, { onDetect, onError }) {
videoEl.srcObject = stream videoEl.srcObject = stream
await videoEl.play() await videoEl.play()
if (DEBUG) {
console.log('[scanner] camera started, dimensions:', videoEl.videoWidth, '×', videoEl.videoHeight)
console.log('[scanner] decoder:', detector ? 'native BarcodeDetector' : 'zxing-wasm')
}
// Start decode loop at ~4 fps // Start decode loop at ~4 fps
timer = setInterval(decodeFrame, DECODE_INTERVAL_MS) timer = setInterval(decodeFrame, DECODE_INTERVAL_MS)
} catch (err) { } catch (err) {
console.error('[scanner] getUserMedia failed:', err)
if (!stopped) { if (!stopped) {
onError(err instanceof Error ? err : new Error(String(err))) onError(err instanceof Error ? err : new Error(String(err)))
} }
+2 -1
View File
@@ -1,9 +1,10 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte' import { svelte } from '@sveltejs/vite-plugin-svelte'
import basicSsl from '@vitejs/plugin-basic-ssl'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [svelte()], plugins: [svelte(), basicSsl()],
server: { server: {
// Dev proxy so the frontend can call /api without CORS in production-style setups // Dev proxy so the frontend can call /api without CORS in production-style setups
proxy: { proxy: {