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,22 +32,23 @@ class OpenFoodFactsService:
"https://world.openproductsfacts.org/api/v2",
]
async def _lookup_in_database(self, barcode: str, base_url: str) -> Optional[Dict[str, Any]]:
"""Try one Open*Facts database. Returns parsed product dict or None."""
async def _lookup_in_database(
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:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{base_url}/product/{barcode}.json",
headers={"User-Agent": self.USER_AGENT},
timeout=10.0,
)
if response.status_code == 404:
return None
response.raise_for_status()
data = response.json()
if data.get("status") != 1:
return None
return self._parse_product_data(data, barcode)
response = await client.get(
f"{base_url}/product/{barcode}.json",
headers={"User-Agent": self.USER_AGENT},
timeout=10.0,
)
if response.status_code == 404:
return None
response.raise_for_status()
data = response.json()
if data.get("status") != 1:
return None
return self._parse_product_data(data, barcode)
except httpx.HTTPError as e:
logger.debug("HTTP error for %s at %s: %s", barcode, base_url, e)
return None
@ -59,22 +60,26 @@ class OpenFoodFactsService:
"""
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:
barcode: UPC/EAN barcode (8-13 digits)
Returns:
Dictionary with product information, or None if not found in any database.
"""
result = await self._lookup_in_database(barcode, self.BASE_URL)
if result:
return result
for db_url in self._FALLBACK_DATABASES:
result = await self._lookup_in_database(barcode, db_url)
async with httpx.AsyncClient() as client:
result = await self._lookup_in_database(barcode, self.BASE_URL, client)
if result:
logger.info("Barcode %s found in fallback database: %s", barcode, db_url)
return result
for db_url in self._FALLBACK_DATABASES:
result = await self._lookup_in_database(barcode, db_url, client)
if result:
logger.info("Barcode %s found in fallback database: %s", barcode, db_url)
return result
logger.info("Barcode %s not found in any Open*Facts database", barcode)
return None

View file

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

View file

@ -585,7 +585,8 @@ export interface BuildRequest {
export const recipesAPI = {
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
},
async getRecipe(id: number): Promise<RecipeSuggestion> {