fix: barcode scan performance + timeout + success message

- 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")
This commit is contained in:
pyr0ball 2026-04-16 09:57:53 -07:00
parent 200a6ef87b
commit 9a277f9b42
3 changed files with 33 additions and 25 deletions

View file

@ -32,10 +32,11 @@ class OpenFoodFactsService:
"https://world.openproductsfacts.org/api/v2", "https://world.openproductsfacts.org/api/v2",
] ]
async def _lookup_in_database(self, barcode: str, base_url: str) -> Optional[Dict[str, Any]]: async def _lookup_in_database(
"""Try one Open*Facts database. Returns parsed product dict or None.""" self, barcode: str, base_url: str, client: httpx.AsyncClient
) -> Optional[Dict[str, Any]]:
"""Try one Open*Facts database using an existing client. Returns parsed product dict or None."""
try: try:
async with httpx.AsyncClient() as client:
response = await client.get( response = await client.get(
f"{base_url}/product/{barcode}.json", f"{base_url}/product/{barcode}.json",
headers={"User-Agent": self.USER_AGENT}, headers={"User-Agent": self.USER_AGENT},
@ -59,18 +60,22 @@ class OpenFoodFactsService:
""" """
Look up a product by barcode, trying OFFs then fallback databases. Look up a product by barcode, trying OFFs then fallback databases.
A single httpx.AsyncClient is created for the whole lookup chain so that
connection pooling and TLS session reuse apply across all database attempts.
Args: Args:
barcode: UPC/EAN barcode (8-13 digits) barcode: UPC/EAN barcode (8-13 digits)
Returns: Returns:
Dictionary with product information, or None if not found in any database. Dictionary with product information, or None if not found in any database.
""" """
result = await self._lookup_in_database(barcode, self.BASE_URL) async with httpx.AsyncClient() as client:
result = await self._lookup_in_database(barcode, self.BASE_URL, client)
if result: if result:
return result return result
for db_url in self._FALLBACK_DATABASES: for db_url in self._FALLBACK_DATABASES:
result = await self._lookup_in_database(barcode, db_url) result = await self._lookup_in_database(barcode, db_url, client)
if result: if result:
logger.info("Barcode %s found in fallback database: %s", barcode, db_url) logger.info("Barcode %s found in fallback database: %s", barcode, db_url)
return result return result

View file

@ -454,7 +454,7 @@ import { storeToRefs } from 'pinia'
import { useInventoryStore } from '../stores/inventory' import { useInventoryStore } from '../stores/inventory'
import { useSettingsStore } from '../stores/settings' import { useSettingsStore } from '../stores/settings'
import { inventoryAPI } from '../services/api' import { inventoryAPI } from '../services/api'
import type { InventoryItem, BarcodeScanResponse } from '../services/api' import type { InventoryItem } from '../services/api'
import { formatQuantity } from '../utils/units' import { formatQuantity } from '../utils/units'
import EditItemModal from './EditItemModal.vue' import EditItemModal from './EditItemModal.vue'
import ConfirmDialog from './ConfirmDialog.vue' import ConfirmDialog from './ConfirmDialog.vue'
@ -716,9 +716,11 @@ async function handleScannerGunInput() {
const item = result.results[0] const item = result.results[0]
if (item?.added_to_inventory) { if (item?.added_to_inventory) {
const productName = item.product?.name || 'item'
const productBrand = item.product?.brand ? ` (${item.product.brand})` : ''
scannerResults.value.push({ scannerResults.value.push({
type: 'success', type: 'success',
message: `Added: ${item.product?.name || 'item'} to ${scannerLocation.value}`, message: `Added: ${productName}${productBrand} to ${scannerLocation.value}`,
}) })
await refreshItems() await refreshItems()
} else if (item?.needs_manual_entry) { } else if (item?.needs_manual_entry) {

View file

@ -585,7 +585,8 @@ export interface BuildRequest {
export const recipesAPI = { export const recipesAPI = {
async suggest(req: RecipeRequest): Promise<RecipeResult> { async suggest(req: RecipeRequest): Promise<RecipeResult> {
const response = await api.post('/recipes/suggest', req) // Allow up to 120s — cf-orch model cold-start can take 60+ seconds on first request
const response = await api.post('/recipes/suggest', req, { timeout: 120000 })
return response.data return response.data
}, },
async getRecipe(id: number): Promise<RecipeSuggestion> { async getRecipe(id: number): Promise<RecipeSuggestion> {