Tested and working, added ability to use vite
This commit is contained in:
@@ -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,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
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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": {
|
"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",
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
try {
|
||||||
|
if (DEBUG) console.log('[scanner] loading zxing-wasm…')
|
||||||
const mod = await import('zxing-wasm')
|
const mod = await import('zxing-wasm')
|
||||||
// zxing-wasm v3 exports readBarcodesFromImageData
|
// zxing-wasm v3 exports readBarcodesFromImageData
|
||||||
zxingReader = mod.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)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user