Tested and working, added ability to use vite
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
name: be-implementer
|
||||
description: Backend implementer for FastAPI + SQLAlchemy + SQLite
|
||||
tools: read, write, edit, grep, find, ls, bash
|
||||
model: deepseek/deepseek-v4-pro
|
||||
thinking: xhigh
|
||||
model: deepseek/deepseek-v4-flash
|
||||
thinking: high
|
||||
---
|
||||
|
||||
You are a backend implementer. Write code, run tests, iterate until green.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
name: fe-implementer
|
||||
description: Frontend implementer for Svelte 5 + Vite
|
||||
tools: read, write, edit, grep, find, ls, bash
|
||||
model: deepseek/deepseek-v4-pro
|
||||
thinking: xhigh
|
||||
model: deepseek/deepseek-v4-flash
|
||||
thinking: high
|
||||
---
|
||||
|
||||
You are a frontend implementer. Write code, run tests, iterate until green.
|
||||
|
||||
+15
-4
@@ -155,15 +155,26 @@ detection in `services/nutrition.py`). Read the TICKET-007 section of
|
||||
the camera+decode loop (native BarcodeDetector + lazy zxing-wasm,
|
||||
~4fps, teardown in `stop()`). `BarcodeScanner.svelte` stops on first
|
||||
detect + on destroy (§8.2 rule 6).
|
||||
- **§8.4 phone checklist PENDING USER TESTING** (native BarcodeDetector on
|
||||
Android Chrome, zxing-wasm on iOS Safari, EAN-13/UPC-A decode). Requires
|
||||
Caddy HTTPS. The manual-barcode fallback + OFF search flows are
|
||||
playwright-verified; do not regress them.
|
||||
- **§8.4 phone checklist — tested** (2026-07-26):
|
||||
- ✅ Desktop webcam (native BarcodeDetector on Chrome): works with good
|
||||
lighting. Scanner requests 640×480 min resolution. Added `[scanner]`/
|
||||
`[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
|
||||
responses were cached within a Vite dev session; a fresh Vite restart
|
||||
cleared it. Not a code defect (production build unaffected). If it
|
||||
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
|
||||
|
||||
- `.playwright-cli/` artifacts are polluting the repo (some were even tracked
|
||||
|
||||
@@ -59,3 +59,6 @@ cd frontend && npm test # frontend suite (vitest)
|
||||
```bash
|
||||
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.
|
||||
@@ -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
|
||||
Generated
+14
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@vitejs/plugin-basic-ssl": "^2.3.0",
|
||||
"svelte": "^5.56.4",
|
||||
"vite": "^7.3.6",
|
||||
"vitest": "^4.1.10"
|
||||
@@ -953,6 +954,19 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:host": "vite --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@vitejs/plugin-basic-ssl": "^2.3.0",
|
||||
"svelte": "^5.56.4",
|
||||
"vite": "^7.3.6",
|
||||
"vitest": "^4.1.10"
|
||||
|
||||
+13
-2
@@ -107,37 +107,48 @@
|
||||
}
|
||||
|
||||
async function handleBarcode(barcode) {
|
||||
console.log('[App] handleBarcode:', barcode)
|
||||
resetScanFlow()
|
||||
scanBarcode = barcode
|
||||
scanLoading = true
|
||||
|
||||
// Step 1: Check local DB by barcode
|
||||
try {
|
||||
console.log('[App] searching local DB for barcode:', barcode)
|
||||
const results = await api.searchFoodsByBarcode(barcode)
|
||||
console.log('[App] local search results:', results?.length ?? 0)
|
||||
if (results && results.length > 0) {
|
||||
localFood = results[0]
|
||||
scanLogQuantity = defaultQuantity(localFood)
|
||||
scanLoading = false
|
||||
scanPhase = 'localFound'
|
||||
console.log('[App] local food found:', localFood.name)
|
||||
return
|
||||
}
|
||||
console.log('[App] not found locally, trying OpenFoodFacts…')
|
||||
} catch (e) {
|
||||
console.warn('[App] local search failed:', e)
|
||||
// Local lookup failed — try OFF anyway
|
||||
}
|
||||
|
||||
// Step 2: Not found locally → try OFF
|
||||
try {
|
||||
console.log('[App] fetching OFF product:', barcode)
|
||||
offFood = await api.offProduct(barcode)
|
||||
scanLoading = false
|
||||
scanPhase = 'offFound'
|
||||
console.log('[App] OFF product found:', offFood?.product_name ?? offFood?.name)
|
||||
} catch (e) {
|
||||
console.warn('[App] OFF lookup failed:', e.message)
|
||||
// OFF miss (404) or network error
|
||||
scanLoading = false
|
||||
if (e.message?.includes('404') || e.message?.includes('not found')) {
|
||||
scanPhase = 'notFound'
|
||||
console.log('[App] OFF product not found (404)')
|
||||
} else {
|
||||
scanError = e.message
|
||||
scanPhase = 'error'
|
||||
console.error('[App] OFF lookup error:', e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -572,8 +583,8 @@
|
||||
padding: 0 0.3rem;
|
||||
}
|
||||
.macro-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.form-actions {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
let stopHandle = null
|
||||
|
||||
function handleDetected(barcode) {
|
||||
console.log('[BarcodeScanner] 🎯 barcode detected:', barcode)
|
||||
// Stop scanning once we have a barcode
|
||||
if (stopHandle) {
|
||||
stopHandle.stop()
|
||||
@@ -28,6 +29,7 @@
|
||||
}
|
||||
|
||||
function handleError(err) {
|
||||
console.error('[BarcodeScanner] camera error:', err)
|
||||
loading = false
|
||||
if (isPermissionDeniedError(err)) {
|
||||
cameraUnavailable = true
|
||||
@@ -45,6 +47,7 @@
|
||||
e.preventDefault()
|
||||
const code = manualBarcode.trim()
|
||||
if (!code) return
|
||||
console.log('[BarcodeScanner] manual barcode submitted:', code)
|
||||
handleDetected(code)
|
||||
}
|
||||
|
||||
|
||||
@@ -538,8 +538,8 @@
|
||||
}
|
||||
|
||||
.macro-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,13 @@
|
||||
*
|
||||
* NOTE: getUserMedia requires a secure context — HTTPS via the Caddy
|
||||
* 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
|
||||
// on the native path never pay the WASM download cost.
|
||||
// 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
|
||||
const vw = videoEl.videoWidth
|
||||
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.height = vh
|
||||
ctx.drawImage(videoEl, 0, 0, vw, vh)
|
||||
|
||||
if (DEBUG) console.log('[scanner] frame extracted', vw, '×', vh)
|
||||
|
||||
let barcode = null
|
||||
|
||||
if (detector) {
|
||||
// Native BarcodeDetector path
|
||||
try {
|
||||
const detections = await detector.detect(canvas)
|
||||
if (DEBUG) console.log('[scanner] BarcodeDetector returned', detections.length, 'results')
|
||||
if (detections.length > 0 && !stopped) {
|
||||
barcode = detections[0].rawValue
|
||||
if (DEBUG) console.log('[scanner] 🎯 detected via BarcodeDetector:', barcode)
|
||||
}
|
||||
} catch {
|
||||
// Native detector can throw on some frames; ignore and try next
|
||||
} catch (e) {
|
||||
console.warn('[scanner] BarcodeDetector error on frame:', e)
|
||||
}
|
||||
} else {
|
||||
// zxing-wasm fallback — load once
|
||||
if (!zxingReader) {
|
||||
try {
|
||||
if (DEBUG) console.log('[scanner] loading zxing-wasm…')
|
||||
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 {
|
||||
const imageData = ctx.getImageData(0, 0, vw, vh)
|
||||
const results = await zxingReader(imageData, {
|
||||
formats: BARCODE_FORMATS,
|
||||
})
|
||||
if (DEBUG) console.log('[scanner] zxing returned', results.length, 'results')
|
||||
if (results.length > 0 && !stopped) {
|
||||
barcode = results[0].text
|
||||
if (DEBUG) console.log('[scanner] 🎯 detected via zxing:', barcode)
|
||||
}
|
||||
} catch {
|
||||
// zxing decode errors on non-barcode frames — ignore
|
||||
} catch (e) {
|
||||
console.warn('[scanner] zxing decode error on frame:', e)
|
||||
}
|
||||
}
|
||||
|
||||
if (barcode && !stopped) {
|
||||
onDetect(barcode)
|
||||
}
|
||||
} catch {
|
||||
// Frame extraction can fail if video isn't ready yet — ignore
|
||||
} catch (e) {
|
||||
console.warn('[scanner] frame extraction error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Start camera ──────────────────────────────────────────────────────
|
||||
;(async () => {
|
||||
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({
|
||||
video: { facingMode: 'environment' },
|
||||
video: {
|
||||
facingMode: 'environment',
|
||||
width: { min: 640, ideal: 1280 },
|
||||
height: { min: 480, ideal: 720 },
|
||||
},
|
||||
audio: false,
|
||||
})
|
||||
if (stopped) {
|
||||
@@ -143,9 +171,15 @@ export function startScanner(videoEl, { onDetect, onError }) {
|
||||
videoEl.srcObject = stream
|
||||
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
|
||||
timer = setInterval(decodeFrame, DECODE_INTERVAL_MS)
|
||||
} catch (err) {
|
||||
console.error('[scanner] getUserMedia failed:', err)
|
||||
if (!stopped) {
|
||||
onError(err instanceof Error ? err : new Error(String(err)))
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import basicSsl from '@vitejs/plugin-basic-ssl'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
plugins: [svelte(), basicSsl()],
|
||||
server: {
|
||||
// Dev proxy so the frontend can call /api without CORS in production-style setups
|
||||
proxy: {
|
||||
|
||||
Reference in New Issue
Block a user