diff --git a/api/index.php b/api/index.php index 4ee834e..12ef0bd 100644 --- a/api/index.php +++ b/api/index.php @@ -1350,6 +1350,7 @@ function generateRecipe(PDO $db): void { $appliances = $input['appliances'] ?? []; $dietaryRestrictions = $input['dietary_restrictions'] ?? ''; $todayRecipes = $input['today_recipes'] ?? []; + $mealPlanType = $input['meal_plan_type'] ?? ''; // e.g. 'pasta', 'pesce', 'legumi', ... // Fetch all inventory items with expiry info $stmt = $db->query(" @@ -1503,6 +1504,79 @@ function generateRecipe(PDO $db): void { $dietaryText = "\n\nRESTRIZIONI ALIMENTARI:\n{$dietaryRestrictions}\nRispetta SEMPRE queste restrizioni."; } + // Weekly meal plan type hint + $mealPlanTypeLabels = [ + 'pasta' => 'Pasta (primo piatto a base di pasta)', + 'riso' => 'Riso (risotto, insalata di riso, riso saltato, ecc.)', + 'carne' => 'Carne (secondo piatto a base di carne)', + 'pesce' => 'Pesce (secondo piatto a base di pesce o frutti di mare)', + 'legumi' => 'Legumi (zuppa, insalata, hummus, pasta e fagioli, ecc.)', + 'uova' => 'Uova (frittata, uova strapazzate, quiche, ecc.)', + 'formaggio' => 'Formaggio (fonduta, gnocchi al formaggio, torta salata, ecc.)', + 'pizza' => 'Pizza o focaccia (impastata in casa o usi ingredienti simili)', + 'affettati' => 'Affettati (tagliere misto, piadina, panino, ecc.)', + 'verdure' => 'Verdure (piatto principale a base di verdure, contorno abbondante)', + 'zuppa' => 'Zuppa o minestra (zuppe, vellutate, minestrone)', + 'insalata' => 'Insalata (insalata mista, insalata di riso o pasta, poke)', + 'pane' => 'Pane / Sandwich (toast, tramezzino, bruschette)', + 'dolce' => 'Dolce o dessert', + 'libero' => '', + ]; + + // Keywords to match inventory names against each meal plan type + $typeKeywords = [ + 'pesce' => ['tonno', 'salmone', 'merluzzo', 'branzino', 'orata', 'sardine', 'acciughe', 'alici', 'gamberi', 'cozze', 'vongole', 'polpo', 'calamari', 'seppia', 'sgombro', 'trota', 'baccalà', 'dentice', 'spigola', 'pesce'], + 'carne' => ['pollo', 'manzo', 'maiale', 'vitello', 'agnello', 'tacchino', 'salsiccia', 'hamburger', 'bistecca', 'cotoletta', 'pancetta', 'speck', 'carne', 'arrosto', 'filetto', 'lonza', 'braciola'], + 'pasta' => ['pasta', 'spaghetti', 'penne', 'rigatoni', 'fusilli', 'tagliatelle', 'lasagne', 'farfalle', 'orecchiette', 'bucatini', 'linguine', 'maccheroni', 'gnocchi', 'pennette', 'bavette'], + 'riso' => ['riso', 'basmati', 'arborio', 'carnaroli', 'parboiled', 'riso integrale'], + 'legumi' => ['fagioli', 'ceci', 'lenticchie', 'piselli', 'fave', 'lupini', 'soia', 'legumi', 'borlotti', 'cannellini', 'azuki'], + 'uova' => ['uova', 'uovo'], + 'formaggio' => ['formaggio', 'parmigiano', 'mozzarella', 'ricotta', 'pecorino', 'grana', 'gorgonzola', 'scamorza', 'fontina', 'emmental', 'asiago', 'provola', 'provolone', 'taleggio', 'stracchino'], + 'pizza' => ['farina', 'lievito', 'pizza', 'focaccia'], + 'affettati' => ['prosciutto', 'salame', 'bresaola', 'mortadella', 'speck', 'coppa', 'affettati', 'wurstel', 'würstel', 'piadina', 'pancetta cotta'], + 'verdure' => ['zucchine', 'zucchina', 'melanzane', 'peperoni', 'spinaci', 'cavolfiore', 'broccoli', 'carote', 'zucca', 'bietole', 'cavolo', 'carciofi', 'asparagi', 'lattuga', 'rucola', 'radicchio', 'cicoria', 'finocchio', 'cipolla', 'porri', 'verdure'], + 'zuppa' => ['brodo', 'zuppa', 'minestra', 'minestrone', 'vellutata', 'orzo', 'farro', 'fagioli', 'ceci', 'lenticchie'], + 'insalata' => ['insalata', 'lattuga', 'rucola', 'spinaci', 'radicchio', 'misticanza', 'valeriana', 'songino'], + 'pane' => ['pane', 'pancarrè', 'baguette', 'toast', 'tramezzino', 'crackers', 'grissini', 'ciabatta', 'rosetta'], + 'dolce' => ['cioccolato', 'cacao', 'zucchero', 'miele', 'marmellata', 'nutella', 'creme caramel', 'savoiardi', 'biscotti', 'pan di spagna', 'panna'], + ]; + + $mealPlanText = ''; + $mealPlanRule = ''; + if (!empty($mealPlanType) && isset($mealPlanTypeLabels[$mealPlanType]) && $mealPlanTypeLabels[$mealPlanType] !== '') { + $hint = $mealPlanTypeLabels[$mealPlanType]; + + // Scan inventory for ingredients matching this meal plan type + $matchingItems = []; + if (isset($typeKeywords[$mealPlanType])) { + foreach ($items as $item) { + $nameLower = mb_strtolower($item['name'] . ' ' . ($item['brand'] ?? '')); + foreach ($typeKeywords[$mealPlanType] as $kw) { + if (mb_strpos($nameLower, $kw) !== false) { + $entry = "→ {$item['name']}" . ($item['brand'] ? " ({$item['brand']})" : '') . ": {$item['quantity']} {$item['unit']}"; + if (!empty($item['expiry_date'])) { + $dl = intval($item['days_left']); + $entry .= $dl < 0 ? " [SCADUTO]" : " [scade tra $dl giorni]"; + } + $matchingItems[] = $entry; + break; + } + } + } + $matchingItems = array_unique($matchingItems); + } + + if (!empty($matchingItems)) { + $matchingList = implode("\n", $matchingItems); + $matchingBlock = "Ingredienti disponibili in dispensa compatibili con questa tipologia (usa almeno uno di questi come BASE della ricetta):\n{$matchingList}"; + } else { + $matchingBlock = "Nessun ingrediente perfettamente corrispondente trovato — usa la cosa più affine disponibile e segnalalo in nutrition_note."; + } + + $mealPlanText = "\n\n🎯 TIPOLOGIA PASTO PIANIFICATA — OBBLIGATORIA:\nOggi questo pasto DEVE essere: {$hint}\nQuesta è una regola del piano alimentare personale dell'utente, NON un suggerimento.\n{$matchingBlock}"; + $mealPlanRule = "0. TIPOLOGIA PASTO OBBLIGATORIA: la ricetta DEVE rispettare il tipo pianificato ({$hint}). Usa gli ingredienti compatibili evidenziati sopra come base principale del piatto. Non ignorare questa regola.\n "; + } + // Today's previous recipes from DB - avoid repetition $todayText = ''; $today = date('Y-m-d'); @@ -1542,10 +1616,10 @@ function generateRecipe(PDO $db): void { $prompt = << specification $listData = bringRequest('GET', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}"); if ($listData && isset($listData['purchase'])) { foreach ($listData['purchase'] as $existingItem) { - $existingNames[] = strtolower($existingItem['name'] ?? ''); + $existingItems[strtolower($existingItem['name'] ?? '')] = $existingItem['specification'] ?? ''; } } @@ -2183,14 +2258,26 @@ function bringAddItems(): void { // Map Italian name to Bring! catalog key (German) for proper recognition $bringName = italianToBring($name); + $bringKey = strtolower($bringName); + $spec = $item['specification'] ?? ''; + $update_spec = $item['update_spec'] ?? false; // explicit flag to force spec update - // Skip if already on the list - if (in_array(strtolower($bringName), $existingNames)) { - $skipped++; + if (array_key_exists($bringKey, $existingItems)) { + // Item already on the list — only update if specification changed and update_spec requested + if ($update_spec && $existingItems[$bringKey] !== $spec) { + $body = http_build_query([ + 'uuid' => $listUUID, + 'purchase' => $bringName, + 'specification' => $spec, + ]); + $result = bringRequest('PUT', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}", $body); + if ($result !== null) $updated++; + } else { + $skipped++; + } continue; } - $spec = $item['specification'] ?? ''; $body = http_build_query([ 'uuid' => $listUUID, 'purchase' => $bringName, @@ -2205,7 +2292,11 @@ function bringAddItems(): void { } } - echo json_encode(['success' => true, 'added' => $added, 'skipped' => $skipped, 'errors' => $errors]); + if ($added > 0 || $updated > 0) { + // Invalidate cache so next smart_shopping request reflects the updated Bring! list + @unlink(__DIR__ . '/../data/smart_shopping_cache.json'); + } + echo json_encode(['success' => true, 'added' => $added, 'updated' => $updated, 'skipped' => $skipped, 'errors' => $errors]); } function bringRemoveItem(): void { @@ -2234,6 +2325,10 @@ function bringRemoveItem(): void { ]); $result = bringRequest('PUT', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}", $body); + if ($result !== null) { + // Invalidate cache so next smart_shopping request reflects the updated Bring! list + @unlink(__DIR__ . '/../data/smart_shopping_cache.json'); + } echo json_encode(['success' => $result !== null]); } @@ -2305,6 +2400,43 @@ function smartShoppingCached(PDO $db): void { * Smart Shopping List: analyzes usage frequency, stock levels, expiry to produce * intelligent urgency-ranked shopping recommendations. */ + +/** + * Token-based fuzzy match: returns true if the product name shares at least one + * significant word (> 2 chars, not a stopword) with any key in $bringItems. + * Mirrors the JS _findSimilarItem / _nameTokens logic. + */ +/** + * Strict matching: returns true only when a Bring item's name "covers" the product name, + * i.e. the FIRST significant token of the product matches the FIRST significant token of + * a Bring item name. This prevents false positives like "Früchte/Frutta" matching the + * product "Muesli Frutta Secca" (which has "frutta" as a secondary token, not the first). + * Mirrors JS _matchBringToSmart / _syncOnBringFlags logic. + */ +function _productOnBring(string $productName, array $bringItems): bool { + // Exact key match (both German raw and Italian translated keys are stored) + if (isset($bringItems[mb_strtolower($productName)])) return true; + static $stop = ['di','del','della','dei','degli','dalle','delle','da','in','con','per','su', + 'a','e','il','lo','la','i','gli','le','un','uno','una','al','alle','agli','allo']; + $tokenize = function(string $s) use ($stop): array { + $clean = mb_strtolower(preg_replace('/[^\p{L}\s]/u', ' ', $s)); + return array_values(array_filter( + preg_split('/\s+/', trim($clean)), + fn($t) => mb_strlen($t) > 2 && !in_array($t, $stop) + )); + }; + $pTokens = $tokenize($productName); + if (empty($pTokens)) return false; + $pFirst = $pTokens[0]; + foreach (array_keys($bringItems) as $bKey) { + $bTokens = $tokenize($bKey); + if (empty($bTokens)) continue; + // First token of product must equal first token of Bring item + if ($bTokens[0] === $pFirst) return true; + } + return false; +} + function smartShopping(PDO $db): void { $now = time(); $today = date('Y-m-d'); @@ -2513,8 +2645,8 @@ function smartShopping(PDO $db): void { if ($useCount >= 8) $score += 15; elseif ($useCount >= 5) $score += 10; - // Is already on Bring? - $onBring = isset($bringItems[mb_strtolower($p['name'])]); + // Is already on Bring? (fuzzy token match — mirrors JS _findSimilarItem logic) + $onBring = _productOnBring($p['name'], $bringItems); $items[] = [ 'product_id' => $pid, diff --git a/assets/css/style.css b/assets/css/style.css index 80a62fc..38011b6 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -2970,6 +2970,155 @@ body { font-size: 1rem; } +/* ===== WEEKLY MEAL PLAN ===== */ +.mplan-grid { + display: flex; + flex-direction: column; + gap: 4px; +} +.mplan-header { + display: flex; + align-items: center; + gap: 8px; + padding: 0 4px 2px; + padding-left: 44px; +} +.mplan-col-header { + flex: 1; + text-align: center; + font-size: 0.75rem; + font-weight: 600; + color: var(--text-muted); +} +.mplan-row { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 4px; + border-radius: var(--radius-sm); + background: var(--bg); +} +.mplan-row-today { + background: rgba(45,80,22,0.08); + outline: 2px solid var(--primary-light); + border-radius: var(--radius-sm); +} +.mplan-day-name { + width: 36px; + font-size: 0.78rem; + font-weight: 700; + color: var(--text-muted); + flex-shrink: 0; + text-align: center; +} +.mplan-badge { + flex: 1; + min-width: 0; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 3px; + background: var(--primary); + color: #fff; + border-radius: 20px; + padding: 7px 6px; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: background 0.15s, transform 0.1s; + text-align: center; + line-height: 1.2; +} +.mplan-badge:active { transform: scale(0.93); } +.mplan-badge-pranzo { background: var(--primary-light); } +.mplan-badge-cena { background: #1e3a6e; } + +/* Meal plan picker popup */ +.mplan-picker { + position: fixed; + z-index: 600; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-lg); + padding: 12px; + display: flex; + flex-wrap: wrap; + gap: 8px; + left: 50% !important; + transform: translateX(-50%); + width: min(320px, 92vw); +} +.mplan-pick-btn { + background: var(--bg); + border: 1.5px solid var(--border); + border-radius: 20px; + padding: 7px 12px; + font-size: 0.85rem; + cursor: pointer; + transition: background 0.15s; +} +.mplan-pick-btn.active { + background: var(--primary); + color: #fff; + border-color: var(--primary); +} +.mplan-pick-btn:active { opacity: 0.7; } + +/* Legend in settings */ +.mplan-legend { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 8px; +} + +/* Recipe dialog banner (top strip) */ +.recipe-mealplan-banner { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + background: var(--primary); + color: #fff; + padding: 11px 20px; + font-size: 1rem; + font-weight: 700; + text-align: center; + border-radius: var(--radius) var(--radius) 0 0; + margin: -20px -20px 16px; + letter-spacing: 0.2px; +} + +/* Recipe dialog hint */ +.recipe-mealplan-hint { + display: flex; + align-items: center; + gap: 8px; + background: rgba(45,80,22,0.07); + border: 1px solid rgba(45,80,22,0.18); + border-radius: var(--radius-sm); + padding: 8px 12px; + margin: 4px 0 12px; + font-size: 0.88rem; +} +.mplan-hint-badge { + background: var(--primary); + color: #fff; + border-radius: 20px; + padding: 3px 10px; + font-size: 0.82rem; + font-weight: 700; + white-space: nowrap; +} +.mplan-hint-label { + color: var(--text-muted); + font-size: 0.80rem; +} + /* ===== COOKING MODE ===== */ .cooking-overlay { position: fixed; @@ -3191,6 +3340,29 @@ body { to { opacity: 0.5; transform: scale(1.06); } } +/* ===== COOKING SCREEN FLASH ===== */ +.cooking-flash-overlay { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 50; + background: transparent; +} +.cooking-flash-overlay.flash-warning { + animation: cookFlashOrange 1.1s ease-in-out infinite; +} +.cooking-flash-overlay.flash-done { + animation: cookFlashRed 0.65s ease-in-out infinite; +} +@keyframes cookFlashOrange { + 0%, 100% { background: transparent; } + 50% { background: rgba(249, 115, 22, 0.28); } +} +@keyframes cookFlashRed { + 0%, 100% { background: transparent; } + 50% { background: rgba(220, 38, 38, 0.42); } +} + .ctimer-btns { display: flex; gap: 4px; @@ -3664,6 +3836,23 @@ body { border-color: var(--primary); } +/* Meal-plan chip: visually highlighted to stand out as the planned food type */ +.recipe-opt-mealplan-chip { + grid-column: 1 / -1; + background: rgba(100, 60, 20, 0.07); + border-color: #b07830; + font-weight: 600; +} +.recipe-opt-mealplan-chip:has(input:checked) { + background: rgba(120, 70, 10, 0.13); + border-color: #c08020; +} +.recipe-opt-mealplan-chip:has(input:not(:checked)) { + opacity: 0.6; + text-decoration: line-through; + text-decoration-color: #c08020; +} + .recipe-option-chip input[type="checkbox"] { width: 16px; height: 16px; @@ -4628,6 +4817,24 @@ body { .screensaver-fact.visible { opacity: 1; } +.screensaver-mealplan { + text-align: center; + user-select: none; + margin: -8px 0 4px; +} +.screensaver-mealplan-badge { + display: inline-block; + background: rgba(255,255,255,0.10); + color: rgba(255,255,255,0.70); + border: 1px solid rgba(255,255,255,0.18); + border-radius: 28px; + padding: 8px 24px; + font-size: 1.35rem; + font-weight: 400; + letter-spacing: 0.4px; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} .screensaver-shortcuts { position: absolute; bottom: max(32px, env(safe-area-inset-bottom, 32px)); diff --git a/assets/js/app.js b/assets/js/app.js index f0e28b9..bea12a5 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -633,6 +633,21 @@ async function loadSettingsUI() { loadCameraDevices(); renderAppliances(s.appliances || []); loadSpesaSettings(); + const mealPlanEnabled = s.meal_plan_enabled !== false; + const mpEnabledEl = document.getElementById('setting-meal-plan-enabled'); + if (mpEnabledEl) mpEnabledEl.checked = mealPlanEnabled; + const mpConfigSection = document.getElementById('meal-plan-config-section'); + if (mpConfigSection) mpConfigSection.style.display = mealPlanEnabled ? '' : 'none'; + const mpLegendCard = document.getElementById('meal-plan-legend-card'); + if (mpLegendCard) mpLegendCard.style.display = mealPlanEnabled ? '' : 'none'; + renderMealPlanEditor(); + // Render legend + const legend = document.querySelector('.mplan-legend'); + if (legend) { + legend.innerHTML = MEAL_PLAN_TYPES.map(t => + `${t.icon} ${t.label}` + ).join(''); + } // Load server-side settings if not already set locally try { @@ -730,6 +745,9 @@ async function saveSettings() { s.dietary = document.getElementById('setting-dietary').value.trim(); // Camera s.camera_facing = document.getElementById('setting-camera-facing').value; + // Meal plan enabled toggle + const mpEnabledEl = document.getElementById('setting-meal-plan-enabled'); + if (mpEnabledEl) s.meal_plan_enabled = mpEnabledEl.checked; // Save spesa AI prompt if the field exists const spesaPromptEl = document.getElementById('setting-spesa-ai-prompt'); if (spesaPromptEl) s.spesa_ai_prompt = spesaPromptEl.value.trim(); @@ -3661,6 +3679,7 @@ function _nameTokens(name) { * Check whether `name` matches any item in `list` (array of {name}). * Returns the matching item or null. * A match = at least one significant token in common. + * NOTE: intentionally loose — use _matchBringToSmart for display/urgency matching. */ function _findSimilarItem(name, list) { const tokens = _nameTokens(name); @@ -3671,6 +3690,40 @@ function _findSimilarItem(name, list) { }) || null; } +/** + * Strict matching: find the smart item that corresponds to a Bring item by name. + * Rules (in order): + * 1. Exact case-insensitive match. + * 2. First significant token of both names must be identical + * ("Latte" → "Latte Parzialmente Scremato" ✓; "Frutta" ≠ "Muesli Frutta Secca" ✗). + * 3. For multi-token Bring names: all Bring tokens appear in the smart item tokens. + * This avoids false positives when a generic word ("frutta", "noci") appears as a + * secondary word inside an unrelated long product name. + */ +function _matchBringToSmart(bringName, smartItems) { + const bLower = bringName.toLowerCase(); + const exact = smartItems.find(sd => sd.name.toLowerCase() === bLower); + if (exact) return exact; + const bTokens = _nameTokens(bringName); + if (bTokens.length === 0) return null; + const bFirst = bTokens[0]; + // Rule 2: first token match + const firstMatch = smartItems.find(sd => { + const sdTokens = _nameTokens(sd.name); + return sdTokens.length > 0 && sdTokens[0] === bFirst; + }); + if (firstMatch) return firstMatch; + // Rule 3: multi-token full subset + if (bTokens.length >= 2) { + const allMatch = smartItems.find(sd => { + const sdTokens = _nameTokens(sd.name); + return bTokens.every(t => sdTokens.includes(t)); + }); + if (allMatch) return allMatch; + } + return null; +} + function showLowStockBringPrompt(result, afterCallback) { const name = result.product_name || currentProduct?.name || ''; const unit = result.product_unit || currentProduct?.unit || 'pz'; @@ -4288,6 +4341,19 @@ function toggleShoppingTag(itemIdx, tag) { if (existing.length) tags[key] = existing; else delete tags[key]; localStorage.setItem('shopping_tags', JSON.stringify(tags)); + + // Sync urgente/presto tag to Bring specification so it's visible in the Bring app + if (tag === 'urgente' && shoppingListUUID) { + const isNowUrgent = existing.includes('urgente'); + const newSpec = isNowUrgent ? '⚡ Urgente' : ''; + api('bring_add', {}, 'POST', { + items: [{ name: item.name, specification: newSpec, update_spec: true }], + listUUID: shoppingListUUID, + }).catch(() => {}); + // Update local item spec for immediate re-render + item.specification = newSpec; + } + renderShoppingItems(); } catch (e) { console.error('toggleShoppingTag', e); } } @@ -4320,12 +4386,24 @@ async function confirmShoppingItemFound() { } // ===== AUTO-ADD CRITICAL ITEMS TO BRING! ===== + +/** Build a Bring specification string that encodes urgency + optional brand. */ +function _urgencyToSpec(urgency, brand) { + const urgencyLabels = { critical: '⚡ Urgente', high: '🟠 Presto', medium: '', low: '' }; + const urgLabel = urgencyLabels[urgency] || ''; + if (urgLabel && brand) return `${urgLabel} · ${brand}`; + if (urgLabel) return urgLabel; + return brand || ''; +} + async function autoAddCriticalItems() { - if (sessionStorage.getItem('_autoAddedCritical')) return; - sessionStorage.setItem('_autoAddedCritical', '1'); + // Time-based guard: run at most once every 10 minutes (not session-based, so new critical items get added promptly) + const lastRun = parseInt(localStorage.getItem('_autoAddedCriticalTs') || '0'); + if (Date.now() - lastRun < 10 * 60 * 1000) return; + localStorage.setItem('_autoAddedCriticalTs', String(Date.now())); const critical = smartShoppingItems.filter(i => i.urgency === 'critical' && !i.on_bring); if (critical.length === 0) return; - const itemsToAdd = critical.map(i => ({ name: i.name, specification: i.brand || '' })); + const itemsToAdd = critical.map(i => ({ name: i.name, specification: _urgencyToSpec(i.urgency, i.brand) })); try { const result = await api('bring_add', {}, 'POST', { items: itemsToAdd, listUUID: shoppingListUUID }); if (result.success && result.added > 0) { @@ -4353,7 +4431,7 @@ async function cleanupObsoleteBringItems() { // Load all products from our DB to cross-reference let allProducts = []; try { - const res = await api('products'); + const res = await api('products_list'); allProducts = res.products || res.data || []; } catch (e) { return; } if (!allProducts.length) return; @@ -4529,6 +4607,28 @@ function _updateSmartUrgencyBadge() { } } +/** + * Sync the on_bring flag for every smartShoppingItem against the current shoppingItems list. + * The server cache can be up to 10 min old so on_bring may be stale — this corrects it + * client-side using strict first-token matching: a Bring item matches a smart item only when + * the first significant token of the Bring item's name equals the first significant token of + * the smart item's name (or exact name match). This avoids false positives like + * "Frutta" (fresh fruit on Bring) matching "Muesli Frutta Secca" (a different product). + */ +function _syncOnBringFlags() { + for (const si of smartShoppingItems) { + const siLower = si.name.toLowerCase(); + const siFirst = _nameTokens(si.name)[0]; + si.on_bring = !!( + shoppingItems.find(bi => bi.name.toLowerCase() === siLower) || + (siFirst && shoppingItems.find(bi => { + const biFirst = _nameTokens(bi.name)[0]; + return biFirst === siFirst; + })) + ); + } +} + function _renderSmartLastUpdate() { const el = document.getElementById('smart-last-update'); if (!el || !_smartShoppingLastFetch) return; @@ -4704,7 +4804,7 @@ async function addSmartToBring() { if (item) { itemsToAdd.push({ name: item.name, - specification: item.brand || '', + specification: _urgencyToSpec(item.urgency, item.brand), }); } }); @@ -4759,6 +4859,67 @@ async function loadShoppingCount() { } } +/** + * Sync local 'urgente' tag from Bring specification. + * If a Bring item's specification contains 'urgente', ensure the local tag is set. + * If a Bring item's specification is empty/cleared, remove the local urgente tag + * UNLESS smart shopping considers it critical (to avoid losing urgency on stale specs). + */ +function _syncTagsFromBringSpec() { + try { + const tags = JSON.parse(localStorage.getItem('shopping_tags') || '{}'); + let changed = false; + for (const item of shoppingItems) { + const key = item.name.toLowerCase(); + const spec = (item.specification || '').toLowerCase(); + const existing = tags[key] || []; + const hasUrgente = existing.includes('urgente'); + const smartMatch = _matchBringToSmart(item.name, smartShoppingItems); + const smartIsCritical = smartMatch && (smartMatch.urgency === 'critical' || smartMatch.urgency === 'high'); + if ((spec.includes('urgente') || spec.includes('presto') || smartIsCritical) && !hasUrgente) { + existing.push('urgente'); + tags[key] = existing; + changed = true; + } else if (!spec.includes('urgente') && !spec.includes('presto') && !smartIsCritical && hasUrgente) { + existing.splice(existing.indexOf('urgente'), 1); + if (existing.length) tags[key] = existing; + else delete tags[key]; + changed = true; + } + } + if (changed) localStorage.setItem('shopping_tags', JSON.stringify(tags)); + } catch (e) { /* ignore */ } +} + +/** + * After smart shopping loads, push urgency specifications to Bring for all matched items. + * This makes urgency visible in the native Bring app via the item specification field. + * Only updates if the spec has changed (to avoid unnecessary API calls). + */ +async function autoSyncUrgencySpecs() { + if (!shoppingListUUID || !smartShoppingItems.length) return; + const toUpdate = []; + for (const item of shoppingItems) { + const smartMatch = _matchBringToSmart(item.name, smartShoppingItems); + if (!smartMatch) continue; + const expectedSpec = _urgencyToSpec(smartMatch.urgency, ''); + const currentSpec = (item.specification || '').toLowerCase(); + // Only update if urgency marker changed (don't clobber user-set spec info that isn't urgency) + const currentHasUrgencyMarker = currentSpec.includes('urgente') || currentSpec.includes('presto'); + const needsUpdate = expectedSpec && !currentHasUrgencyMarker; + const needsClear = !expectedSpec && currentHasUrgencyMarker; + if (needsUpdate || needsClear) { + toUpdate.push({ name: item.name, specification: expectedSpec, update_spec: true }); + // Optimistically update local item so re-render is immediate + item.specification = expectedSpec; + } + } + if (toUpdate.length === 0) return; + try { + await api('bring_add', {}, 'POST', { items: toUpdate, listUUID: shoppingListUUID }); + } catch (e) { /* ignore - sync is best-effort */ } +} + async function loadShoppingList() { const statusEl = document.getElementById('bring-status'); const currentEl = document.getElementById('shopping-current'); @@ -4794,19 +4955,23 @@ async function loadShoppingList() { if (pricesChanged) saveShoppingPrices(); loadShoppingPrices(); + // Sync urgente local tags from Bring specification (items marked urgent by us or manually) + _syncTagsFromBringSpec(); renderShoppingItems(); currentEl.style.display = 'block'; // Load smart shopping predictions, then re-render to show badges + auto-add critical loadSmartShopping().then(() => { + _syncOnBringFlags(); // sync on_bring against current Bring list before any logic reads it + _syncTagsFromBringSpec(); // re-sync tags now that smart data is available + autoSyncUrgencySpecs(); // push urgency specs to Bring for matched items + renderSmartShopping(); // re-render smart tab with corrected on_bring flags + updateShoppingTabCounts(); // update tab badges with corrected counts autoAddCriticalItems(); cleanupObsoleteBringItems(); - renderShoppingItems(); // re-render to apply urgency badges from smart data + renderShoppingItems(); // re-render shopping tab with urgency badges }); - // Show tabs once data is ready - updateShoppingTabCounts(); - } catch (err) { console.error('Bring! error:', err); statusEl.style.display = 'block'; @@ -4814,6 +4979,24 @@ async function loadShoppingList() { } } +/** Return the spec text to show in the UI, stripping urgency markers (those are shown as badges). */ +function _specDisplayText(spec) { + if (!spec) return ''; + // Strip known urgency prefixes set by _urgencyToSpec (case-insensitive, then trim separator) + const lower = spec.toLowerCase(); + for (const prefix of ['⚡ urgente', '🟠 presto']) { + if (lower.startsWith(prefix)) { + return spec.slice(prefix.length).replace(/^\s*[·\-]\s*/, '').trim(); + } + } + return spec; +} + +/** Return the spec for price search, stripping urgency markers that would confuse the AI. */ +function _cleanSpecForSearch(spec) { + return _specDisplayText(spec); +} + async function renderShoppingItems() { const container = document.getElementById('shopping-items'); const countEl = document.getElementById('shopping-count'); @@ -4855,10 +5038,17 @@ async function renderShoppingItems() { low: { icon: '🟢', label: 'Ok', cls: 'badge-low' }, }; - // Map each item to its section + urgency + // Map each item to its section + urgency (strict first-token matching to avoid false positives) + // Also derive urgency from Bring specification if smart matching fails const enriched = shoppingItems.map((item, idx) => { - const smartData = smartShoppingItems.find(sd => sd.name.toLowerCase() === item.name.toLowerCase()); - const urgency = smartData?.urgency || null; + const smartData = _matchBringToSmart(item.name, smartShoppingItems); + let urgency = smartData?.urgency || null; + // Fallback: read urgency from Bring specification (set by our app when adding) + if (!urgency && item.specification) { + const spec = item.specification.toLowerCase(); + if (spec.includes('urgente')) urgency = 'critical'; + else if (spec.includes('presto')) urgency = 'high'; + } const sec = getItemSection(item.name); return { item, idx, smartData, urgency, sec }; }); @@ -4965,7 +5155,7 @@ async function renderShoppingItems() { ${escapeHtml(item.name)} 📷 - ${item.specification ? `
${escapeHtml(item.specification)}
` : ''} + ${_specDisplayText(item.specification) ? `
${escapeHtml(_specDisplayText(item.specification))}
` : ''} ${(urgencyBadge || freqBadge || localTagHtml) ? `
${urgencyBadge}${freqBadge}${localTagHtml}
` : ''} ${detailHtml} @@ -5052,9 +5242,9 @@ async function searchItemPrice(idx, force = false) { renderShoppingItems(); try { - // Send item name as query, spec separately for AI selection + // Send item name as query, spec separately for AI selection (strip urgency markers) const searchQ = item.name; - const spec = item.specification || ''; + const spec = _cleanSpecForSearch(item.specification || ''); const s2 = getSettings(); const aiPrompt = s2.spesa_ai_prompt || ''; @@ -5064,7 +5254,7 @@ async function searchItemPrice(idx, force = false) { prompt: aiPrompt }); if (res.success && res.product) { - shoppingPrices[priceKey] = { searched: true, product: res.product, spec: item.specification || '' }; + shoppingPrices[priceKey] = { searched: true, product: res.product, spec: _cleanSpecForSearch(item.specification || '') }; } else { shoppingPrices[priceKey] = { searched: true, product: null }; } @@ -5120,11 +5310,11 @@ async function searchAllPrices() { const aiPrompt = s.spesa_ai_prompt || ''; const res = await api(`${provider}_search`, { q: item.name, - spec: item.specification || '', + spec: _cleanSpecForSearch(item.specification || ''), prompt: aiPrompt }); if (res.success && res.product) { - shoppingPrices[priceKey] = { searched: true, product: res.product, spec: item.specification || '' }; + shoppingPrices[priceKey] = { searched: true, product: res.product, spec: _cleanSpecForSearch(item.specification || '') }; } else { shoppingPrices[priceKey] = { searched: true, product: null }; } @@ -5564,6 +5754,161 @@ async function loadLog(more = false) { } } +// ===== WEEKLY MEAL PLAN ===== + +/** + * All selectable meal categories per slot. + * id must be URL-safe; icon + label shown in UI. + */ +const MEAL_PLAN_TYPES = [ + { id: 'pasta', icon: '🍝', label: 'Pasta' }, + { id: 'riso', icon: '🍚', label: 'Riso' }, + { id: 'carne', icon: '🥩', label: 'Carne' }, + { id: 'pesce', icon: '🐟', label: 'Pesce' }, + { id: 'legumi', icon: '🫘', label: 'Legumi' }, + { id: 'uova', icon: '🥚', label: 'Uova' }, + { id: 'formaggio', icon: '🧀', label: 'Formaggio' }, + { id: 'pizza', icon: '🍕', label: 'Pizza' }, + { id: 'affettati', icon: '🥓', label: 'Affettati' }, + { id: 'verdure', icon: '🥦', label: 'Verdure' }, + { id: 'zuppa', icon: '🍲', label: 'Zuppa' }, + { id: 'insalata', icon: '🥗', label: 'Insalata' }, + { id: 'pane', icon: '🥪', label: 'Pane/Sandwich' }, + { id: 'dolce', icon: '🍰', label: 'Dolce' }, + { id: 'libero', icon: '🎲', label: 'Libero' }, +]; + +const MEAL_PLAN_TYPE_MAP = {}; +MEAL_PLAN_TYPES.forEach(t => { MEAL_PLAN_TYPE_MAP[t.id] = t; }); + +const WEEK_DAYS = ['Lunedì','Martedì','Mercoledì','Giovedì','Venerdì','Sabato','Domenica']; +const WEEK_DAYS_SHORT = ['Lun','Mar','Mer','Gio','Ven','Sab','Dom']; + +/** Default weekly plan as requested. */ +const DEFAULT_MEAL_PLAN = { + 1: { pranzo: 'pasta', cena: 'pesce' }, + 2: { pranzo: 'riso', cena: 'carne' }, + 3: { pranzo: 'legumi', cena: 'uova' }, + 4: { pranzo: 'pasta', cena: 'pesce' }, + 5: { pranzo: 'riso', cena: 'formaggio' }, + 6: { pranzo: 'legumi', cena: 'pizza' }, + 0: { pranzo: 'carne', cena: 'affettati' }, // 0 = Sunday (getDay()) +}; + +function getMealPlan() { + const s = getSettings(); + return s.meal_plan || DEFAULT_MEAL_PLAN; +} + +/** Return today's planned meal type for a given slot ('pranzo'|'cena'), or null. */ +function getTodayMealPlanType(slot) { + const s = getSettings(); + if (s.meal_plan_enabled === false) return null; + const dow = new Date().getDay(); // 0=Sun,1=Mon,...,6=Sat + const plan = getMealPlan(); + return plan[dow]?.[slot] || null; +} + +/** Toggle handler for the enable/disable switch in settings. */ +function onMealPlanEnabledChange(el) { + const s = getSettings(); + s.meal_plan_enabled = el.checked; + saveSettingsToStorage(s); + const mpConfigSection = document.getElementById('meal-plan-config-section'); + if (mpConfigSection) mpConfigSection.style.display = el.checked ? '' : 'none'; + const mpLegendCard = document.getElementById('meal-plan-legend-card'); + if (mpLegendCard) mpLegendCard.style.display = el.checked ? '' : 'none'; + // Close picker if open + const picker = document.getElementById('meal-plan-picker'); + if (picker) picker.style.display = 'none'; +} + +/** + * Render the weekly meal plan editor into #meal-plan-grid. + * Each cell shows the current type badge + a picker dropdown. + */ +function renderMealPlanEditor() { + const container = document.getElementById('meal-plan-grid'); + if (!container) return; + const plan = getMealPlan(); + // JS getDay: 0=Sun … but we display Mon-Sun (1..6,0) + const dayOrder = [1,2,3,4,5,6,0]; + const today = new Date().getDay(); + + const header = `
+ 🌤️ Pranzo + 🌙 Cena +
`; + + const rows = dayOrder.map((dow, i) => { + const pranzo = plan[dow]?.pranzo || 'libero'; + const cena = plan[dow]?.cena || 'libero'; + const pt = MEAL_PLAN_TYPE_MAP[pranzo] || MEAL_PLAN_TYPE_MAP.libero; + const ct = MEAL_PLAN_TYPE_MAP[cena] || MEAL_PLAN_TYPE_MAP.libero; + const todayClass = dow === today ? ' mplan-row-today' : ''; + return `
+
${WEEK_DAYS_SHORT[i]}
+ ${pt.icon} ${pt.label} + ${ct.icon} ${ct.label} +
`; + }).join(''); + + container.innerHTML = header + rows; +} + +let _mplanPickerTarget = null; // {dow, slot, badgeEl} +function openMealPlanPicker(dow, slot, badgeEl) { + // Close any open picker first + closeMealPlanPicker(); + _mplanPickerTarget = { dow, slot, badgeEl }; + const picker = document.getElementById('meal-plan-picker'); + if (!picker) return; + const plan = getMealPlan(); + const current = plan[dow]?.[slot] || 'libero'; + picker.innerHTML = MEAL_PLAN_TYPES.map(t => + `` + ).join(''); + // Position vertically near the badge, centered horizontally (CSS handles centering) + const rect = badgeEl.getBoundingClientRect(); + const pickerEl = picker; + // Show first to measure height + pickerEl.style.display = 'flex'; + const pickerH = pickerEl.offsetHeight || 160; + const spaceBelow = window.innerHeight - rect.bottom - 8; + const top = spaceBelow >= pickerH + ? rect.bottom + 8 + : Math.max(8, rect.top - pickerH - 8); + pickerEl.style.top = top + 'px'; + // Close on outside tap + setTimeout(() => document.addEventListener('click', _mplanPickerOutside, { once: true }), 0); +} +function _mplanPickerOutside(e) { + const picker = document.getElementById('meal-plan-picker'); + if (picker && !picker.contains(e.target)) closeMealPlanPicker(); +} +function closeMealPlanPicker() { + const picker = document.getElementById('meal-plan-picker'); + if (picker) picker.style.display = 'none'; + _mplanPickerTarget = null; + document.removeEventListener('click', _mplanPickerOutside); +} +function selectMealPlanType(dow, slot, typeId) { + const s = getSettings(); + if (!s.meal_plan) s.meal_plan = JSON.parse(JSON.stringify(DEFAULT_MEAL_PLAN)); + if (!s.meal_plan[dow]) s.meal_plan[dow] = {}; + s.meal_plan[dow][slot] = typeId; + saveSettingsToStorage(s); + closeMealPlanPicker(); + renderMealPlanEditor(); +} +function resetMealPlan() { + const s = getSettings(); + s.meal_plan = JSON.parse(JSON.stringify(DEFAULT_MEAL_PLAN)); + saveSettingsToStorage(s); + renderMealPlanEditor(); + showToast('Piano settimanale ripristinato', 'success'); +} + // ===== RECIPE GENERATION ===== const MEAL_TYPES = [ { id: 'colazione', icon: '☀️', label: 'Colazione', from: 6, to: 11 }, @@ -5710,6 +6055,9 @@ function openRecipeDialog() { } updateRecipeMealTitle(); + // Show today's meal plan hint + _renderMealPlanHint(meal); + // Check for cached recipe matching current meal type if (_cachedRecipe && _cachedRecipe.meal === meal && _cachedRecipe.recipe) { document.getElementById('recipe-ask').style.display = 'none'; @@ -6377,6 +6725,7 @@ function removeCookingTimer(id) { if (t && t.interval) clearInterval(t.interval); _cookingTimers = _cookingTimers.filter(t => t.id !== id); renderTimersBar(); + _updateScreenFlash(); } function toggleCookingTimerById(id) { @@ -6431,6 +6780,25 @@ function _updateTimerCard(id) { } toggleBtn.textContent = t.running ? '⏸' : '▶'; toggleBtn.classList.toggle('running', t.running); + _updateScreenFlash(); +} + +/** Update the full-screen colour flash based on the worst active timer state. */ +function _updateScreenFlash() { + const flashEl = document.getElementById('cooking-flash-overlay'); + if (!flashEl) return; + let hasDone = false, hasWarning = false; + for (const t of _cookingTimers) { + if (t.seconds <= 0) { hasDone = true; break; } + if (t.seconds <= 30 && t.running) hasWarning = true; + } + if (hasDone) { + flashEl.className = 'cooking-flash-overlay flash-done'; + } else if (hasWarning) { + flashEl.className = 'cooking-flash-overlay flash-warning'; + } else { + flashEl.className = 'cooking-flash-overlay'; + } } function renderTimersBar() { @@ -6466,6 +6834,7 @@ function clearAllCookingTimers() { _cookingSuggestedLabel = ''; const bar = document.getElementById('cooking-timers-bar'); if (bar) { bar.style.display = 'none'; bar.innerHTML = ''; } + _updateScreenFlash(); } // ===== END COOKING TIMER SYSTEM ===== @@ -6511,6 +6880,48 @@ function cookingUseIngredient(idx, productId, location, qtyNumber, btn) { function updateRecipeMealTitle() { const meal = getSelectedMealType(); document.getElementById('recipe-meal-title').textContent = MEAL_LABELS[meal] || '🍳 Ricetta'; + _renderMealPlanHint(meal); +} + +/** Show/hide the meal-plan badge hint + top banner in the recipe dialog. */ +function _renderMealPlanHint(mealSlot) { + const el = document.getElementById('recipe-mealplan-hint'); + const banner = document.getElementById('recipe-mealplan-banner'); + const chipWrap = document.getElementById('recipe-opt-mealplan-wrap'); + const chipLabel = document.getElementById('recipe-opt-mealplan-label'); + const chipCb = document.getElementById('recipe-opt-mealplan'); + // mealSlot = 'pranzo' or 'cena' (from getMealType/getSelectedMealType) + const typeId = (mealSlot === 'pranzo' || mealSlot === 'cena') + ? getTodayMealPlanType(mealSlot) + : null; + if (!typeId || typeId === 'libero') { + if (el) el.style.display = 'none'; + if (banner) banner.style.display = 'none'; + if (chipWrap) chipWrap.style.display = 'none'; + return; + } + const t = MEAL_PLAN_TYPE_MAP[typeId]; + if (!t) { + if (el) el.style.display = 'none'; + if (banner) banner.style.display = 'none'; + if (chipWrap) chipWrap.style.display = 'none'; + return; + } + if (el) { + el.innerHTML = `${t.icon} ${t.label} suggerito dal piano settimanale`; + el.style.display = 'flex'; + } + if (banner) { + const slotLabel = mealSlot === 'pranzo' ? '🌤️ Pranzo' : '🌙 Cena'; + banner.innerHTML = `${slotLabel}·${t.icon} ${t.label}`; + banner.style.display = 'flex'; + } + // Show the meal-plan chip (active by default, user can uncheck to ignore the plan) + if (chipWrap) { + chipWrap.style.display = ''; + if (chipLabel) chipLabel.textContent = `${t.icon} ${t.label}`; + if (chipCb) chipCb.checked = true; + } } function regenerateRecipe() { @@ -6535,6 +6946,15 @@ async function generateRecipe() { const meal = getSelectedMealType(); const persons = parseInt(document.getElementById('recipe-persons').value) || 1; const settings = getSettings(); + + // Determine meal plan type for today's selected slot, + // but only if the user has NOT unchecked the meal-plan chip + const mealPlanChipWrap = document.getElementById('recipe-opt-mealplan-wrap'); + const mealPlanCb = document.getElementById('recipe-opt-mealplan'); + const mealPlanChipActive = !mealPlanChipWrap || mealPlanChipWrap.style.display === 'none' || (mealPlanCb && mealPlanCb.checked); + const mealPlanType = mealPlanChipActive && (meal === 'pranzo' || meal === 'cena') + ? (getTodayMealPlanType(meal) || null) + : null; // Gather active options from checkboxes const options = []; @@ -6562,7 +6982,8 @@ async function generateRecipe() { options, appliances: settings.appliances || [], dietary_restrictions: settings.dietary_restrictions || '', - today_recipes: await getTodayRecipeTitles() + today_recipes: await getTodayRecipeTitles(), + meal_plan_type: mealPlanType, }); if (!result.success) { @@ -6817,6 +7238,25 @@ function updateScreensaverClock() { const date = now.toLocaleDateString('it-IT', { weekday: 'long', day: 'numeric', month: 'long' }); const el = document.getElementById('screensaver-clock'); if (el) el.innerHTML = `${time}
${date}
`; + updateScreensaverMealPlan(); +} + +/** Show/hide the planned meal type badge on the screensaver based on current time slot. */ +function updateScreensaverMealPlan() { + const el = document.getElementById('screensaver-mealplan'); + if (!el) return; + const s = getSettings(); + if (s.meal_plan_enabled === false) { el.style.display = 'none'; return; } + const hour = new Date().getHours(); + // Before 15:00 show pranzo, from 15:00 onwards show cena + const slot = hour < 15 ? 'pranzo' : 'cena'; + const typeId = getTodayMealPlanType(slot); + if (!typeId || typeId === 'libero') { el.style.display = 'none'; return; } + const t = MEAL_PLAN_TYPE_MAP[typeId]; + if (!t) { el.style.display = 'none'; return; } + const slotLabel = slot === 'pranzo' ? '🌤️ Pranzo' : '🌙 Cena'; + el.innerHTML = `${slotLabel} · ${t.icon} ${t.label}`; + el.style.display = 'block'; } function dismissScreensaver(targetPage) { @@ -7315,14 +7755,107 @@ function initInactivityWatcher() { // ===== INITIALIZATION ===== document.addEventListener('DOMContentLoaded', () => { + // Migrate old session-based flags to time-based + if (sessionStorage.getItem('_autoAddedCritical')) { + sessionStorage.removeItem('_autoAddedCritical'); + } + // One-time reset of bg sync timestamp so first load always triggers a sync + if (!localStorage.getItem('_bgBringSyncReset_v1')) { + localStorage.removeItem('_bgBringSyncTs'); + localStorage.setItem('_bgBringSyncReset_v1', '1'); + } syncSettingsFromDB(); showPage('dashboard'); initInactivityWatcher(); initSpesaMode(); initScreensaverShortcuts(); startBgShoppingRefresh(); + + // Auto-refresh Bring list when user returns to the browser tab (e.g. was in the Bring app) + document.addEventListener('visibilitychange', () => { + if (!document.hidden && _currentPageId === 'shopping') { + loadShoppingList(); + } + }); + + // Silent background sync: update urgency specs on Bring and add missing critical items + // Runs once at startup (time-gated: max every 10 min) without affecting the UI + _backgroundBringSync(); }); +/** + * Background sync at startup: + * 1. Fetches Bring list + smart shopping in parallel + * 2. Adds any critical items missing from Bring + * 3. Updates urgency specs for items already on Bring that need it + * Fully silent — no toasts, no loading spinners. + */ +async function _backgroundBringSync() { + const lastRun = parseInt(localStorage.getItem('_bgBringSyncTs') || '0'); + if (Date.now() - lastRun < 10 * 60 * 1000) return; + localStorage.setItem('_bgBringSyncTs', String(Date.now())); + + try { + const [bringData, smartData] = await Promise.all([ + api('bring_list').catch(() => null), + api('smart_shopping').catch(() => null), + ]); + + if (!bringData?.success || !smartData?.success) return; + + const listUUID = bringData.listUUID; + const bringItems = bringData.purchase || []; + const smartItems = smartData.items || []; + + if (!listUUID || !smartItems.length) return; + + // Update local smart cache so other functions can use it + if (!smartShoppingItems.length) { + smartShoppingItems = smartItems; + _smartShoppingLastFetch = Date.now(); + } + if (!shoppingListUUID) shoppingListUUID = listUUID; + if (!shoppingItems.length) shoppingItems = bringItems; + + const toAdd = []; // new items not yet on Bring + const toUpdate = []; // items on Bring that need spec updated + + for (const si of smartItems) { + if (si.urgency === 'none' || si.urgency === 'low') continue; + const expectedSpec = _urgencyToSpec(si.urgency, ''); + const bringMatch = bringItems.find(bi => { + const biL = bi.name.toLowerCase(); + const siL = si.name.toLowerCase(); + if (biL === siL) return true; + const biFirst = _nameTokens(bi.name)[0]; + const siFirst = _nameTokens(si.name)[0]; + return biFirst && biFirst === siFirst; + }); + + if (!bringMatch) { + // Not on Bring — add if critical + if (si.urgency === 'critical') { + toAdd.push({ name: si.name, specification: expectedSpec }); + } + } else { + // On Bring — update spec if urgency marker is missing/wrong + const currentSpec = (bringMatch.specification || '').toLowerCase(); + const hasUrgencyMarker = currentSpec.includes('urgente') || currentSpec.includes('presto'); + if (!hasUrgencyMarker && expectedSpec) { + toUpdate.push({ name: si.name, specification: expectedSpec, update_spec: true }); + bringMatch.specification = expectedSpec; // update local copy + } + } + } + + const allChanges = [...toAdd, ...toUpdate]; + if (allChanges.length === 0) return; + + await api('bring_add', {}, 'POST', { items: allChanges, listUUID }); + logOperation('bg_bring_sync', { added: toAdd.map(i=>i.name), updated: toUpdate.map(i=>i.name) }); + } catch (e) { /* silent — best effort */ } +} + // ===== DUPLICLICK (SPESA ONLINE) ===== function selectSpesaProvider(btn, provider) { diff --git a/data/cron.log b/data/cron.log new file mode 100644 index 0000000..eaed3cc --- /dev/null +++ b/data/cron.log @@ -0,0 +1,968 @@ +[2026-04-01 05:55:02] OK — 15 items cached +[2026-04-01 06:00:02] OK — 15 items cached +[2026-04-01 06:05:02] OK — 15 items cached +[2026-04-01 06:10:01] OK — 15 items cached +[2026-04-01 06:15:03] OK — 15 items cached +[2026-04-01 06:20:01] OK — 15 items cached +[2026-04-01 06:25:02] OK — 15 items cached +[2026-04-01 06:30:02] OK — 15 items cached +[2026-04-01 06:35:01] OK — 15 items cached +[2026-04-01 06:40:02] OK — 15 items cached +[2026-04-01 06:45:01] OK — 15 items cached +[2026-04-01 06:50:02] OK — 15 items cached +[2026-04-01 06:55:02] OK — 15 items cached +[2026-04-01 07:00:02] OK — 15 items cached +[2026-04-01 07:05:01] OK — 15 items cached +[2026-04-01 07:10:02] OK — 15 items cached +[2026-04-01 07:15:02] OK — 15 items cached +[2026-04-01 07:20:02] OK — 15 items cached +[2026-04-01 07:25:01] OK — 15 items cached +[2026-04-01 07:30:02] OK — 15 items cached +[2026-04-01 07:35:02] OK — 15 items cached +[2026-04-01 07:40:01] OK — 15 items cached +[2026-04-01 07:45:02] OK — 15 items cached +[2026-04-01 07:50:02] OK — 15 items cached +[2026-04-01 07:55:01] OK — 15 items cached +[2026-04-01 08:00:02] OK — 15 items cached +[2026-04-01 08:05:01] OK — 15 items cached +[2026-04-01 08:10:02] OK — 15 items cached +[2026-04-01 08:15:02] OK — 15 items cached +[2026-04-01 08:20:02] OK — 15 items cached +[2026-04-01 08:25:02] OK — 15 items cached +[2026-04-01 08:30:02] OK — 15 items cached +[2026-04-01 08:35:01] OK — 15 items cached +[2026-04-01 08:40:02] OK — 15 items cached +[2026-04-01 08:45:02] OK — 15 items cached +[2026-04-01 08:50:01] OK — 15 items cached +[2026-04-01 08:55:02] OK — 15 items cached +[2026-04-01 09:00:02] OK — 15 items cached +[2026-04-01 09:05:01] OK — 15 items cached +[2026-04-01 09:10:02] OK — 15 items cached +[2026-04-01 09:15:02] OK — 16 items cached +[2026-04-01 09:20:01] OK — 16 items cached +[2026-04-01 09:25:01] OK — 16 items cached +[2026-04-01 09:30:02] OK — 16 items cached +[2026-04-01 09:35:01] OK — 16 items cached +[2026-04-01 09:40:02] OK — 16 items cached +[2026-04-01 09:45:02] OK — 16 items cached +[2026-04-01 09:50:01] OK — 16 items cached +[2026-04-01 09:55:02] OK — 16 items cached +[2026-04-01 10:00:02] OK — 16 items cached +[2026-04-01 10:05:01] OK — 16 items cached +[2026-04-01 10:10:02] OK — 16 items cached +[2026-04-01 10:15:02] OK — 16 items cached +[2026-04-01 10:20:02] OK — 16 items cached +[2026-04-01 10:25:01] OK — 16 items cached +[2026-04-01 10:30:02] OK — 16 items cached +[2026-04-01 10:35:02] OK — 16 items cached +[2026-04-01 10:40:01] OK — 16 items cached +[2026-04-01 10:45:02] OK — 16 items cached +[2026-04-01 10:50:01] OK — 16 items cached +[2026-04-01 10:55:02] OK — 16 items cached +[2026-04-01 11:00:02] OK — 16 items cached +[2026-04-01 11:05:01] OK — 16 items cached +[2026-04-01 11:10:02] OK — 16 items cached +[2026-04-01 11:15:02] OK — 16 items cached +[2026-04-01 11:20:01] OK — 16 items cached +[2026-04-01 11:25:02] OK — 16 items cached +[2026-04-01 11:30:01] OK — 16 items cached +[2026-04-01 11:35:02] OK — 16 items cached +[2026-04-01 11:40:02] OK — 16 items cached +[2026-04-01 11:45:01] OK — 16 items cached +[2026-04-01 11:50:02] OK — 16 items cached +[2026-04-01 11:55:02] OK — 16 items cached +[2026-04-01 12:00:01] OK — 16 items cached +[2026-04-01 12:05:02] OK — 16 items cached +[2026-04-01 12:10:01] OK — 16 items cached +[2026-04-01 12:15:03] OK — 16 items cached +[2026-04-01 12:20:01] OK — 16 items cached +[2026-04-01 12:25:02] OK — 16 items cached +[2026-04-01 12:30:02] OK — 16 items cached +[2026-04-01 12:35:01] OK — 15 items cached +[2026-04-01 12:40:02] OK — 14 items cached +[2026-04-01 12:45:02] OK — 13 items cached +[2026-04-01 12:50:02] OK — 13 items cached +[2026-04-01 12:55:01] OK — 13 items cached +[2026-04-01 13:00:02] OK — 13 items cached +[2026-04-01 13:05:02] OK — 13 items cached +[2026-04-01 13:10:01] OK — 13 items cached +[2026-04-01 13:15:02] OK — 13 items cached +[2026-04-01 13:20:01] OK — 13 items cached +[2026-04-01 13:25:02] OK — 13 items cached +[2026-04-01 13:30:02] OK — 13 items cached +[2026-04-01 13:35:02] OK — 13 items cached +[2026-04-01 13:40:01] OK — 13 items cached +[2026-04-01 13:45:02] OK — 13 items cached +[2026-04-01 13:50:02] OK — 13 items cached +[2026-04-01 13:55:01] OK — 13 items cached +[2026-04-01 14:00:02] OK — 13 items cached +[2026-04-01 14:05:02] OK — 13 items cached +[2026-04-01 14:10:02] OK — 13 items cached +[2026-04-01 14:15:02] OK — 13 items cached +[2026-04-01 14:20:01] OK — 13 items cached +[2026-04-01 14:25:02] OK — 13 items cached +[2026-04-01 14:30:02] OK — 13 items cached +[2026-04-01 14:35:02] OK — 13 items cached +[2026-04-01 14:40:06] OK — 13 items cached +[2026-04-01 14:45:01] OK — 13 items cached +[2026-04-01 14:50:02] OK — 13 items cached +[2026-04-01 14:55:02] OK — 13 items cached +[2026-04-01 15:00:02] OK — 13 items cached +[2026-04-01 15:05:01] OK — 13 items cached +[2026-04-01 15:10:02] OK — 13 items cached +[2026-04-01 15:15:02] OK — 13 items cached +[2026-04-01 15:20:02] OK — 13 items cached +[2026-04-01 15:25:02] OK — 13 items cached +[2026-04-01 15:30:01] OK — 13 items cached +[2026-04-01 15:35:02] OK — 13 items cached +[2026-04-01 15:40:02] OK — 13 items cached +[2026-04-01 15:45:01] OK — 13 items cached +[2026-04-01 15:50:02] OK — 13 items cached +[2026-04-01 15:55:02] OK — 13 items cached +[2026-04-01 16:00:01] OK — 13 items cached +[2026-04-01 16:05:02] OK — 13 items cached +[2026-04-01 16:10:02] OK — 13 items cached +[2026-04-01 16:15:02] OK — 13 items cached +[2026-04-01 16:20:02] OK — 13 items cached +[2026-04-01 16:25:02] OK — 13 items cached +[2026-04-01 16:30:02] OK — 13 items cached +[2026-04-01 16:35:01] OK — 13 items cached +[2026-04-01 16:40:02] OK — 13 items cached +[2026-04-01 16:45:02] OK — 12 items cached +[2026-04-01 16:50:01] OK — 12 items cached +[2026-04-01 16:55:02] OK — 12 items cached +[2026-04-01 17:00:02] OK — 12 items cached +[2026-04-01 17:05:01] OK — 12 items cached +[2026-04-01 17:10:02] OK — 12 items cached +[2026-04-01 17:15:02] OK — 12 items cached +[2026-04-01 17:20:02] OK — 12 items cached +[2026-04-01 17:25:02] OK — 12 items cached +[2026-04-01 17:30:01] OK — 12 items cached +[2026-04-01 17:35:02] OK — 12 items cached +[2026-04-01 17:40:02] OK — 12 items cached +[2026-04-01 17:45:01] OK — 12 items cached +[2026-04-01 17:50:02] OK — 12 items cached +[2026-04-01 17:55:02] OK — 12 items cached +[2026-04-01 18:00:01] OK — 12 items cached +[2026-04-01 18:05:02] OK — 12 items cached +[2026-04-01 18:10:02] OK — 12 items cached +[2026-04-01 18:15:02] OK — 12 items cached +[2026-04-01 18:20:02] OK — 12 items cached +[2026-04-01 18:25:02] OK — 12 items cached +[2026-04-01 18:30:01] OK — 12 items cached +[2026-04-01 18:35:02] OK — 11 items cached +[2026-04-01 18:40:02] OK — 11 items cached +[2026-04-01 18:45:02] OK — 11 items cached +[2026-04-01 18:50:01] OK — 11 items cached +[2026-04-01 18:55:02] OK — 11 items cached +[2026-04-01 19:00:02] OK — 11 items cached +[2026-04-01 19:05:02] OK — 11 items cached +[2026-04-01 19:10:01] OK — 11 items cached +[2026-04-01 19:15:02] OK — 11 items cached +[2026-04-01 19:20:02] OK — 11 items cached +[2026-04-01 19:25:02] OK — 11 items cached +[2026-04-01 19:30:02] OK — 11 items cached +[2026-04-01 19:35:01] OK — 11 items cached +[2026-04-01 19:40:02] OK — 11 items cached +[2026-04-01 19:45:02] OK — 11 items cached +[2026-04-01 19:50:02] OK — 11 items cached +[2026-04-01 19:55:01] OK — 11 items cached +[2026-04-01 20:00:02] OK — 11 items cached +[2026-04-01 20:05:02] OK — 11 items cached +[2026-04-01 20:10:01] OK — 11 items cached +[2026-04-01 20:15:03] OK — 11 items cached +[2026-04-01 20:20:01] OK — 11 items cached +[2026-04-01 20:25:02] OK — 11 items cached +[2026-04-01 20:30:02] OK — 11 items cached +[2026-04-01 20:35:01] OK — 11 items cached +[2026-04-01 20:40:02] OK — 11 items cached +[2026-04-01 20:45:02] OK — 11 items cached +[2026-04-01 20:50:01] OK — 11 items cached +[2026-04-01 20:55:02] OK — 11 items cached +[2026-04-01 21:00:02] OK — 11 items cached +[2026-04-01 21:05:01] OK — 11 items cached +[2026-04-01 21:10:02] OK — 11 items cached +[2026-04-01 21:15:02] OK — 11 items cached +[2026-04-01 21:20:02] OK — 11 items cached +[2026-04-01 21:25:02] OK — 11 items cached +[2026-04-01 21:30:01] OK — 11 items cached +[2026-04-01 21:35:02] OK — 11 items cached +[2026-04-01 21:40:01] OK — 11 items cached +[2026-04-01 21:45:01] OK — 11 items cached +[2026-04-01 21:50:02] OK — 11 items cached +[2026-04-01 21:55:01] OK — 11 items cached +[2026-04-01 22:00:02] OK — 11 items cached +[2026-04-01 22:05:02] OK — 11 items cached +[2026-04-01 22:10:01] OK — 11 items cached +[2026-04-01 22:15:03] OK — 11 items cached +[2026-04-01 22:20:01] OK — 11 items cached +[2026-04-01 22:25:02] OK — 11 items cached +[2026-04-01 22:30:01] OK — 11 items cached +[2026-04-01 22:35:02] OK — 11 items cached +[2026-04-01 22:40:02] OK — 11 items cached +[2026-04-01 22:45:01] OK — 11 items cached +[2026-04-01 22:50:02] OK — 11 items cached +[2026-04-01 22:55:02] OK — 11 items cached +[2026-04-01 23:00:01] OK — 11 items cached +[2026-04-01 23:05:02] OK — 11 items cached +[2026-04-01 23:10:02] OK — 11 items cached +[2026-04-01 23:15:02] OK — 11 items cached +[2026-04-01 23:20:01] OK — 11 items cached +[2026-04-01 23:25:02] OK — 11 items cached +[2026-04-01 23:30:02] OK — 11 items cached +[2026-04-01 23:35:01] OK — 11 items cached +[2026-04-01 23:40:02] OK — 11 items cached +[2026-04-01 23:45:02] OK — 11 items cached +[2026-04-01 23:50:01] OK — 11 items cached +[2026-04-01 23:55:02] OK — 11 items cached +[2026-04-02 00:00:02] OK — 11 items cached +[2026-04-02 00:05:01] OK — 11 items cached +[2026-04-02 00:10:02] OK — 11 items cached +[2026-04-02 00:15:02] OK — 11 items cached +[2026-04-02 00:20:02] OK — 11 items cached +[2026-04-02 00:25:02] OK — 11 items cached +[2026-04-02 00:30:01] OK — 11 items cached +[2026-04-02 00:35:02] OK — 11 items cached +[2026-04-02 00:40:01] OK — 11 items cached +[2026-04-02 00:45:02] OK — 11 items cached +[2026-04-02 00:50:02] OK — 11 items cached +[2026-04-02 00:55:01] OK — 11 items cached +[2026-04-02 01:00:02] OK — 11 items cached +[2026-04-02 01:05:02] OK — 11 items cached +[2026-04-02 01:10:01] OK — 11 items cached +[2026-04-02 01:15:02] OK — 11 items cached +[2026-04-02 01:20:02] OK — 11 items cached +[2026-04-02 01:25:02] OK — 11 items cached +[2026-04-02 01:30:01] OK — 11 items cached +[2026-04-02 01:35:02] OK — 11 items cached +[2026-04-02 01:40:02] OK — 11 items cached +[2026-04-02 01:45:01] OK — 11 items cached +[2026-04-02 01:50:02] OK — 11 items cached +[2026-04-02 01:55:01] OK — 11 items cached +[2026-04-02 02:00:02] OK — 11 items cached +[2026-04-02 02:05:02] OK — 11 items cached +[2026-04-02 02:10:01] OK — 11 items cached +[2026-04-02 02:15:02] OK — 11 items cached +[2026-04-02 02:20:02] OK — 11 items cached +[2026-04-02 02:25:02] OK — 11 items cached +[2026-04-02 02:30:01] OK — 11 items cached +[2026-04-02 02:35:02] OK — 11 items cached +[2026-04-02 02:40:02] OK — 11 items cached +[2026-04-02 02:45:01] OK — 11 items cached +[2026-04-02 02:50:02] OK — 11 items cached +[2026-04-02 02:55:01] OK — 11 items cached +[2026-04-02 03:00:02] OK — 11 items cached +[2026-04-02 03:05:01] OK — 11 items cached +[2026-04-02 03:10:02] OK — 11 items cached +[2026-04-02 03:15:02] OK — 11 items cached +[2026-04-02 03:20:01] OK — 11 items cached +[2026-04-02 03:25:02] OK — 11 items cached +[2026-04-02 03:30:02] OK — 11 items cached +[2026-04-02 03:35:01] OK — 11 items cached +[2026-04-02 03:40:02] OK — 11 items cached +[2026-04-02 03:45:02] OK — 11 items cached +[2026-04-02 03:50:01] OK — 11 items cached +[2026-04-02 03:55:02] OK — 11 items cached +[2026-04-02 04:00:01] OK — 11 items cached +[2026-04-02 04:05:02] OK — 11 items cached +[2026-04-02 04:10:02] OK — 11 items cached +[2026-04-02 04:15:02] OK — 11 items cached +[2026-04-02 04:20:01] OK — 11 items cached +[2026-04-02 04:25:02] OK — 11 items cached +[2026-04-02 04:30:02] OK — 11 items cached +[2026-04-02 04:35:01] OK — 11 items cached +[2026-04-02 04:40:02] OK — 11 items cached +[2026-04-02 04:45:02] OK — 11 items cached +[2026-04-02 04:50:01] OK — 11 items cached +[2026-04-02 04:55:02] OK — 11 items cached +[2026-04-02 05:00:02] OK — 11 items cached +[2026-04-02 05:05:01] OK — 11 items cached +[2026-04-02 05:10:02] OK — 11 items cached +[2026-04-02 05:15:02] OK — 11 items cached +[2026-04-02 05:20:02] OK — 11 items cached +[2026-04-02 05:25:01] OK — 11 items cached +[2026-04-02 05:30:02] OK — 11 items cached +[2026-04-02 05:35:01] OK — 11 items cached +[2026-04-02 05:40:02] OK — 11 items cached +[2026-04-02 05:45:02] OK — 11 items cached +[2026-04-02 05:50:01] OK — 11 items cached +[2026-04-02 05:55:02] OK — 11 items cached +[2026-04-02 06:00:02] OK — 11 items cached +[2026-04-02 06:05:01] OK — 11 items cached +[2026-04-02 06:10:02] OK — 11 items cached +[2026-04-02 06:15:02] OK — 11 items cached +[2026-04-02 06:20:02] OK — 11 items cached +[2026-04-02 06:25:01] OK — 11 items cached +[2026-04-02 06:30:02] OK — 11 items cached +[2026-04-02 06:35:02] OK — 11 items cached +[2026-04-02 06:40:01] OK — 11 items cached +[2026-04-02 06:45:02] OK — 11 items cached +[2026-04-02 06:50:02] OK — 11 items cached +[2026-04-02 06:55:01] OK — 11 items cached +[2026-04-02 07:00:02] OK — 11 items cached +[2026-04-02 07:05:02] OK — 11 items cached +[2026-04-02 07:10:02] OK — 11 items cached +[2026-04-02 07:15:02] OK — 11 items cached +[2026-04-02 07:20:01] OK — 11 items cached +[2026-04-02 07:25:02] OK — 11 items cached +[2026-04-02 07:30:02] OK — 11 items cached +[2026-04-02 07:35:01] OK — 11 items cached +[2026-04-02 07:40:02] OK — 11 items cached +[2026-04-02 07:45:02] OK — 11 items cached +[2026-04-02 07:50:01] OK — 11 items cached +[2026-04-02 07:55:02] OK — 11 items cached +[2026-04-02 08:00:02] OK — 11 items cached +[2026-04-02 08:05:01] OK — 11 items cached +[2026-04-02 08:10:02] OK — 11 items cached +[2026-04-02 08:15:02] OK — 11 items cached +[2026-04-02 08:20:02] OK — 11 items cached +[2026-04-02 08:25:01] OK — 11 items cached +[2026-04-02 08:30:01] OK — 11 items cached +[2026-04-02 08:35:02] OK — 11 items cached +[2026-04-02 08:40:01] OK — 11 items cached +[2026-04-02 08:45:02] OK — 11 items cached +[2026-04-02 08:50:02] OK — 11 items cached +[2026-04-02 08:55:01] OK — 11 items cached +[2026-04-02 09:00:02] OK — 11 items cached +[2026-04-02 09:05:02] OK — 11 items cached +[2026-04-02 09:10:01] OK — 11 items cached +[2026-04-02 09:15:03] OK — 11 items cached +[2026-04-02 09:20:01] OK — 11 items cached +[2026-04-02 09:25:02] OK — 11 items cached +[2026-04-02 09:30:02] OK — 11 items cached +[2026-04-02 09:35:01] OK — 11 items cached +[2026-04-02 09:40:02] OK — 11 items cached +[2026-04-02 09:45:01] OK — 11 items cached +[2026-04-02 09:50:02] OK — 11 items cached +[2026-04-02 09:55:02] OK — 11 items cached +[2026-04-02 10:00:01] OK — 11 items cached +[2026-04-02 10:05:02] OK — 11 items cached +[2026-04-02 10:10:02] OK — 11 items cached +[2026-04-02 10:15:02] OK — 11 items cached +[2026-04-02 10:20:01] OK — 11 items cached +[2026-04-02 10:25:02] OK — 11 items cached +[2026-04-02 10:30:02] OK — 11 items cached +[2026-04-02 10:35:01] OK — 11 items cached +[2026-04-02 10:40:02] OK — 11 items cached +[2026-04-02 10:45:02] OK — 11 items cached +[2026-04-02 10:50:01] OK — 11 items cached +[2026-04-02 10:55:02] OK — 11 items cached +[2026-04-02 11:00:01] OK — 11 items cached +[2026-04-02 11:05:02] OK — 11 items cached +[2026-04-02 11:10:02] OK — 11 items cached +[2026-04-02 11:15:02] OK — 11 items cached +[2026-04-02 11:20:02] OK — 11 items cached +[2026-04-02 11:25:01] OK — 11 items cached +[2026-04-02 11:30:02] OK — 11 items cached +[2026-04-02 11:35:02] OK — 11 items cached +[2026-04-02 11:40:01] OK — 11 items cached +[2026-04-02 11:45:02] OK — 11 items cached +[2026-04-02 11:50:02] OK — 11 items cached +[2026-04-02 11:55:01] OK — 11 items cached +[2026-04-02 12:00:02] OK — 11 items cached +[2026-04-02 12:05:01] OK — 11 items cached +[2026-04-02 12:10:02] OK — 11 items cached +[2026-04-02 12:15:02] OK — 11 items cached +[2026-04-02 12:20:02] OK — 11 items cached +[2026-04-02 12:25:02] OK — 11 items cached +[2026-04-02 12:30:01] OK — 11 items cached +[2026-04-02 12:35:02] OK — 11 items cached +[2026-04-02 12:40:01] OK — 11 items cached +[2026-04-02 12:45:02] OK — 11 items cached +[2026-04-02 12:50:02] OK — 11 items cached +[2026-04-02 12:55:02] OK — 11 items cached +[2026-04-02 13:00:01] OK — 11 items cached +[2026-04-02 13:05:02] OK — 11 items cached +[2026-04-02 13:10:02] OK — 11 items cached +[2026-04-02 13:15:02] OK — 11 items cached +[2026-04-02 13:20:02] OK — 11 items cached +[2026-04-02 13:25:01] OK — 11 items cached +[2026-04-02 13:30:02] OK — 11 items cached +[2026-04-02 13:35:02] OK — 11 items cached +[2026-04-02 13:40:01] OK — 11 items cached +[2026-04-02 13:45:02] OK — 11 items cached +[2026-04-02 13:50:01] OK — 10 items cached +[2026-04-02 13:55:02] OK — 10 items cached +[2026-04-02 14:00:02] OK — 10 items cached +[2026-04-02 14:05:01] OK — 10 items cached +[2026-04-02 14:10:01] OK — 10 items cached +[2026-04-02 14:15:02] OK — 10 items cached +[2026-04-02 14:20:02] OK — 10 items cached +[2026-04-02 14:25:02] OK — 10 items cached +[2026-04-02 14:30:01] OK — 10 items cached +[2026-04-02 14:35:02] OK — 10 items cached +[2026-04-02 14:40:02] OK — 10 items cached +[2026-04-02 14:45:06] OK — 10 items cached +[2026-04-02 14:50:01] OK — 10 items cached +[2026-04-02 14:55:02] OK — 10 items cached +[2026-04-02 15:00:02] OK — 10 items cached +[2026-04-02 15:05:01] OK — 10 items cached +[2026-04-02 15:10:02] OK — 10 items cached +[2026-04-02 15:15:02] OK — 10 items cached +[2026-04-02 15:20:02] OK — 10 items cached +[2026-04-02 15:25:02] OK — 10 items cached +[2026-04-02 15:30:01] OK — 10 items cached +[2026-04-02 15:35:02] OK — 10 items cached +[2026-04-02 15:40:02] OK — 10 items cached +[2026-04-02 15:45:01] OK — 10 items cached +[2026-04-02 15:50:02] OK — 10 items cached +[2026-04-02 15:55:02] OK — 10 items cached +[2026-04-02 16:00:01] OK — 10 items cached +[2026-04-02 16:05:02] OK — 10 items cached +[2026-04-02 16:10:02] OK — 10 items cached +[2026-04-02 16:15:02] OK — 10 items cached +[2026-04-02 16:20:02] OK — 10 items cached +[2026-04-02 16:25:01] OK — 10 items cached +[2026-04-02 16:30:02] OK — 10 items cached +[2026-04-02 16:35:02] OK — 10 items cached +[2026-04-02 16:40:02] OK — 10 items cached +[2026-04-02 16:45:01] OK — 10 items cached +[2026-04-02 16:50:03] OK — 10 items cached +[2026-04-02 16:55:01] OK — 10 items cached +[2026-04-02 17:00:02] OK — 10 items cached +[2026-04-02 17:05:02] OK — 10 items cached +[2026-04-02 17:10:01] OK — 10 items cached +[2026-04-02 17:15:02] OK — 11 items cached +[2026-04-02 17:20:02] OK — 11 items cached +[2026-04-02 17:25:02] OK — 11 items cached +[2026-04-02 17:30:01] OK — 11 items cached +[2026-04-02 17:35:02] OK — 11 items cached +[2026-04-02 17:40:02] OK — 11 items cached +[2026-04-02 17:45:01] OK — 11 items cached +[2026-04-02 17:50:02] OK — 11 items cached +[2026-04-02 17:55:02] OK — 11 items cached +[2026-04-02 18:00:01] OK — 11 items cached +[2026-04-02 18:05:02] OK — 11 items cached +[2026-04-02 18:10:02] OK — 11 items cached +[2026-04-02 18:15:02] OK — 11 items cached +[2026-04-02 18:20:02] OK — 11 items cached +[2026-04-02 18:25:01] OK — 11 items cached +[2026-04-02 18:30:02] OK — 11 items cached +[2026-04-02 18:35:02] OK — 11 items cached +[2026-04-02 18:40:01] OK — 11 items cached +[2026-04-02 18:45:02] OK — 11 items cached +[2026-04-02 18:50:02] OK — 11 items cached +[2026-04-02 18:55:01] OK — 11 items cached +[2026-04-02 19:00:02] OK — 11 items cached +[2026-04-02 19:05:02] OK — 11 items cached +[2026-04-02 19:10:01] OK — 11 items cached +[2026-04-02 19:15:03] OK — 11 items cached +[2026-04-02 19:20:01] OK — 11 items cached +[2026-04-02 19:25:02] OK — 11 items cached +[2026-04-02 19:30:01] OK — 11 items cached +[2026-04-02 19:35:02] OK — 11 items cached +[2026-04-02 19:40:02] OK — 11 items cached +[2026-04-02 19:45:02] OK — 11 items cached +[2026-04-02 19:50:01] OK — 11 items cached +[2026-04-02 19:55:02] OK — 11 items cached +[2026-04-02 20:00:01] OK — 11 items cached +[2026-04-02 20:05:01] OK — 11 items cached +[2026-04-02 20:10:02] OK — 11 items cached +[2026-04-02 20:15:02] OK — 11 items cached +[2026-04-02 20:20:02] OK — 11 items cached +[2026-04-02 20:25:02] OK — 11 items cached +[2026-04-02 20:30:01] OK — 11 items cached +[2026-04-02 20:35:02] OK — 11 items cached +[2026-04-02 20:40:01] OK — 11 items cached +[2026-04-02 20:45:02] OK — 11 items cached +[2026-04-02 20:50:02] OK — 11 items cached +[2026-04-02 20:55:01] OK — 11 items cached +[2026-04-02 21:00:02] OK — 11 items cached +[2026-04-02 21:05:02] OK — 11 items cached +[2026-04-02 21:10:01] OK — 11 items cached +[2026-04-02 21:15:03] OK — 11 items cached +[2026-04-02 21:20:01] OK — 11 items cached +[2026-04-02 21:25:02] OK — 11 items cached +[2026-04-02 21:30:02] OK — 11 items cached +[2026-04-02 21:35:01] OK — 11 items cached +[2026-04-02 21:40:02] OK — 11 items cached +[2026-04-02 21:45:01] OK — 11 items cached +[2026-04-02 21:50:02] OK — 11 items cached +[2026-04-02 21:55:02] OK — 11 items cached +[2026-04-02 22:00:01] OK — 11 items cached +[2026-04-02 22:05:02] OK — 11 items cached +[2026-04-02 22:10:02] OK — 11 items cached +[2026-04-02 22:15:02] OK — 11 items cached +[2026-04-02 22:20:01] OK — 11 items cached +[2026-04-02 22:25:02] OK — 11 items cached +[2026-04-02 22:30:02] OK — 11 items cached +[2026-04-02 22:35:01] OK — 11 items cached +[2026-04-02 22:40:02] OK — 11 items cached +[2026-04-02 22:45:02] OK — 11 items cached +[2026-04-02 22:50:01] OK — 11 items cached +[2026-04-02 22:55:02] OK — 11 items cached +[2026-04-02 23:00:02] OK — 11 items cached +[2026-04-02 23:05:01] OK — 11 items cached +[2026-04-02 23:10:02] OK — 11 items cached +[2026-04-02 23:15:02] OK — 11 items cached +[2026-04-02 23:20:02] OK — 11 items cached +[2026-04-02 23:25:01] OK — 11 items cached +[2026-04-02 23:30:02] OK — 11 items cached +[2026-04-02 23:35:02] OK — 11 items cached +[2026-04-02 23:40:01] OK — 11 items cached +[2026-04-02 23:45:02] OK — 11 items cached +[2026-04-02 23:50:01] OK — 11 items cached +[2026-04-02 23:55:02] OK — 11 items cached +[2026-04-03 00:00:02] OK — 12 items cached +[2026-04-03 00:05:01] OK — 12 items cached +[2026-04-03 00:10:02] OK — 12 items cached +[2026-04-03 00:15:02] OK — 12 items cached +[2026-04-03 00:20:02] OK — 12 items cached +[2026-04-03 00:25:01] OK — 12 items cached +[2026-04-03 00:30:02] OK — 12 items cached +[2026-04-03 00:35:02] OK — 12 items cached +[2026-04-03 00:40:01] OK — 12 items cached +[2026-04-03 00:45:02] OK — 12 items cached +[2026-04-03 00:50:02] OK — 12 items cached +[2026-04-03 00:55:01] OK — 12 items cached +[2026-04-03 01:00:02] OK — 12 items cached +[2026-04-03 01:05:02] OK — 12 items cached +[2026-04-03 01:10:02] OK — 12 items cached +[2026-04-03 01:15:02] OK — 12 items cached +[2026-04-03 01:20:01] OK — 12 items cached +[2026-04-03 01:25:02] OK — 12 items cached +[2026-04-03 01:30:02] OK — 12 items cached +[2026-04-03 01:35:01] OK — 12 items cached +[2026-04-03 01:40:02] OK — 12 items cached +[2026-04-03 01:45:02] OK — 12 items cached +[2026-04-03 01:50:01] OK — 12 items cached +[2026-04-03 01:55:02] OK — 12 items cached +[2026-04-03 02:00:01] OK — 12 items cached +[2026-04-03 02:05:02] OK — 12 items cached +[2026-04-03 02:10:02] OK — 12 items cached +[2026-04-03 02:15:02] OK — 12 items cached +[2026-04-03 02:20:02] OK — 12 items cached +[2026-04-03 02:25:01] OK — 12 items cached +[2026-04-03 02:30:02] OK — 12 items cached +[2026-04-03 02:35:02] OK — 12 items cached +[2026-04-03 02:40:01] OK — 12 items cached +[2026-04-03 02:45:02] OK — 12 items cached +[2026-04-03 02:50:02] OK — 12 items cached +[2026-04-03 02:55:01] OK — 12 items cached +[2026-04-03 03:00:02] OK — 12 items cached +[2026-04-03 03:05:02] OK — 12 items cached +[2026-04-03 03:10:01] OK — 12 items cached +[2026-04-03 03:15:03] OK — 12 items cached +[2026-04-03 03:20:01] OK — 12 items cached +[2026-04-03 03:25:02] OK — 12 items cached +[2026-04-03 03:30:02] OK — 12 items cached +[2026-04-03 03:35:01] OK — 12 items cached +[2026-04-03 03:40:02] OK — 12 items cached +[2026-04-03 03:45:01] OK — 12 items cached +[2026-04-03 03:50:01] OK — 12 items cached +[2026-04-03 03:55:02] OK — 12 items cached +[2026-04-03 04:00:01] OK — 12 items cached +[2026-04-03 04:05:02] OK — 12 items cached +[2026-04-03 04:10:02] OK — 12 items cached +[2026-04-03 04:15:02] OK — 12 items cached +[2026-04-03 04:20:02] OK — 12 items cached +[2026-04-03 04:25:01] OK — 12 items cached +[2026-04-03 04:30:02] OK — 11 items cached +[2026-04-03 04:35:01] OK — 11 items cached +[2026-04-03 04:40:02] OK — 11 items cached +[2026-04-03 04:45:02] OK — 11 items cached +[2026-04-03 04:50:01] OK — 11 items cached +[2026-04-03 04:55:02] OK — 11 items cached +[2026-04-03 05:00:02] OK — 11 items cached +[2026-04-03 05:05:01] OK — 11 items cached +[2026-04-03 05:10:02] OK — 11 items cached +[2026-04-03 05:15:02] OK — 11 items cached +[2026-04-03 05:20:02] OK — 11 items cached +[2026-04-03 05:25:01] OK — 11 items cached +[2026-04-03 05:30:02] OK — 11 items cached +[2026-04-03 05:35:02] OK — 11 items cached +[2026-04-03 05:40:01] OK — 12 items cached +[2026-04-03 05:45:01] OK — 12 items cached +[2026-04-03 05:50:02] OK — 12 items cached +[2026-04-03 05:55:02] OK — 12 items cached +[2026-04-03 06:00:01] OK — 12 items cached +[2026-04-03 06:05:02] OK — 12 items cached +[2026-04-03 06:10:02] OK — 12 items cached +[2026-04-03 06:15:02] OK — 12 items cached +[2026-04-03 06:20:02] OK — 12 items cached +[2026-04-03 06:25:01] OK — 12 items cached +[2026-04-03 06:30:02] OK — 12 items cached +[2026-04-03 06:35:02] OK — 12 items cached +[2026-04-03 06:40:01] OK — 12 items cached +[2026-04-03 06:45:02] OK — 12 items cached +[2026-04-03 06:50:02] OK — 12 items cached +[2026-04-03 06:55:01] OK — 12 items cached +[2026-04-03 07:00:02] OK — 12 items cached +[2026-04-03 07:05:02] OK — 12 items cached +[2026-04-03 07:10:01] OK — 12 items cached +[2026-04-03 07:15:02] OK — 12 items cached +[2026-04-03 07:20:02] OK — 12 items cached +[2026-04-03 07:25:02] OK — 12 items cached +[2026-04-03 07:30:02] OK — 12 items cached +[2026-04-03 07:35:01] OK — 12 items cached +[2026-04-03 07:40:02] OK — 12 items cached +[2026-04-03 07:45:02] OK — 12 items cached +[2026-04-03 07:50:01] OK — 12 items cached +[2026-04-03 07:55:02] OK — 12 items cached +[2026-04-03 08:00:02] OK — 12 items cached +[2026-04-03 08:05:02] OK — 12 items cached +[2026-04-03 08:10:01] OK — 12 items cached +[2026-04-03 08:15:02] OK — 12 items cached +[2026-04-03 08:20:02] OK — 12 items cached +[2026-04-03 08:25:02] OK — 12 items cached +[2026-04-03 08:30:01] OK — 12 items cached +[2026-04-03 08:35:02] OK — 12 items cached +[2026-04-03 08:40:02] OK — 12 items cached +[2026-04-03 08:45:01] OK — 12 items cached +[2026-04-03 08:50:02] OK — 12 items cached +[2026-04-03 08:55:02] OK — 12 items cached +[2026-04-03 09:00:01] OK — 12 items cached +[2026-04-03 09:05:02] OK — 12 items cached +[2026-04-03 09:10:02] OK — 12 items cached +[2026-04-03 09:15:02] OK — 12 items cached +[2026-04-03 09:20:01] OK — 12 items cached +[2026-04-03 09:25:02] OK — 12 items cached +[2026-04-03 09:30:02] OK — 12 items cached +[2026-04-03 09:35:01] OK — 12 items cached +[2026-04-03 09:40:02] OK — 12 items cached +[2026-04-03 09:45:02] OK — 12 items cached +[2026-04-03 09:50:01] OK — 12 items cached +[2026-04-03 09:55:02] OK — 13 items cached +[2026-04-03 10:00:02] OK — 13 items cached +[2026-04-03 10:05:01] OK — 12 items cached +[2026-04-03 10:10:02] OK — 12 items cached +[2026-04-03 10:15:02] OK — 12 items cached +[2026-04-03 10:20:02] OK — 12 items cached +[2026-04-03 10:25:02] OK — 12 items cached +[2026-04-03 10:30:01] OK — 12 items cached +[2026-04-03 10:35:02] OK — 12 items cached +[2026-04-03 10:40:02] OK — 12 items cached +[2026-04-03 10:45:01] OK — 12 items cached +[2026-04-03 10:50:02] OK — 12 items cached +[2026-04-03 10:55:02] OK — 12 items cached +[2026-04-03 11:00:01] OK — 12 items cached +[2026-04-03 11:05:02] OK — 12 items cached +[2026-04-03 11:10:01] OK — 12 items cached +[2026-04-03 11:15:03] OK — 12 items cached +[2026-04-03 11:20:01] OK — 12 items cached +[2026-04-03 11:25:02] OK — 12 items cached +[2026-04-03 11:30:02] OK — 12 items cached +[2026-04-03 11:35:01] OK — 12 items cached +[2026-04-03 11:40:02] OK — 12 items cached +[2026-04-03 11:45:02] OK — 12 items cached +[2026-04-03 11:50:01] OK — 12 items cached +[2026-04-03 11:55:02] OK — 12 items cached +[2026-04-03 12:00:02] OK — 12 items cached +[2026-04-03 12:05:01] OK — 12 items cached +[2026-04-03 12:10:02] OK — 12 items cached +[2026-04-03 12:15:02] OK — 12 items cached +[2026-04-03 12:20:02] OK — 12 items cached +[2026-04-03 12:25:01] OK — 12 items cached +[2026-04-03 12:30:02] OK — 12 items cached +[2026-04-03 12:35:02] OK — 12 items cached +[2026-04-03 12:40:01] OK — 12 items cached +[2026-04-03 12:45:02] OK — 12 items cached +[2026-04-03 12:50:02] OK — 12 items cached +[2026-04-03 12:55:01] OK — 12 items cached +[2026-04-03 13:00:02] OK — 12 items cached +[2026-04-03 13:05:02] OK — 12 items cached +[2026-04-03 13:10:01] OK — 12 items cached +[2026-04-03 13:15:02] OK — 12 items cached +[2026-04-03 13:20:02] OK — 12 items cached +[2026-04-03 13:25:02] OK — 12 items cached +[2026-04-03 13:30:01] OK — 12 items cached +[2026-04-03 13:35:02] OK — 12 items cached +[2026-04-03 13:40:02] OK — 12 items cached +[2026-04-03 13:45:01] OK — 12 items cached +[2026-04-03 13:50:02] OK — 12 items cached +[2026-04-03 13:55:02] OK — 12 items cached +[2026-04-03 14:00:01] OK — 12 items cached +[2026-04-03 14:05:02] OK — 12 items cached +[2026-04-03 14:10:01] OK — 12 items cached +[2026-04-03 14:15:03] OK — 12 items cached +[2026-04-03 14:20:01] OK — 12 items cached +[2026-04-03 14:25:02] OK — 12 items cached +[2026-04-03 14:30:01] OK — 12 items cached +[2026-04-03 14:35:02] OK — 12 items cached +[2026-04-03 14:40:02] OK — 12 items cached +[2026-04-03 14:45:01] OK — 12 items cached +[2026-04-03 14:50:07] OK — 12 items cached +[2026-04-03 14:55:01] OK — 12 items cached +[2026-04-03 15:00:02] OK — 12 items cached +[2026-04-03 15:05:02] OK — 12 items cached +[2026-04-03 15:10:01] OK — 12 items cached +[2026-04-03 15:15:03] OK — 12 items cached +[2026-04-03 15:20:01] OK — 12 items cached +[2026-04-03 15:25:02] OK — 12 items cached +[2026-04-03 15:30:01] OK — 12 items cached +[2026-04-03 15:35:02] OK — 12 items cached +[2026-04-03 15:40:02] OK — 12 items cached +[2026-04-03 15:45:01] OK — 12 items cached +[2026-04-03 15:50:02] OK — 12 items cached +[2026-04-03 15:55:02] OK — 12 items cached +[2026-04-03 16:00:01] OK — 12 items cached +[2026-04-03 16:05:02] OK — 12 items cached +[2026-04-03 16:10:02] OK — 12 items cached +[2026-04-03 16:15:02] OK — 12 items cached +[2026-04-03 16:20:02] OK — 12 items cached +[2026-04-03 16:25:01] OK — 12 items cached +[2026-04-03 16:30:02] OK — 12 items cached +[2026-04-03 16:35:01] OK — 12 items cached +[2026-04-03 16:40:02] OK — 12 items cached +[2026-04-03 16:45:02] OK — 12 items cached +[2026-04-03 16:50:01] OK — 12 items cached +[2026-04-03 16:55:02] OK — 12 items cached +[2026-04-03 17:00:02] OK — 12 items cached +[2026-04-03 17:05:01] OK — 12 items cached +[2026-04-03 17:10:02] OK — 12 items cached +[2026-04-03 17:15:02] OK — 12 items cached +[2026-04-03 17:20:02] OK — 12 items cached +[2026-04-03 17:25:01] OK — 12 items cached +[2026-04-03 17:30:02] OK — 12 items cached +[2026-04-03 17:35:02] OK — 12 items cached +[2026-04-03 17:40:01] OK — 12 items cached +[2026-04-03 17:45:02] OK — 12 items cached +[2026-04-03 17:50:01] OK — 12 items cached +[2026-04-03 17:55:02] OK — 12 items cached +[2026-04-03 18:00:02] OK — 12 items cached +[2026-04-03 18:05:01] OK — 12 items cached +[2026-04-03 18:10:02] OK — 12 items cached +[2026-04-03 18:15:02] OK — 12 items cached +[2026-04-03 18:20:02] OK — 12 items cached +[2026-04-03 18:25:02] OK — 12 items cached +[2026-04-03 18:30:01] OK — 12 items cached +[2026-04-03 18:35:02] OK — 12 items cached +[2026-04-03 18:40:01] OK — 12 items cached +[2026-04-03 18:45:02] OK — 12 items cached +[2026-04-03 18:50:02] OK — 12 items cached +[2026-04-03 18:55:01] OK — 12 items cached +[2026-04-03 19:00:02] OK — 12 items cached +[2026-04-03 19:05:02] OK — 12 items cached +[2026-04-03 19:10:01] OK — 12 items cached +[2026-04-03 19:15:02] OK — 12 items cached +[2026-04-03 19:20:02] OK — 12 items cached +[2026-04-03 19:25:02] OK — 12 items cached +[2026-04-03 19:30:01] OK — 12 items cached +[2026-04-03 19:35:02] OK — 12 items cached +[2026-04-03 19:40:01] OK — 12 items cached +[2026-04-03 19:45:02] OK — 12 items cached +[2026-04-03 19:50:02] OK — 12 items cached +[2026-04-03 19:55:01] OK — 12 items cached +[2026-04-03 20:00:02] OK — 12 items cached +[2026-04-03 20:05:02] OK — 12 items cached +[2026-04-03 20:10:01] OK — 12 items cached +[2026-04-03 20:15:02] OK — 12 items cached +[2026-04-03 20:20:02] OK — 12 items cached +[2026-04-03 20:25:02] OK — 12 items cached +[2026-04-03 20:30:01] OK — 12 items cached +[2026-04-03 20:35:02] OK — 12 items cached +[2026-04-03 20:40:01] OK — 12 items cached +[2026-04-03 20:45:02] OK — 12 items cached +[2026-04-03 20:50:02] OK — 12 items cached +[2026-04-03 20:55:01] OK — 12 items cached +[2026-04-03 21:00:02] OK — 12 items cached +[2026-04-03 21:05:02] OK — 12 items cached +[2026-04-03 21:10:01] OK — 12 items cached +[2026-04-03 21:15:02] OK — 12 items cached +[2026-04-03 21:20:02] OK — 12 items cached +[2026-04-03 21:25:02] OK — 12 items cached +[2026-04-03 21:30:01] OK — 12 items cached +[2026-04-03 21:35:02] OK — 12 items cached +[2026-04-03 21:40:02] OK — 12 items cached +[2026-04-03 21:45:01] OK — 12 items cached +[2026-04-03 21:50:02] OK — 12 items cached +[2026-04-03 21:55:01] OK — 12 items cached +[2026-04-03 22:00:02] OK — 12 items cached +[2026-04-03 22:05:02] OK — 12 items cached +[2026-04-03 22:10:01] OK — 12 items cached +[2026-04-03 22:15:02] OK — 12 items cached +[2026-04-03 22:20:02] OK — 12 items cached +[2026-04-03 22:25:02] OK — 12 items cached +[2026-04-03 22:30:01] OK — 12 items cached +[2026-04-03 22:35:02] OK — 12 items cached +[2026-04-03 22:40:02] OK — 12 items cached +[2026-04-03 22:45:01] OK — 12 items cached +[2026-04-03 22:50:02] OK — 12 items cached +[2026-04-03 22:55:01] OK — 12 items cached +[2026-04-03 23:00:02] OK — 12 items cached +[2026-04-03 23:05:02] OK — 12 items cached +[2026-04-03 23:10:01] OK — 12 items cached +[2026-04-03 23:15:02] OK — 12 items cached +[2026-04-03 23:20:02] OK — 12 items cached +[2026-04-03 23:25:02] OK — 12 items cached +[2026-04-03 23:30:01] OK — 12 items cached +[2026-04-03 23:35:02] OK — 12 items cached +[2026-04-03 23:40:01] OK — 12 items cached +[2026-04-03 23:45:01] OK — 12 items cached +[2026-04-03 23:50:02] OK — 12 items cached +[2026-04-03 23:55:01] OK — 12 items cached +[2026-04-04 00:00:02] OK — 12 items cached +[2026-04-04 00:05:02] OK — 12 items cached +[2026-04-04 00:10:01] OK — 12 items cached +[2026-04-04 00:15:03] OK — 12 items cached +[2026-04-04 00:20:01] OK — 12 items cached +[2026-04-04 00:25:02] OK — 12 items cached +[2026-04-04 00:30:01] OK — 12 items cached +[2026-04-04 00:35:02] OK — 12 items cached +[2026-04-04 00:40:02] OK — 12 items cached +[2026-04-04 00:45:01] OK — 12 items cached +[2026-04-04 00:50:02] OK — 12 items cached +[2026-04-04 00:55:02] OK — 12 items cached +[2026-04-04 01:00:01] OK — 12 items cached +[2026-04-04 01:05:02] OK — 12 items cached +[2026-04-04 01:10:01] OK — 12 items cached +[2026-04-04 01:15:03] OK — 12 items cached +[2026-04-04 01:20:01] OK — 12 items cached +[2026-04-04 01:25:02] OK — 12 items cached +[2026-04-04 01:30:01] OK — 12 items cached +[2026-04-04 01:35:02] OK — 12 items cached +[2026-04-04 01:40:02] OK — 12 items cached +[2026-04-04 01:45:01] OK — 12 items cached +[2026-04-04 01:50:02] OK — 12 items cached +[2026-04-04 01:55:01] OK — 12 items cached +[2026-04-04 02:00:01] OK — 12 items cached +[2026-04-04 02:05:02] OK — 12 items cached +[2026-04-04 02:10:01] OK — 12 items cached +[2026-04-04 02:15:02] OK — 12 items cached +[2026-04-04 02:20:02] OK — 12 items cached +[2026-04-04 02:25:02] OK — 12 items cached +[2026-04-04 02:30:01] OK — 12 items cached +[2026-04-04 02:35:02] OK — 12 items cached +[2026-04-04 02:40:02] OK — 12 items cached +[2026-04-04 02:45:01] OK — 12 items cached +[2026-04-04 02:50:02] OK — 12 items cached +[2026-04-04 02:55:01] OK — 12 items cached +[2026-04-04 03:00:02] OK — 12 items cached +[2026-04-04 03:05:02] OK — 12 items cached +[2026-04-04 03:10:02] OK — 12 items cached +[2026-04-04 03:15:02] OK — 12 items cached +[2026-04-04 03:20:02] OK — 12 items cached +[2026-04-04 03:25:01] OK — 12 items cached +[2026-04-04 03:30:02] OK — 12 items cached +[2026-04-04 03:35:01] OK — 12 items cached +[2026-04-04 03:40:02] OK — 12 items cached +[2026-04-04 03:45:02] OK — 12 items cached +[2026-04-04 03:50:01] OK — 12 items cached +[2026-04-04 03:55:02] OK — 12 items cached +[2026-04-04 04:00:02] OK — 12 items cached +[2026-04-04 04:05:01] OK — 12 items cached +[2026-04-04 04:10:02] OK — 12 items cached +[2026-04-04 04:15:02] OK — 12 items cached +[2026-04-04 04:20:02] OK — 12 items cached +[2026-04-04 04:25:01] OK — 12 items cached +[2026-04-04 04:30:02] OK — 12 items cached +[2026-04-04 04:35:02] OK — 12 items cached +[2026-04-04 04:40:01] OK — 12 items cached +[2026-04-04 04:45:02] OK — 12 items cached +[2026-04-04 04:50:01] OK — 12 items cached +[2026-04-04 04:55:01] OK — 12 items cached +[2026-04-04 05:00:02] OK — 12 items cached +[2026-04-04 05:05:01] OK — 12 items cached +[2026-04-04 05:10:02] OK — 12 items cached +[2026-04-04 05:15:02] OK — 12 items cached +[2026-04-04 05:20:02] OK — 12 items cached +[2026-04-04 05:25:01] OK — 12 items cached +[2026-04-04 05:30:02] OK — 12 items cached +[2026-04-04 05:35:02] OK — 12 items cached +[2026-04-04 05:40:01] OK — 12 items cached +[2026-04-04 05:45:02] OK — 12 items cached +[2026-04-04 05:50:02] OK — 12 items cached +[2026-04-04 05:55:01] OK — 12 items cached +[2026-04-04 06:00:02] OK — 12 items cached +[2026-04-04 06:05:01] OK — 11 items cached +[2026-04-04 06:10:02] OK — 11 items cached +[2026-04-04 06:15:03] OK — 11 items cached +[2026-04-04 06:20:01] OK — 11 items cached +[2026-04-04 06:25:02] OK — 11 items cached +[2026-04-04 06:30:02] OK — 11 items cached +[2026-04-04 06:35:01] OK — 11 items cached +[2026-04-04 06:40:02] OK — 11 items cached +[2026-04-04 06:45:01] OK — 11 items cached +[2026-04-04 06:50:02] OK — 11 items cached +[2026-04-04 06:55:02] OK — 11 items cached +[2026-04-04 07:00:01] OK — 11 items cached +[2026-04-04 07:05:02] OK — 11 items cached +[2026-04-04 07:10:01] OK — 11 items cached +[2026-04-04 07:15:02] OK — 11 items cached +[2026-04-04 07:20:01] OK — 11 items cached +[2026-04-04 07:25:02] OK — 11 items cached +[2026-04-04 07:30:02] OK — 11 items cached +[2026-04-04 07:35:01] OK — 11 items cached +[2026-04-04 07:40:02] OK — 11 items cached +[2026-04-04 07:45:01] OK — 11 items cached +[2026-04-04 07:50:02] OK — 11 items cached +[2026-04-04 07:55:02] OK — 11 items cached +[2026-04-04 08:00:01] OK — 11 items cached +[2026-04-04 08:05:02] OK — 11 items cached +[2026-04-04 08:10:02] OK — 11 items cached +[2026-04-04 08:15:02] OK — 11 items cached +[2026-04-04 08:20:02] OK — 11 items cached +[2026-04-04 08:25:02] OK — 11 items cached +[2026-04-04 08:30:01] OK — 11 items cached +[2026-04-04 08:35:02] OK — 11 items cached +[2026-04-04 08:40:02] OK — 11 items cached +[2026-04-04 08:45:01] OK — 11 items cached +[2026-04-04 08:50:02] OK — 11 items cached +[2026-04-04 08:55:01] OK — 11 items cached +[2026-04-04 09:00:02] OK — 11 items cached +[2026-04-04 09:05:02] OK — 11 items cached +[2026-04-04 09:10:02] OK — 11 items cached +[2026-04-04 09:15:02] OK — 11 items cached +[2026-04-04 09:20:01] OK — 11 items cached +[2026-04-04 09:25:02] OK — 11 items cached +[2026-04-04 09:30:02] OK — 11 items cached +[2026-04-04 09:35:01] OK — 11 items cached +[2026-04-04 09:40:02] OK — 11 items cached +[2026-04-04 09:45:02] OK — 11 items cached +[2026-04-04 09:50:01] OK — 11 items cached +[2026-04-04 09:55:02] OK — 11 items cached +[2026-04-04 10:00:02] OK — 11 items cached +[2026-04-04 10:05:01] OK — 11 items cached +[2026-04-04 10:10:02] OK — 11 items cached +[2026-04-04 10:15:02] OK — 11 items cached +[2026-04-04 10:20:02] OK — 11 items cached +[2026-04-04 10:25:01] OK — 11 items cached +[2026-04-04 10:30:02] OK — 11 items cached +[2026-04-04 10:35:02] OK — 11 items cached +[2026-04-04 10:40:01] OK — 11 items cached +[2026-04-04 10:45:02] OK — 11 items cached +[2026-04-04 10:50:02] OK — 11 items cached +[2026-04-04 10:55:01] OK — 11 items cached +[2026-04-04 11:00:02] OK — 11 items cached +[2026-04-04 11:05:02] OK — 9 items cached +[2026-04-04 11:10:01] OK — 9 items cached +[2026-04-04 11:15:02] OK — 9 items cached +[2026-04-04 11:20:02] OK — 9 items cached +[2026-04-04 11:25:02] OK — 9 items cached +[2026-04-04 11:30:01] OK — 9 items cached +[2026-04-04 11:35:02] OK — 9 items cached +[2026-04-04 11:40:02] OK — 10 items cached +[2026-04-04 11:45:01] OK — 10 items cached +[2026-04-04 11:50:02] OK — 10 items cached +[2026-04-04 11:55:02] OK — 10 items cached +[2026-04-04 12:00:01] OK — 10 items cached +[2026-04-04 12:05:02] OK — 10 items cached +[2026-04-04 12:10:02] OK — 10 items cached +[2026-04-04 12:15:02] OK — 10 items cached +[2026-04-04 12:20:01] OK — 11 items cached +[2026-04-04 12:25:01] OK — 11 items cached +[2026-04-04 12:30:02] OK — 11 items cached +[2026-04-04 12:35:02] OK — 11 items cached +[2026-04-04 12:40:01] OK — 11 items cached +[2026-04-04 12:45:02] OK — 11 items cached +[2026-04-04 12:50:02] OK — 11 items cached +[2026-04-04 12:55:01] OK — 11 items cached +[2026-04-04 13:00:02] OK — 11 items cached +[2026-04-04 13:05:02] OK — 11 items cached +[2026-04-04 13:10:01] OK — 11 items cached +[2026-04-04 13:15:02] OK — 11 items cached +[2026-04-04 13:20:02] OK — 11 items cached +[2026-04-04 13:25:02] OK — 11 items cached +[2026-04-04 13:30:01] OK — 11 items cached +[2026-04-04 13:35:02] OK — 11 items cached +[2026-04-04 13:40:02] OK — 11 items cached +[2026-04-04 13:45:01] OK — 11 items cached +[2026-04-04 13:50:02] OK — 11 items cached +[2026-04-04 13:55:02] OK — 11 items cached +[2026-04-04 14:00:01] OK — 11 items cached +[2026-04-04 14:05:02] OK — 11 items cached +[2026-04-04 14:10:02] OK — 11 items cached +[2026-04-04 14:15:02] OK — 11 items cached +[2026-04-04 14:20:02] OK — 11 items cached +[2026-04-04 14:25:01] OK — 11 items cached +[2026-04-04 14:30:02] OK — 11 items cached diff --git a/data/dispensa.db b/data/dispensa.db index 25a88db..ce4e9f8 100644 Binary files a/data/dispensa.db and b/data/dispensa.db differ diff --git a/index.html b/index.html index d35d817..4660b01 100644 --- a/index.html +++ b/index.html @@ -643,6 +643,7 @@ + @@ -707,6 +708,36 @@ + +
+
+

📅 Piano Pasti Settimanale

+

Imposta la tipologia di pasto per ogni giorno. Sarà usata come guida nella generazione delle ricette.

+
+ +
+
+
+ +
+ +
+
+ 🌤️ = Pranzo  ·  🌙 = Cena  ·  Tocca un badge per cambiarlo. +
+
+
+
+

📋 Tipologie disponibili

+
+
+
@@ -884,6 +915,7 @@