When a barcode scan finds no product in FDC/OFF, paid-tier users now see a
"Capture label" offer instead of a dead-end "add manually" prompt.
Backend:
- Migration 036: captured_products local cache table (keyed by barcode,
UPSERT on conflict so re-capture refreshes rather than errors)
- store.get_captured_product / save_captured_product (with JSON decode for
ingredient_names and allergens)
- app/services/label_capture.py: wraps cf-core VisionRouter (caption API);
graceful fallback to zero-confidence mock when stub/error; JSON fence
stripping; confidence clamped to [0,1]; KIWI_LABEL_CAPTURE_MOCK=1 for tests
- New schemas: LabelCaptureResponse, LabelConfirmRequest, LabelConfirmResponse
- POST /inventory/scan/label-capture — image to extraction (paid+ gate, 403)
- POST /inventory/scan/label-confirm — save confirmed product + optional
inventory add
- Both scan endpoints now: check captured_products cache before FDC/OFF;
set needs_visual_capture=True for gap products on paid tier; BarcodeScanResult
gains needs_visual_capture field
- visual_label_capture feature gate added to tiers.py (paid)
Tests: 42 new tests (service, store/migration, API endpoints) — 367 total passing
Frontend:
- InventoryList.vue: capturePhase state machine (offer => uploading => reviewing)
- Offer card appears after scan gap (calm UX: no urgency, Discard always visible)
- Review form: pre-populated from extraction; amber label highlights for
unread fields (confidence < 0.7); comma-separated ingredients/allergens
- api.ts: LabelCaptureResult + LabelConfirmRequest types; captureLabelPhoto()
and confirmLabelCapture() API methods
- Refactor _lookup_in_database to accept a shared httpx.AsyncClient so
all three Open*Facts database attempts reuse one TLS connection instead
of opening a new one per call; restores pre-fallback scan speed
- Increase recipe suggest timeout to 120s (was 30s) to survive cf-orch
model cold-start on first request of a session
- Include product brand in barcode scan success message so the user can
clearly see what was found (e.g. "Added: Cheerios (General Mills) to pantry")
Adds GET /export/json that bundles inventory and saved recipes into a
single timestamped JSON file for data portability. The export envelope
includes schema version and export timestamp so future import logic can
handle version differences.
Frontend: new primary-styled JSON download button in the Export card with
a short description of what is included.
Closes#62
When a barcode is not found in Open Food Facts, the service now tries
Open Beauty Facts and Open Products Facts before giving up. All three
share the same API format; only the host URL differs.
When all databases miss, the scan endpoint sets needs_manual_entry=true
in the result. The frontend detects this, shows a calm informational
message, and switches to manual entry mode automatically.
Also fixes a latent bug where not-found scans showed 'Added: item to
pantry' due to the success condition checking barcodes_found (always 1)
instead of added_to_inventory.
Closes#65
#12 — partial consume:
- POST /inventory/items/{id}/consume now accepts optional {quantity}
body; decrements by that amount and only marks status=consumed when
quantity reaches zero (store.partial_consume_item)
- OFFs barcode scan pre-fills sub-unit quantity when product data
includes a pack size (number_of_units or 'N x ...' quantity string)
- Consume button shows quantity-aware label and opens ActionDialog
with number input for multi-unit items ('use some or all')
- consumeItem() in api.ts now returns InventoryItem and accepts
optional quantity param
#60 — disposal logging:
- Migration 031: adds disposal_reason TEXT column to inventory_items
(status='discarded' was already in the CHECK constraint)
- POST /inventory/items/{id}/discard endpoint with optional DiscardRequest
body (free text or preset reason)
- Calm framing: 'item not used' not 'wasted'; reason presets avoid
blame language ('went bad before I could use it', 'too much — had excess')
- Muted discard button (X icon, tertiary color) — not alarming
Shared:
- New ActionDialog.vue component for dialogs with inline inputs
(quantity stepper or reason dropdown); keeps ConfirmDialog simple
- disposal_reason field added to InventoryItemResponse
Closes#12Closes#60
- Settings: add unit_system key (metric | imperial, default metric)
- Recipe LLM prompts: inject unit instruction into L3 and L4 prompts
so generated recipes use the user's preferred units throughout
- Frontend: new utils/units.ts converter (mirrors Python units.py)
- Inventory list: display quantities converted to preferred units
- Settings view: metric/imperial toggle with save button
- Settings store: load/save unit_system alongside cooking_equipment
Closes#81
- index.html: inline anti-FOUC CSS so sidebar stays hidden on mobile
before the JS bundle hydrates (eliminates ~100ms flash of sidebar)
- InventoryList: scan-mode-toggle fills card width on <=480px so buttons
don't overflow; mode button labels hidden on <=360px (icons only)
- Very narrow phones get icon-only mode toggle, all three buttons still
accessible via aria-label
- Style/category filter panel with active chip display
- Dismiss (excluded_ids) support — recipes don't reappear until next fresh search
- Load more appends next batch without full re-fetch
- Prep notes 'Before you start:' section above directions
- Nutrition macro chips (kcal, fat, protein, carbs, fiber, sugar, sodium)
- Composables extracted for reuse
- EditItemModal: replace all hardcoded colors (#eee, #f5f5f5, #2196F3, etc.) with CSS variable tokens; restyle modal header with display font, blur backdrop, and theme-aware form elements
- ReceiptsView: replace emoji headings, hardcoded spinner, and non-theme .button class with themed equivalents; all colors through var(--color-*) tokens
- RecipesView: fix broken --color-warning-rgb / --color-primary-rgb references (not defined in theme); use --color-warning-bg and --color-info-bg instead; apply section-title to heading
- SettingsView: apply section-title display-font class to heading for consistency
- InventoryList: remove three dead functions (formatDate, getDaysUntilExpiry, getExpiryClass) that caused TS6133 build errors