feat: bring urgency sync, background auto-sync, recipe mealplan chip, screensaver fix

This commit is contained in:
dadaloop82
2026-04-04 14:32:25 +00:00
parent 6e3e451a39
commit 63db7cc114
6 changed files with 1907 additions and 31 deletions
+143 -11
View File
@@ -1350,6 +1350,7 @@ function generateRecipe(PDO $db): void {
$appliances = $input['appliances'] ?? []; $appliances = $input['appliances'] ?? [];
$dietaryRestrictions = $input['dietary_restrictions'] ?? ''; $dietaryRestrictions = $input['dietary_restrictions'] ?? '';
$todayRecipes = $input['today_recipes'] ?? []; $todayRecipes = $input['today_recipes'] ?? [];
$mealPlanType = $input['meal_plan_type'] ?? ''; // e.g. 'pasta', 'pesce', 'legumi', ...
// Fetch all inventory items with expiry info // Fetch all inventory items with expiry info
$stmt = $db->query(" $stmt = $db->query("
@@ -1503,6 +1504,79 @@ function generateRecipe(PDO $db): void {
$dietaryText = "\n\nRESTRIZIONI ALIMENTARI:\n{$dietaryRestrictions}\nRispetta SEMPRE queste restrizioni."; $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 // Today's previous recipes from DB - avoid repetition
$todayText = ''; $todayText = '';
$today = date('Y-m-d'); $today = date('Y-m-d');
@@ -1542,10 +1616,10 @@ function generateRecipe(PDO $db): void {
$prompt = <<<PROMPT $prompt = <<<PROMPT
Sei un nutrizionista e chef italiano esperto. Genera UNA ricetta per $mealLabel per $persons persona/e usando PRINCIPALMENTE gli ingredienti disponibili nella dispensa dell'utente. Sei un nutrizionista e chef italiano esperto. Genera UNA ricetta per $mealLabel per $persons persona/e usando PRINCIPALMENTE gli ingredienti disponibili nella dispensa dell'utente.
{$extraRulesText}{$appliancesText}{$dietaryText}{$varietyText}{$mustUseText} {$extraRulesText}{$appliancesText}{$dietaryText}{$mealPlanText}{$varietyText}{$mustUseText}
REGOLE IMPORTANTI: REGOLE IMPORTANTI:
1. ORDINE DI PRIORITÀ INGREDIENTI (dal più urgente al meno urgente) — gli ingredienti nella lista sono già ordinati per priorità: {$mealPlanRule}1. ORDINE DI PRIORITÀ INGREDIENTI (dal più urgente al meno urgente) — gli ingredienti nella lista sono già ordinati per priorità:
a) PRODOTTI SCADUTI: se ancora commestibili, usali SUBITO — hanno la priorità massima assoluta a) PRODOTTI SCADUTI: se ancora commestibili, usali SUBITO — hanno la priorità massima assoluta
b) PRODOTTI IN SCADENZA IMMINENTE (≤3 giorni): usa questi per primi dopo gli scaduti b) PRODOTTI IN SCADENZA IMMINENTE (≤3 giorni): usa questi per primi dopo gli scaduti
c) PRODOTTI IN SCADENZA RAVVICINATA (≤7 giorni): poi questi c) PRODOTTI IN SCADENZA RAVVICINATA (≤7 giorni): poi questi
@@ -2165,15 +2239,16 @@ function bringAddItems(): void {
} }
$added = 0; $added = 0;
$updated = 0;
$skipped = 0; $skipped = 0;
$errors = []; $errors = [];
// Fetch current list to check for duplicates // Fetch current list to check for duplicates and existing specs
$existingNames = []; $existingItems = []; // strtolower(name) => specification
$listData = bringRequest('GET', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}"); $listData = bringRequest('GET', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}");
if ($listData && isset($listData['purchase'])) { if ($listData && isset($listData['purchase'])) {
foreach ($listData['purchase'] as $existingItem) { 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 // Map Italian name to Bring! catalog key (German) for proper recognition
$bringName = italianToBring($name); $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 (array_key_exists($bringKey, $existingItems)) {
if (in_array(strtolower($bringName), $existingNames)) { // 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++; $skipped++;
}
continue; continue;
} }
$spec = $item['specification'] ?? '';
$body = http_build_query([ $body = http_build_query([
'uuid' => $listUUID, 'uuid' => $listUUID,
'purchase' => $bringName, '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 { function bringRemoveItem(): void {
@@ -2234,6 +2325,10 @@ function bringRemoveItem(): void {
]); ]);
$result = bringRequest('PUT', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}", $body); $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]); 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 * Smart Shopping List: analyzes usage frequency, stock levels, expiry to produce
* intelligent urgency-ranked shopping recommendations. * 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 { function smartShopping(PDO $db): void {
$now = time(); $now = time();
$today = date('Y-m-d'); $today = date('Y-m-d');
@@ -2513,8 +2645,8 @@ function smartShopping(PDO $db): void {
if ($useCount >= 8) $score += 15; if ($useCount >= 8) $score += 15;
elseif ($useCount >= 5) $score += 10; elseif ($useCount >= 5) $score += 10;
// Is already on Bring? // Is already on Bring? (fuzzy token match — mirrors JS _findSimilarItem logic)
$onBring = isset($bringItems[mb_strtolower($p['name'])]); $onBring = _productOnBring($p['name'], $bringItems);
$items[] = [ $items[] = [
'product_id' => $pid, 'product_id' => $pid,
+207
View File
@@ -2970,6 +2970,155 @@ body {
font-size: 1rem; 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 MODE ===== */
.cooking-overlay { .cooking-overlay {
position: fixed; position: fixed;
@@ -3191,6 +3340,29 @@ body {
to { opacity: 0.5; transform: scale(1.06); } 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 { .ctimer-btns {
display: flex; display: flex;
gap: 4px; gap: 4px;
@@ -3664,6 +3836,23 @@ body {
border-color: var(--primary); 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"] { .recipe-option-chip input[type="checkbox"] {
width: 16px; width: 16px;
height: 16px; height: 16px;
@@ -4628,6 +4817,24 @@ body {
.screensaver-fact.visible { .screensaver-fact.visible {
opacity: 1; 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 { .screensaver-shortcuts {
position: absolute; position: absolute;
bottom: max(32px, env(safe-area-inset-bottom, 32px)); bottom: max(32px, env(safe-area-inset-bottom, 32px));
+552 -19
View File
@@ -633,6 +633,21 @@ async function loadSettingsUI() {
loadCameraDevices(); loadCameraDevices();
renderAppliances(s.appliances || []); renderAppliances(s.appliances || []);
loadSpesaSettings(); 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 =>
`<span class="mplan-badge" style="opacity:0.85">${t.icon} ${t.label}</span>`
).join('');
}
// Load server-side settings if not already set locally // Load server-side settings if not already set locally
try { try {
@@ -730,6 +745,9 @@ async function saveSettings() {
s.dietary = document.getElementById('setting-dietary').value.trim(); s.dietary = document.getElementById('setting-dietary').value.trim();
// Camera // Camera
s.camera_facing = document.getElementById('setting-camera-facing').value; 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 // Save spesa AI prompt if the field exists
const spesaPromptEl = document.getElementById('setting-spesa-ai-prompt'); const spesaPromptEl = document.getElementById('setting-spesa-ai-prompt');
if (spesaPromptEl) s.spesa_ai_prompt = spesaPromptEl.value.trim(); 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}). * Check whether `name` matches any item in `list` (array of {name}).
* Returns the matching item or null. * Returns the matching item or null.
* A match = at least one significant token in common. * A match = at least one significant token in common.
* NOTE: intentionally loose use _matchBringToSmart for display/urgency matching.
*/ */
function _findSimilarItem(name, list) { function _findSimilarItem(name, list) {
const tokens = _nameTokens(name); const tokens = _nameTokens(name);
@@ -3671,6 +3690,40 @@ function _findSimilarItem(name, list) {
}) || null; }) || 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) { function showLowStockBringPrompt(result, afterCallback) {
const name = result.product_name || currentProduct?.name || ''; const name = result.product_name || currentProduct?.name || '';
const unit = result.product_unit || currentProduct?.unit || 'pz'; const unit = result.product_unit || currentProduct?.unit || 'pz';
@@ -4288,6 +4341,19 @@ function toggleShoppingTag(itemIdx, tag) {
if (existing.length) tags[key] = existing; if (existing.length) tags[key] = existing;
else delete tags[key]; else delete tags[key];
localStorage.setItem('shopping_tags', JSON.stringify(tags)); 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(); renderShoppingItems();
} catch (e) { console.error('toggleShoppingTag', e); } } catch (e) { console.error('toggleShoppingTag', e); }
} }
@@ -4320,12 +4386,24 @@ async function confirmShoppingItemFound() {
} }
// ===== AUTO-ADD CRITICAL ITEMS TO BRING! ===== // ===== 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() { async function autoAddCriticalItems() {
if (sessionStorage.getItem('_autoAddedCritical')) return; // Time-based guard: run at most once every 10 minutes (not session-based, so new critical items get added promptly)
sessionStorage.setItem('_autoAddedCritical', '1'); 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); const critical = smartShoppingItems.filter(i => i.urgency === 'critical' && !i.on_bring);
if (critical.length === 0) return; 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 { try {
const result = await api('bring_add', {}, 'POST', { items: itemsToAdd, listUUID: shoppingListUUID }); const result = await api('bring_add', {}, 'POST', { items: itemsToAdd, listUUID: shoppingListUUID });
if (result.success && result.added > 0) { if (result.success && result.added > 0) {
@@ -4353,7 +4431,7 @@ async function cleanupObsoleteBringItems() {
// Load all products from our DB to cross-reference // Load all products from our DB to cross-reference
let allProducts = []; let allProducts = [];
try { try {
const res = await api('products'); const res = await api('products_list');
allProducts = res.products || res.data || []; allProducts = res.products || res.data || [];
} catch (e) { return; } } catch (e) { return; }
if (!allProducts.length) 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() { function _renderSmartLastUpdate() {
const el = document.getElementById('smart-last-update'); const el = document.getElementById('smart-last-update');
if (!el || !_smartShoppingLastFetch) return; if (!el || !_smartShoppingLastFetch) return;
@@ -4704,7 +4804,7 @@ async function addSmartToBring() {
if (item) { if (item) {
itemsToAdd.push({ itemsToAdd.push({
name: item.name, 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() { async function loadShoppingList() {
const statusEl = document.getElementById('bring-status'); const statusEl = document.getElementById('bring-status');
const currentEl = document.getElementById('shopping-current'); const currentEl = document.getElementById('shopping-current');
@@ -4794,19 +4955,23 @@ async function loadShoppingList() {
if (pricesChanged) saveShoppingPrices(); if (pricesChanged) saveShoppingPrices();
loadShoppingPrices(); loadShoppingPrices();
// Sync urgente local tags from Bring specification (items marked urgent by us or manually)
_syncTagsFromBringSpec();
renderShoppingItems(); renderShoppingItems();
currentEl.style.display = 'block'; currentEl.style.display = 'block';
// Load smart shopping predictions, then re-render to show badges + auto-add critical // Load smart shopping predictions, then re-render to show badges + auto-add critical
loadSmartShopping().then(() => { 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(); autoAddCriticalItems();
cleanupObsoleteBringItems(); 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) { } catch (err) {
console.error('Bring! error:', err); console.error('Bring! error:', err);
statusEl.style.display = 'block'; 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() { async function renderShoppingItems() {
const container = document.getElementById('shopping-items'); const container = document.getElementById('shopping-items');
const countEl = document.getElementById('shopping-count'); const countEl = document.getElementById('shopping-count');
@@ -4855,10 +5038,17 @@ async function renderShoppingItems() {
low: { icon: '🟢', label: 'Ok', cls: 'badge-low' }, 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 enriched = shoppingItems.map((item, idx) => {
const smartData = smartShoppingItems.find(sd => sd.name.toLowerCase() === item.name.toLowerCase()); const smartData = _matchBringToSmart(item.name, smartShoppingItems);
const urgency = smartData?.urgency || null; 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); const sec = getItemSection(item.name);
return { item, idx, smartData, urgency, sec }; return { item, idx, smartData, urgency, sec };
}); });
@@ -4965,7 +5155,7 @@ async function renderShoppingItems() {
<span class="shopping-item-name">${escapeHtml(item.name)}</span> <span class="shopping-item-name">${escapeHtml(item.name)}</span>
<span class="shopping-item-scan-hint">📷</span> <span class="shopping-item-scan-hint">📷</span>
</div> </div>
${item.specification ? `<div class="shopping-item-spec">${escapeHtml(item.specification)}</div>` : ''} ${_specDisplayText(item.specification) ? `<div class="shopping-item-spec">${escapeHtml(_specDisplayText(item.specification))}</div>` : ''}
${(urgencyBadge || freqBadge || localTagHtml) ? `<div class="shopping-item-badges">${urgencyBadge}${freqBadge}${localTagHtml}</div>` : ''} ${(urgencyBadge || freqBadge || localTagHtml) ? `<div class="shopping-item-badges">${urgencyBadge}${freqBadge}${localTagHtml}</div>` : ''}
${detailHtml} ${detailHtml}
</div> </div>
@@ -5052,9 +5242,9 @@ async function searchItemPrice(idx, force = false) {
renderShoppingItems(); renderShoppingItems();
try { 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 searchQ = item.name;
const spec = item.specification || ''; const spec = _cleanSpecForSearch(item.specification || '');
const s2 = getSettings(); const s2 = getSettings();
const aiPrompt = s2.spesa_ai_prompt || ''; const aiPrompt = s2.spesa_ai_prompt || '';
@@ -5064,7 +5254,7 @@ async function searchItemPrice(idx, force = false) {
prompt: aiPrompt prompt: aiPrompt
}); });
if (res.success && res.product) { 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 { } else {
shoppingPrices[priceKey] = { searched: true, product: null }; shoppingPrices[priceKey] = { searched: true, product: null };
} }
@@ -5120,11 +5310,11 @@ async function searchAllPrices() {
const aiPrompt = s.spesa_ai_prompt || ''; const aiPrompt = s.spesa_ai_prompt || '';
const res = await api(`${provider}_search`, { const res = await api(`${provider}_search`, {
q: item.name, q: item.name,
spec: item.specification || '', spec: _cleanSpecForSearch(item.specification || ''),
prompt: aiPrompt prompt: aiPrompt
}); });
if (res.success && res.product) { 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 { } else {
shoppingPrices[priceKey] = { searched: true, product: null }; 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 = `<div class="mplan-header">
<span class="mplan-col-header">🌤 Pranzo</span>
<span class="mplan-col-header">🌙 Cena</span>
</div>`;
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 `<div class="mplan-row${todayClass}">
<div class="mplan-day-name">${WEEK_DAYS_SHORT[i]}</div>
<span class="mplan-badge mplan-badge-pranzo" onclick="openMealPlanPicker(${dow},'pranzo',this)">${pt.icon} ${pt.label}</span>
<span class="mplan-badge mplan-badge-cena" onclick="openMealPlanPicker(${dow},'cena',this)">${ct.icon} ${ct.label}</span>
</div>`;
}).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 =>
`<button class="mplan-pick-btn${t.id === current ? ' active' : ''}" onclick="selectMealPlanType(${dow},'${slot}','${t.id}')">${t.icon} ${t.label}</button>`
).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 ===== // ===== RECIPE GENERATION =====
const MEAL_TYPES = [ const MEAL_TYPES = [
{ id: 'colazione', icon: '☀️', label: 'Colazione', from: 6, to: 11 }, { id: 'colazione', icon: '☀️', label: 'Colazione', from: 6, to: 11 },
@@ -5710,6 +6055,9 @@ function openRecipeDialog() {
} }
updateRecipeMealTitle(); updateRecipeMealTitle();
// Show today's meal plan hint
_renderMealPlanHint(meal);
// Check for cached recipe matching current meal type // Check for cached recipe matching current meal type
if (_cachedRecipe && _cachedRecipe.meal === meal && _cachedRecipe.recipe) { if (_cachedRecipe && _cachedRecipe.meal === meal && _cachedRecipe.recipe) {
document.getElementById('recipe-ask').style.display = 'none'; document.getElementById('recipe-ask').style.display = 'none';
@@ -6377,6 +6725,7 @@ function removeCookingTimer(id) {
if (t && t.interval) clearInterval(t.interval); if (t && t.interval) clearInterval(t.interval);
_cookingTimers = _cookingTimers.filter(t => t.id !== id); _cookingTimers = _cookingTimers.filter(t => t.id !== id);
renderTimersBar(); renderTimersBar();
_updateScreenFlash();
} }
function toggleCookingTimerById(id) { function toggleCookingTimerById(id) {
@@ -6431,6 +6780,25 @@ function _updateTimerCard(id) {
} }
toggleBtn.textContent = t.running ? '⏸' : '▶'; toggleBtn.textContent = t.running ? '⏸' : '▶';
toggleBtn.classList.toggle('running', 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() { function renderTimersBar() {
@@ -6466,6 +6834,7 @@ function clearAllCookingTimers() {
_cookingSuggestedLabel = ''; _cookingSuggestedLabel = '';
const bar = document.getElementById('cooking-timers-bar'); const bar = document.getElementById('cooking-timers-bar');
if (bar) { bar.style.display = 'none'; bar.innerHTML = ''; } if (bar) { bar.style.display = 'none'; bar.innerHTML = ''; }
_updateScreenFlash();
} }
// ===== END COOKING TIMER SYSTEM ===== // ===== END COOKING TIMER SYSTEM =====
@@ -6511,6 +6880,48 @@ function cookingUseIngredient(idx, productId, location, qtyNumber, btn) {
function updateRecipeMealTitle() { function updateRecipeMealTitle() {
const meal = getSelectedMealType(); const meal = getSelectedMealType();
document.getElementById('recipe-meal-title').textContent = MEAL_LABELS[meal] || '🍳 Ricetta'; 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 = `<span class="mplan-hint-badge">${t.icon} ${t.label}</span> <span class="mplan-hint-label">suggerito dal piano settimanale</span>`;
el.style.display = 'flex';
}
if (banner) {
const slotLabel = mealSlot === 'pranzo' ? '🌤️ Pranzo' : '🌙 Cena';
banner.innerHTML = `<span style="opacity:0.75;font-weight:500">${slotLabel}</span><span style="opacity:0.45">·</span><span>${t.icon} ${t.label}</span>`;
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() { function regenerateRecipe() {
@@ -6536,6 +6947,15 @@ async function generateRecipe() {
const persons = parseInt(document.getElementById('recipe-persons').value) || 1; const persons = parseInt(document.getElementById('recipe-persons').value) || 1;
const settings = getSettings(); 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 // Gather active options from checkboxes
const options = []; const options = [];
const optMap = { const optMap = {
@@ -6562,7 +6982,8 @@ async function generateRecipe() {
options, options,
appliances: settings.appliances || [], appliances: settings.appliances || [],
dietary_restrictions: settings.dietary_restrictions || '', dietary_restrictions: settings.dietary_restrictions || '',
today_recipes: await getTodayRecipeTitles() today_recipes: await getTodayRecipeTitles(),
meal_plan_type: mealPlanType,
}); });
if (!result.success) { if (!result.success) {
@@ -6817,6 +7238,25 @@ function updateScreensaverClock() {
const date = now.toLocaleDateString('it-IT', { weekday: 'long', day: 'numeric', month: 'long' }); const date = now.toLocaleDateString('it-IT', { weekday: 'long', day: 'numeric', month: 'long' });
const el = document.getElementById('screensaver-clock'); const el = document.getElementById('screensaver-clock');
if (el) el.innerHTML = `${time}<div class="screensaver-date">${date}</div>`; if (el) el.innerHTML = `${time}<div class="screensaver-date">${date}</div>`;
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 = `<span class="screensaver-mealplan-badge">${slotLabel} · ${t.icon} ${t.label}</span>`;
el.style.display = 'block';
} }
function dismissScreensaver(targetPage) { function dismissScreensaver(targetPage) {
@@ -7315,14 +7755,107 @@ function initInactivityWatcher() {
// ===== INITIALIZATION ===== // ===== INITIALIZATION =====
document.addEventListener('DOMContentLoaded', () => { 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(); syncSettingsFromDB();
showPage('dashboard'); showPage('dashboard');
initInactivityWatcher(); initInactivityWatcher();
initSpesaMode(); initSpesaMode();
initScreensaverShortcuts(); initScreensaverShortcuts();
startBgShoppingRefresh(); 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) ===== // ===== DUPLICLICK (SPESA ONLINE) =====
function selectSpesaProvider(btn, provider) { function selectSpesaProvider(btn, provider) {
+968
View File
@@ -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
BIN
View File
Binary file not shown.
+36
View File
@@ -643,6 +643,7 @@
<button class="settings-tab active" onclick="switchSettingsTab(this, 'tab-api')" data-tab="tab-api" title="API Keys">🔑</button> <button class="settings-tab active" onclick="switchSettingsTab(this, 'tab-api')" data-tab="tab-api" title="API Keys">🔑</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-bring')" data-tab="tab-bring" title="Bring!">🛒</button> <button class="settings-tab" onclick="switchSettingsTab(this, 'tab-bring')" data-tab="tab-bring" title="Bring!">🛒</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-recipe')" data-tab="tab-recipe" title="Ricette">🍳</button> <button class="settings-tab" onclick="switchSettingsTab(this, 'tab-recipe')" data-tab="tab-recipe" title="Ricette">🍳</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-mealplan')" data-tab="tab-mealplan" title="Piano Settimanale">📅</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-appliances')" data-tab="tab-appliances" title="Elettrodomestici">🔌</button> <button class="settings-tab" onclick="switchSettingsTab(this, 'tab-appliances')" data-tab="tab-appliances" title="Elettrodomestici">🔌</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-spesa')" data-tab="tab-spesa" title="Spesa Online">🛍️</button> <button class="settings-tab" onclick="switchSettingsTab(this, 'tab-spesa')" data-tab="tab-spesa" title="Spesa Online">🛍️</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-camera')" data-tab="tab-camera" title="Fotocamera">📷</button> <button class="settings-tab" onclick="switchSettingsTab(this, 'tab-camera')" data-tab="tab-camera" title="Fotocamera">📷</button>
@@ -707,6 +708,36 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Weekly Meal Plan Tab -->
<div class="settings-panel" id="tab-mealplan">
<div class="settings-card">
<h4>📅 Piano Pasti Settimanale</h4>
<p class="settings-hint">Imposta la tipologia di pasto per ogni giorno. Sarà usata come guida nella generazione delle ricette.</p>
<div class="form-group" style="margin-bottom:10px">
<label class="toggle-row">
<span>✅ Attiva piano pasti settimanale</span>
<span class="toggle-switch">
<input type="checkbox" id="setting-meal-plan-enabled" onchange="onMealPlanEnabledChange(this)">
<span class="toggle-slider"></span>
</span>
</label>
</div>
<div id="meal-plan-config-section">
<div id="meal-plan-grid" class="mplan-grid"></div>
<div id="meal-plan-picker" class="mplan-picker" style="display:none"></div>
<div style="margin-top:12px; display:flex; gap:8px; flex-wrap:wrap">
<button class="btn btn-small btn-secondary" onclick="resetMealPlan()">↺ Ripristina default</button>
</div>
<div class="settings-hint" style="margin-top:10px">
🌤️ = Pranzo &nbsp;·&nbsp; 🌙 = Cena &nbsp;·&nbsp; Tocca un badge per cambiarlo.
</div>
</div>
</div>
<div class="settings-card" id="meal-plan-legend-card">
<h4>📋 Tipologie disponibili</h4>
<div class="mplan-legend"></div>
</div>
</div>
<!-- Appliances Tab --> <!-- Appliances Tab -->
<div class="settings-panel" id="tab-appliances"> <div class="settings-panel" id="tab-appliances">
<div class="settings-card"> <div class="settings-card">
@@ -884,6 +915,7 @@
<!-- Recipe Dialog --> <!-- Recipe Dialog -->
<div class="modal-overlay" id="recipe-overlay" style="display:none" onclick="closeRecipeDialog()"> <div class="modal-overlay" id="recipe-overlay" style="display:none" onclick="closeRecipeDialog()">
<div class="modal-content recipe-dialog" onclick="event.stopPropagation()"> <div class="modal-content recipe-dialog" onclick="event.stopPropagation()">
<div id="recipe-mealplan-banner" class="recipe-mealplan-banner" style="display:none"></div>
<div id="recipe-ask" class="recipe-ask"> <div id="recipe-ask" class="recipe-ask">
<h3 id="recipe-meal-title">🍳 Ricetta</h3> <h3 id="recipe-meal-title">🍳 Ricetta</h3>
<p class="recipe-desc">Genero una ricetta sana con gli ingredienti in dispensa, dando priorità a quelli in scadenza.</p> <p class="recipe-desc">Genero una ricetta sana con gli ingredienti in dispensa, dando priorità a quelli in scadenza.</p>
@@ -891,6 +923,7 @@
<label>🕐 Per quale pasto?</label> <label>🕐 Per quale pasto?</label>
<div class="recipe-meal-grid" id="recipe-meal-grid" onchange="updateRecipeMealTitle()"></div> <div class="recipe-meal-grid" id="recipe-meal-grid" onchange="updateRecipeMealTitle()"></div>
</div> </div>
<div id="recipe-mealplan-hint" class="recipe-mealplan-hint" style="display:none"></div>
<div class="form-group"> <div class="form-group">
<label>👥 Quante persone?</label> <label>👥 Quante persone?</label>
<div class="qty-control"> <div class="qty-control">
@@ -902,6 +935,7 @@
<div class="form-group" style="text-align:left"> <div class="form-group" style="text-align:left">
<label>🎯 Tipo di pasto</label> <label>🎯 Tipo di pasto</label>
<div class="recipe-options-grid"> <div class="recipe-options-grid">
<label class="recipe-option-chip recipe-opt-mealplan-chip" id="recipe-opt-mealplan-wrap" style="display:none"><input type="checkbox" id="recipe-opt-mealplan" checked> <span id="recipe-opt-mealplan-label"></span></label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-veloce"> ⚡ Pasto Veloce</label> <label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-veloce"> ⚡ Pasto Veloce</label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-pocafame"> 🥗 Poca Fame</label> <label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-pocafame"> 🥗 Poca Fame</label>
<label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-scadenze"> ⏰ Priorità Scadenze</label> <label class="recipe-option-chip"><input type="checkbox" id="recipe-opt-scadenze"> ⏰ Priorità Scadenze</label>
@@ -954,6 +988,7 @@
<div id="screensaver" class="screensaver-overlay" style="display:none"> <div id="screensaver" class="screensaver-overlay" style="display:none">
<div class="screensaver-content"> <div class="screensaver-content">
<div class="screensaver-clock" id="screensaver-clock"></div> <div class="screensaver-clock" id="screensaver-clock"></div>
<div id="screensaver-mealplan" class="screensaver-mealplan" style="display:none"></div>
<div class="screensaver-fact" id="screensaver-fact"></div> <div class="screensaver-fact" id="screensaver-fact"></div>
</div> </div>
<div class="screensaver-shortcuts"> <div class="screensaver-shortcuts">
@@ -968,6 +1003,7 @@
<!-- ===== COOKING MODE OVERLAY ===== --> <!-- ===== COOKING MODE OVERLAY ===== -->
<div id="cooking-overlay" class="cooking-overlay" style="display:none" aria-live="polite"> <div id="cooking-overlay" class="cooking-overlay" style="display:none" aria-live="polite">
<div id="cooking-flash-overlay" class="cooking-flash-overlay"></div>
<div class="cooking-header"> <div class="cooking-header">
<button class="cooking-close-btn" onclick="closeCookingMode()" title="Chiudi"></button> <button class="cooking-close-btn" onclick="closeCookingMode()" title="Chiudi"></button>
<span class="cooking-title" id="cooking-title"></span> <span class="cooking-title" id="cooking-title"></span>