From 3c9fe7dfea9aaa329d6d9b276fffead7ee0765ee Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Wed, 29 Apr 2026 16:52:36 +0000 Subject: [PATCH] Remove all Dupliclick/Spesa integration; merge annual waste info into status line --- .dockerignore | 1 - .gitignore | 1 - api/index.php | 438 +----------------------------------------- assets/css/style.css | 268 -------------------------- assets/js/app.js | 442 +------------------------------------------ docs/openapi.yaml | 35 ---- index.html | 50 ----- 7 files changed, 8 insertions(+), 1227 deletions(-) diff --git a/.dockerignore b/.dockerignore index 19774dd..24d9b4f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,7 +7,6 @@ data/cron.log data/smart_shopping_cache.json data/bring_token.json data/bring_catalog.json -data/dupliclick_token.json data/client_debug.log data/*.crt data/*.pem diff --git a/.gitignore b/.gitignore index a36e690..7e5d553 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ data/cron.log data/smart_shopping_cache.json data/bring_token.json data/bring_catalog.json -data/dupliclick_token.json data/client_debug.log data/rate_limits/ diff --git a/api/index.php b/api/index.php index cd6cef9..59e1c7d 100644 --- a/api/index.php +++ b/api/index.php @@ -2,7 +2,7 @@ /** * EverShelf - Main API Router * Handles all CRUD operations for products, inventory, shopping lists, - * AI-powered features (Gemini), and third-party integrations (Bring!, DupliClick). + * AI-powered features (Gemini), and third-party integrations (Bring!). * * @author Stimpfl Daniel * @license MIT @@ -65,7 +65,7 @@ function checkRateLimit(string $action): void { // Determine limit based on action $aiActions = ['gemini_readExpiry', 'gemini_chat', 'gemini_identify', 'gemini_suggest_shopping']; - $loginActions = ['dupliclick_login']; + $loginActions = []; $recipeActions = ['generate_recipe', 'generate_recipe_stream']; if (in_array($action, $aiActions)) { @@ -284,25 +284,6 @@ try { migrateUnitsToBase($db); break; - // ===== SPESA ONLINE ===== - case 'dupliclick_login': - dupliclickLogin(); - break; - - case 'dupliclick_search': - dupliclickSearch(); - break; - - case 'dupliclick_status': - $tokenFile = __DIR__ . '/../data/dupliclick_token.json'; - if (file_exists($tokenFile)) { - $td = json_decode(file_get_contents($tokenFile), true); - echo json_encode(['logged_in' => !empty($td['token']), 'email' => $td['email'] ?? '']); - } else { - echo json_encode(['logged_in' => false]); - } - break; - // ===== SHARED APP DATA ===== case 'app_settings_get': appSettingsGet($db); @@ -1963,8 +1944,6 @@ function getServerSettings(): void { 'camera_facing' => env('CAMERA_FACING', 'environment'), 'scale_enabled' => env('SCALE_ENABLED', 'false') === 'true', 'scale_gateway_url' => env('SCALE_GATEWAY_URL', ''), - 'spesa_provider' => env('SPESA_PROVIDER', 'bring'), - 'spesa_ai_prompt' => env('SPESA_AI_PROMPT', ''), 'meal_plan_enabled' => env('MEAL_PLAN_ENABLED', 'false') === 'true', ]); } @@ -1988,8 +1967,6 @@ function saveSettings(): void { 'camera_facing' => 'CAMERA_FACING', 'dietary' => 'DIETARY', 'scale_gateway_url' => 'SCALE_GATEWAY_URL', - 'spesa_provider' => 'SPESA_PROVIDER', - 'spesa_ai_prompt' => 'SPESA_AI_PROMPT', ]; // Boolean keys $boolMap = [ @@ -5194,417 +5171,6 @@ function bringSuggestItems(PDO $db): void { ], JSON_UNESCAPED_UNICODE); } -// ===== DUPLICLICK (GRUPPO POLI) ===== - -function dupliclickLogin(): void { - if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - echo json_encode(['error' => 'POST required']); - return; - } - - $input = json_decode(file_get_contents('php://input'), true); - $email = $input['email'] ?? ''; - $password = $input['password'] ?? ''; - - if (empty($email) || empty($password)) { - echo json_encode(['error' => 'Email e password sono obbligatori']); - return; - } - - $postData = http_build_query([ - 'login' => $email, - 'password' => $password, - 'remember_me' => 'true', - 'show_sectors' => 'false' - ]); - - $ch = curl_init('https://www.dupliclick.it/ebsn/api/auth/login'); - curl_setopt_array($ch, [ - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $postData, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => true, - CURLOPT_TIMEOUT => 15, - CURLOPT_HTTPHEADER => [ - 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8', - 'Accept: application/json', - 'Origin: https://www.dupliclick.it', - 'Referer: https://www.dupliclick.it/', - 'x-ebsn-client: production', - 'x-ebsn-client-redirect: production', - 'x-ebsn-client-uuid: 64b2d6318bb8f97bb1aba47dd8af38f6', - 'x-ebsn-version: 2.0.7' - ], - CURLOPT_SSL_VERIFYPEER => true, - ]); - - $response = curl_exec($ch); - - if (curl_errno($ch)) { - echo json_encode(['error' => 'Errore connessione: ' . curl_error($ch)]); - curl_close($ch); - return; - } - - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); - $headerStr = substr($response, 0, $headerSize); - $body = substr($response, $headerSize); - curl_close($ch); - - // Extract JWT token from x-ebsn-account header - $token = ''; - foreach (explode("\r\n", $headerStr) as $line) { - if (stripos($line, 'x-ebsn-account:') === 0) { - $token = trim(substr($line, strlen('x-ebsn-account:'))); - break; - } - } - - // The response body may have leading whitespace/newlines - trim it - $body = trim($body); - $bodyData = json_decode($body, true); - - // Check login success: status is at response.status (not root level) - if ($bodyData === null) { - echo json_encode(['error' => 'Risposta non valida dal server DupliClick', 'http_code' => $httpCode, 'raw' => substr($body, 0, 500)]); - return; - } - - $respStatus = $bodyData['response']['status'] ?? ($bodyData['status'] ?? -1); - if ($respStatus !== 0) { - $errors = $bodyData['response']['errors'] ?? $bodyData['errors'] ?? []; - $errMsg = $errors[0]['error'] ?? $bodyData['message'] ?? 'Credenziali non valide'; - echo json_encode(['error' => $errMsg, 'status' => $respStatus]); - return; - } - - // User data is at root level, not inside data.user - $userData = $bodyData['data']['user'] ?? $bodyData['user'] ?? null; - $cartId = $bodyData['data']['cartId'] ?? $bodyData['cartId'] ?? null; - - // Save token to file for later use - $tokenData = [ - 'token' => $token, - 'email' => $email, - 'logged_at' => date('c'), - 'user' => $userData, - 'cart_id' => $cartId, - ]; - file_put_contents(__DIR__ . '/../data/dupliclick_token.json', json_encode($tokenData, JSON_PRETTY_PRINT)); - - echo json_encode([ - 'success' => true, - 'token' => !empty($token) ? substr($token, 0, 20) . '...' : '(non trovato)', - 'token_full' => $token, - 'http_code' => $httpCode, - 'data' => $bodyData['data'] ?? null, - 'user' => $userData, - 'response_status' => $respStatus, - 'infos' => $bodyData['response']['infos'] ?? [], - ]); -} - -// ===== DUPLICLICK PRODUCT SEARCH ===== - -function dupliclickSearch(): void { - $query = $_GET['q'] ?? ''; - $spec = $_GET['spec'] ?? ''; - $aiPrompt = $_GET['prompt'] ?? ''; - if (empty($query)) { - echo json_encode(['error' => 'Parametro q obbligatorio']); - return; - } - - // Load saved token - $tokenFile = __DIR__ . '/../data/dupliclick_token.json'; - if (!file_exists($tokenFile)) { - echo json_encode(['error' => 'Non sei loggato a DupliClick. Vai in Configurazione > Spesa Online.']); - return; - } - $tokenData = json_decode(file_get_contents($tokenFile), true); - $token = $tokenData['token'] ?? ''; - if (empty($token)) { - echo json_encode(['error' => 'Token DupliClick non trovato. Effettua il login.']); - return; - } - - $baseHeaders = [ - 'Accept: application/json', - 'Origin: https://www.dupliclick.it', - 'Referer: https://www.dupliclick.it/', - 'x-ebsn-client: production', - 'x-ebsn-client-uuid: 64b2d6318bb8f97bb1aba47dd8af38f6', - 'x-ebsn-version: 2.0.7', - 'x-ebsn-account: ' . $token, - ]; - - // Search catalog by item name only first - $searchResults = dupliclickCatalogSearch($query, $baseHeaders); - if ($searchResults === null) { - echo json_encode(['error' => 'Errore nella ricerca']); - return; - } - - $products = $searchResults['products']; - $total = $searchResults['total']; - - if (empty($products)) { - // Fallback: try searching with spec keywords appended - $specKeywords = dupliclickExtractSpecKeywords($spec); - if ($specKeywords) { - $searchResults = dupliclickCatalogSearch($query . ' ' . $specKeywords, $baseHeaders); - if ($searchResults && !empty($searchResults['products'])) { - $products = $searchResults['products']; - $total = $searchResults['total']; - } - } - if (empty($products)) { - echo json_encode(['success' => true, 'query' => $query, 'product' => null, 'total' => 0]); - return; - } - } - - // Format top 10 products - $topProducts = array_slice($products, 0, 10); - $formatted = array_map('formatDupliclickProduct', $topProducts); - - // If multiple results, use AI to pick the best match - $bestProduct = $formatted[0]; - $aiUsed = false; - if (count($formatted) > 1) { - $aiResult = aiSelectBestProduct($query, $spec, $formatted, $aiPrompt); - if ($aiResult !== null) { - $bestProduct = $aiResult; - $aiUsed = true; - } elseif ($aiResult === null && !empty($spec)) { - // AI said no match — try refined search with spec keywords - $specKeywords = dupliclickExtractSpecKeywords($spec); - if ($specKeywords) { - $refined = dupliclickCatalogSearch($query . ' ' . $specKeywords, $baseHeaders); - if ($refined && !empty($refined['products'])) { - $refinedFormatted = array_map('formatDupliclickProduct', array_slice($refined['products'], 0, 10)); - $aiResult2 = aiSelectBestProduct($query, $spec, $refinedFormatted, $aiPrompt); - if ($aiResult2 !== null) { - $bestProduct = $aiResult2; - $aiUsed = true; - } else { - $bestProduct = $refinedFormatted[0]; - } - } - } - } - } - - echo json_encode([ - 'success' => true, - 'query' => $query, - 'product' => $bestProduct, - 'total' => $total, - 'ai_used' => $aiUsed, - ]); -} - -/** - * Search DupliClick catalog and return raw products array - */ -function dupliclickCatalogSearch(string $query, array $headers): ?array { - $url = 'https://www.dupliclick.it/ebsn/api/products?' . http_build_query([ - 'q' => $query, - 'page' => 1, - 'order_by' => 'search_score desc' - ]); - - $ch = curl_init($url); - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 15, - CURLOPT_HTTPHEADER => $headers, - CURLOPT_SSL_VERIFYPEER => true, - ]); - - $response = curl_exec($ch); - if (curl_errno($ch)) { curl_close($ch); return null; } - curl_close($ch); - - $data = json_decode(trim($response), true); - if (!$data || ($data['response']['status'] ?? -1) !== 0) return null; - - return [ - 'products' => $data['data']['products'] ?? [], - 'total' => $data['data']['page']['totItems'] ?? 0, - ]; -} - -/** - * Extract meaningful product keywords from a Bring specification string, - * stripping quantities, emojis, and noise words. - */ -function dupliclickExtractSpecKeywords(string $spec): string { - if (empty($spec)) return ''; - // Remove priority emojis - $clean = preg_replace('/[\x{1F534}\x{1F7E1}\x{1F7E2}]/u', '', $spec); - // Remove quantities (150g, 500ml, 2x, 1 flacone, etc.) - $clean = preg_replace('/\d+\s*(g|kg|ml|l|pz|pezzi|conf|flacon[ei]|x)\b/i', '', $clean); - $clean = preg_replace('/\d+x\d*/i', '', $clean); - // Remove standalone numbers - $clean = preg_replace('/\b\d+\b/', '', $clean); - // Remove noise words - $noise = ['senza', 'con', 'più', 'meno', 'circa', 'tipo', 'lidl', 'coop', 'conad', 'esselunga']; - $clean = preg_replace('/\b(' . implode('|', $noise) . ')\b/i', '', $clean); - // Remove commas and extra spaces - $clean = preg_replace('/[,+]/', ' ', $clean); - $clean = preg_replace('/\s+/', ' ', trim($clean)); - return $clean; -} - -/** - * Pick the best product from search results using offline text-scoring (no AI needed). - * Returns null when nothing matches well enough (triggers refined search with spec keywords). - */ -function aiSelectBestProduct(string $itemName, string $spec, array $products, string $customPrompt = ''): ?array { - if (empty($products)) return null; - if (count($products) === 1) return $products[0]; - - $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','gr','kg','ml','lt','cl','pz','conf','pack']; - - $tokenize = function(string $s) use ($stop): array { - $clean = mb_strtolower(preg_replace('/[^\p{L}0-9\s]/u', ' ', $s), 'UTF-8'); - return array_values(array_filter( - preg_split('/\s+/', trim($clean)), - fn($t) => mb_strlen($t, 'UTF-8') > 2 && !in_array($t, $stop) - )); - }; - - $queryTokens = $tokenize($itemName); - $specTokens = $tokenize($spec); - - if (empty($queryTokens)) return $products[0]; - - // Variant conflict pairs: if spec says X, penalise products containing opposite - $variantConflicts = [ - 'cubetti' => ['fette','affettata','intera','arrotolata'], - 'fette' => ['cubetti','dadini'], - 'cotto' => ['crudo','stagionato'], - 'crudo' => ['cotto'], - 'intero' => ['macinato','tritato','cubetti','fette'], - 'macinato' => ['intero'], - 'biologico' => [], - ]; - - // Category mismatch: if query implies a category, penalise products from the wrong one - $categoryGuards = [ - ['query' => ['frutta','mele','pere','pesche','fragole','uva','arance','limoni','banane','kiwi'], - 'exclude' => ['succo','succhi','nettare','sciroppo','aranciata','bevanda','bibita']], - ['query' => ['verdura','spinaci','zucchine','carote','finocchio','sedano','broccoli'], - 'exclude' => ['surgelat','succo']], - ['query' => ['formaggio','mozzarella','parmigiano','ricotta','pecorino'], - 'exclude' => ['ravioli','tortellini','cannelloni','lasagne','pizza']], - ['query' => ['pasta','spaghetti','penne','fusilli','rigatoni','tagliatelle'], - 'exclude' => ['insalata','minestra','zuppa','brodo']], - ]; - - $scores = []; - foreach ($products as $idx => $product) { - $productName = $product['name'] ?? ''; - $productBrand = $product['brand'] ?? ''; - $productTokens = $tokenize($productName . ' ' . $productBrand); - $nameLower = mb_strtolower($productName, 'UTF-8'); - $score = 0; - - // --- Token overlap: query vs product --- - foreach ($queryTokens as $qt) { - foreach ($productTokens as $pt) { - if ($qt === $pt) { $score += 6; break; } - if (str_contains($pt, $qt) || str_contains($qt, $pt)) { $score += 2; break; } - } - } - - // --- Spec tokens get extra weight (user specified variant) --- - foreach ($specTokens as $st) { - foreach ($productTokens as $pt) { - if ($st === $pt) { $score += 8; break; } - if (str_contains($pt, $st) || str_contains($st, $pt)) { $score += 3; break; } - } - } - - // --- First-token anchor bonus --- - if (!empty($queryTokens) && !empty($productTokens) && $queryTokens[0] === $productTokens[0]) { - $score += 10; - } - - // --- Category mismatch penalty --- - foreach ($categoryGuards as $guard) { - if (!empty(array_intersect($queryTokens, $guard['query']))) { - foreach ($guard['exclude'] as $exc) { - if (str_contains($nameLower, $exc)) { $score -= 50; break; } - } - } - } - - // --- Variant conflict penalty --- - foreach ($specTokens as $st) { - if (isset($variantConflicts[$st])) { - foreach ($variantConflicts[$st] as $conflict) { - if (str_contains($nameLower, $conflict)) { $score -= 20; } - } - } - } - - $scores[$idx] = $score; - } - - arsort($scores); - reset($scores); - $topIdx = key($scores); - $topScore = current($scores); - next($scores); - $secondScore = current($scores) ?: 0; - - // Return null (triggers spec-refined search) only when spec is given and no product - // matches well, so the caller can retry with more specific keywords. - if (!empty($spec) && $topScore < 4) return null; - - // Otherwise return the best scoring result (fallback to first if score is 0) - return $products[$topIdx]; -} - -function formatDupliclickProduct(array $p): array { - $promo = $p['warehousePromo'] ?? null; - $result = [ - 'productId' => $p['productId'] ?? $p['id'] ?? null, - 'name' => $p['name'] ?? '', - 'brand' => $p['shortDescr'] ?? '', - 'price' => $p['price'] ?? 0, - 'priceDisplay' => $p['priceDisplay'] ?? $p['price'] ?? 0, - 'priceUm' => $p['priceStandardUmDisplay'] ?? null, - 'weightUnit' => $p['weightUnitDisplay'] ?? '', - 'packageDescr' => $p['productInfos']['PACKAGE_DESCR'] ?? '', - 'barcode' => $p['barcode'] ?? '', - 'imageUrl' => $p['mediaURL'] ?? '', - 'slug' => $p['slug'] ?? '', - 'itemUrl' => $p['itemUrl'] ?? '', - 'url' => 'https://www.dupliclick.it' . ($p['itemUrl'] ?? ''), - 'available' => $p['available'] ?? 0, - ]; - - if ($promo) { - $result['promo'] = [ - 'discount' => $promo['discount'] ?? 0, - 'discountPerc' => $promo['discountPerc'] ?? 0, - 'originalPrice' => round(($p['price'] ?? 0) + ($promo['discount'] ?? 0), 2), - 'validFrom' => $promo['validityDate'] ?? '', - 'validTo' => $promo['expireDate'] ?? '', - 'label' => $promo['view']['body'] ?? 'OFFERTA', - 'type' => $promo['promoType'] ?? '', - ]; - } - - return $result; -} - // ===== SHARED APP DATA FUNCTIONS ===== function appSettingsGet(PDO $db): void { diff --git a/assets/css/style.css b/assets/css/style.css index 4d67d64..5ebdf2a 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -499,14 +499,6 @@ body { } .aw-cmp-legend-you { color: #16a34a; } .aw-cmp-legend-avg { color: #f97316; } -/* Annual totals + range note */ -.aw-cmp-range { - font-size: 0.67rem; - color: #6b7280; - margin: 1px 0 4px; - padding: 0; - text-align: center; -} /* Inline status below bars */ .aw-status-inline { font-size: 0.75rem; @@ -1907,57 +1899,6 @@ body { color: #dc2626; } -/* Spesa bar: action buttons below item */ -.spesa-bar { - display: flex; - gap: 6px; - margin-top: 5px; - flex-wrap: wrap; -} - -.spesa-bar-btn { - font-size: 0.7rem; - padding: 3px 8px; - border: 1px solid #e0e0e0; - border-radius: 12px; - background: #f8f9fa; - color: var(--text-secondary); - cursor: pointer; - text-decoration: none; - transition: background 0.2s; - white-space: nowrap; -} - -.spesa-bar-btn:hover, -.spesa-bar-btn:active { - background: #e0f2fe; - border-color: var(--accent); -} - -/* Loading animation */ -.spesa-loading { - font-size: 0.8rem; - color: var(--accent); - margin-top: 3px; - animation: spesaPulse 1.2s ease-in-out infinite; -} - -@keyframes spesaPulse { - 0%, 100% { opacity: 0.4; } - 50% { opacity: 1; } -} - -/* Found product name */ -.spesa-found-name { - font-size: 0.72rem; - color: var(--text-muted); - display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; -} - .shopping-item-remove:hover, .shopping-item-remove:active { background: #fee2e2; @@ -5303,215 +5244,6 @@ body { 30% { transform: translateY(-6px); opacity: 1; } } -/* ===== SPESA ONLINE / DUPLICLICK ===== */ -.provider-selector { - display: flex; - gap: 10px; - flex-wrap: wrap; -} - -.provider-btn { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - padding: 14px 20px; - border: 2px solid var(--border); - border-radius: 12px; - background: var(--card-bg); - cursor: pointer; - transition: all 0.2s; - min-width: 120px; -} - -.provider-btn:hover { - border-color: var(--accent); - background: rgba(45, 80, 22, 0.05); -} - -.provider-btn.active { - border-color: var(--accent); - background: rgba(45, 80, 22, 0.1); - box-shadow: 0 0 0 3px rgba(45, 80, 22, 0.15); -} - -.provider-icon { - font-size: 1.8rem; -} - -.provider-name { - font-weight: 700; - font-size: 0.95rem; - color: var(--text); -} - -.provider-desc { - font-size: 0.75rem; - color: var(--text-muted); -} - -.dupliclick-status { - margin-top: 12px; - padding: 10px 14px; - border-radius: 10px; - font-size: 0.9rem; -} - -.dupliclick-status.success { - background: #dcfce7; - color: #166534; - border: 1px solid #86efac; -} - -.dupliclick-status.error { - background: #fef2f2; - color: #991b1b; - border: 1px solid #fca5a5; -} - -.dupliclick-data { - margin-top: 16px; -} - -.dupliclick-data h4 { - margin-bottom: 12px; - font-size: 1rem; - color: var(--text); -} - -.dupliclick-data-grid { - display: flex; - flex-direction: column; - gap: 6px; -} - -.data-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 12px; - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: 8px; - font-size: 0.85rem; -} - -.data-label { - font-weight: 600; - color: var(--text-muted); - min-width: 120px; -} - -.data-value { - text-align: right; - color: var(--text); - word-break: break-all; - max-width: 60%; -} - -.data-value.token-value { - font-family: monospace; - font-size: 0.8rem; - color: var(--accent); -} - -.raw-data-details { - margin-top: 12px; -} - -.raw-data-details summary { - cursor: pointer; - font-weight: 600; - padding: 8px 0; - color: var(--accent); - font-size: 0.85rem; -} - -.raw-json { - margin-top: 8px; - padding: 12px; - background: #1e293b; - color: #e2e8f0; - border-radius: 10px; - font-size: 0.75rem; - line-height: 1.5; - overflow-x: auto; - max-height: 300px; - overflow-y: auto; - white-space: pre-wrap; - word-break: break-all; -} - -/* ===== SPESA ONLINE - PRICE IN SHOPPING LIST ===== */ -.spesa-total-banner { - background: linear-gradient(135deg, #065f46, #047857); - border-radius: var(--radius); - padding: 14px 16px; - margin-bottom: 12px; - color: white; -} - -.spesa-total-row { - display: flex; - justify-content: space-between; - align-items: center; -} - -.spesa-total-label { - font-size: 0.95rem; - font-weight: 600; -} - -.spesa-total-value { - font-size: 1.4rem; - font-weight: 800; - letter-spacing: -0.5px; -} - -.spesa-total-detail { - font-size: 0.75rem; - opacity: 0.85; - margin-top: 4px; -} - -.spesa-detail-left { - display: flex; - flex-direction: column; - gap: 1px; - margin-top: 3px; -} - -.spesa-pkg { - font-size: 0.68rem; - color: var(--text-muted); -} - -.spesa-promo-badge { - font-size: 0.65rem; - font-weight: 700; - background: #dc2626; - color: white; - padding: 1px 5px; - border-radius: 4px; - white-space: nowrap; -} - -.spesa-promo-expire { - font-size: 0.6rem; - color: var(--text-muted); - font-style: italic; -} - -.spesa-not-found { - font-size: 0.7rem; - color: var(--text-muted); - font-style: italic; -} - -.shopping-item.has-promo { - border-left: 3px solid #dc2626; -} - /* ====== Recipe Archive ====== */ .recipe-archive { display: flex; diff --git a/assets/js/app.js b/assets/js/app.js index 3a86a66..9d42b99 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1553,14 +1553,6 @@ const _debouncedSyncSettings = debounce(function() { pref_zerowaste: s.pref_zerowaste, dietary: s.dietary, appliances: s.appliances, - spesa_provider: s.spesa_provider, - spesa_ai_prompt: s.spesa_ai_prompt, - spesa_email: s.spesa_email, - spesa_password: s.spesa_password, - spesa_logged_in: s.spesa_logged_in, - spesa_user: s.spesa_user, - spesa_data: s.spesa_data, - spesa_token: s.spesa_token }; api('app_settings_save', {}, 'POST', { settings: { user_prefs: shared } }).catch(() => {}); }, 1000); @@ -1576,8 +1568,8 @@ async function syncSettingsFromDB() { const s = getSettings(); const serverKeys = ['default_persons','pref_veloce','pref_pocafame','pref_scadenze', 'pref_healthy','pref_opened','pref_zerowaste','dietary','appliances', - 'camera_facing','scale_enabled','scale_gateway_url','spesa_provider', - 'spesa_ai_prompt','meal_plan_enabled','tts_enabled','tts_url','tts_token', + 'camera_facing','scale_enabled','scale_gateway_url', + 'meal_plan_enabled','tts_enabled','tts_url','tts_token', 'tts_method','tts_auth_type','tts_content_type','tts_payload_key']; for (const key of serverKeys) { if (serverSettings[key] !== undefined && serverSettings[key] !== null && serverSettings[key] !== '') { @@ -1589,16 +1581,6 @@ async function syncSettingsFromDB() { // Also load review_confirmed from DB const res = await api('app_settings_get'); if (res.success && res.settings) { - // Spesa credentials still come from DB (not .env) - if (res.settings.user_prefs) { - const db = res.settings.user_prefs; - for (const key of ['spesa_email','spesa_password','spesa_logged_in', - 'spesa_user','spesa_data','spesa_token']) { - if (db[key] !== undefined) s[key] = db[key]; - } - _settingsCache = s; - localStorage.setItem('evershelf_settings', JSON.stringify(s)); - } if (res.settings.review_confirmed) { _reviewConfirmedCache = res.settings.review_confirmed; } @@ -1624,7 +1606,6 @@ async function loadSettingsUI() { if (cameraSelect) cameraSelect.value = s.camera_facing || 'environment'; loadCameraDevices(); renderAppliances(s.appliances || []); - loadSpesaSettings(); const mealPlanEnabled = s.meal_plan_enabled !== false; const mpEnabledEl = document.getElementById('setting-meal-plan-enabled'); if (mpEnabledEl) mpEnabledEl.checked = mealPlanEnabled; @@ -1692,8 +1673,8 @@ async function loadSettingsUI() { const serverKeys = ['gemini_key','bring_email','bring_password', 'default_persons','pref_veloce','pref_pocafame','pref_scadenze', 'pref_healthy','pref_opened','pref_zerowaste','dietary','appliances', - 'camera_facing','scale_enabled','scale_gateway_url','spesa_provider', - 'spesa_ai_prompt','meal_plan_enabled', + 'camera_facing','scale_enabled','scale_gateway_url', + 'meal_plan_enabled', 'tts_enabled','tts_url','tts_token','tts_method','tts_auth_type', 'tts_content_type','tts_payload_key']; let changed = false; @@ -1906,9 +1887,6 @@ async function saveSettings() { if (ttsPayloadKeyEl2) s.tts_payload_key = ttsPayloadKeyEl2.value.trim() || 'message'; const ttsExtraEl2 = document.getElementById('setting-tts-extra-fields'); if (ttsExtraEl2) s.tts_extra_fields = ttsExtraEl2.value.trim(); - // Save spesa AI prompt if the field exists - const spesaPromptEl = document.getElementById('setting-spesa-ai-prompt'); - if (spesaPromptEl) s.spesa_ai_prompt = spesaPromptEl.value.trim(); // Scale settings const scaleEnabledEl = document.getElementById('setting-scale-enabled'); if (scaleEnabledEl) s.scale_enabled = scaleEnabledEl.checked; @@ -1934,8 +1912,6 @@ async function saveSettings() { camera_facing: s.camera_facing, scale_enabled: s.scale_enabled, scale_gateway_url: s.scale_gateway_url, - spesa_provider: s.spesa_provider, - spesa_ai_prompt: s.spesa_ai_prompt, meal_plan_enabled: s.meal_plan_enabled, tts_enabled: s.tts_enabled, tts_url: s.tts_url, @@ -2374,8 +2350,7 @@ function _renderAntiWasteSection(used30, wasted30, usedP30, wastedP30, usedP60, ▮ ${youLabel} ${myRate}% ${country} ${avgRate}% -

${annualInfo}

-

${statusMsg}

+

${statusMsg}  ·  ${annualInfo}

${allBadges.length > 0 ? `
${initBadges}
` : ''} @@ -7225,7 +7200,6 @@ async function selectProductForAction(productId) { let shoppingListUUID = ''; let shoppingItems = []; let suggestionItems = []; -let shoppingPrices = {}; // { itemName: { product, searched: true } } let _spesaScanTarget = null; // { name, rawName, idx } when tapping item to scan // ===== SHOPPING TABS ===== @@ -7512,41 +7486,6 @@ function logOperation(action, details) { } catch (e) { /* ignore */ } } -const DEFAULT_SPESA_AI_PROMPT = `Sei un assistente per la spesa online. Ti viene dato il nome di un prodotto che l'utente vuole comprare e una lista di prodotti trovati nel catalogo del supermercato. - -Regole di selezione: -- Scegli il prodotto che corrisponde ESATTAMENTE a quello richiesto (stessa categoria merceologica) -- Preferisci prodotti freschi/sfusi rispetto a trasformati (es. "Arance" = arance frutta, NON aranciata bevanda) -- Se c'è una descrizione (es. "a cubetti", "biologico"), trova il prodotto che include quella caratteristica -- Se ci sono più varianti valide, scegli quella con il miglior rapporto qualità/prezzo -- Preferisci formati standard per una famiglia -- NON scegliere mai un prodotto di categoria diversa (bevanda vs frutta, surgelato vs fresco, condimento vs ortaggio, ecc.) -- "Finocchio" = ortaggio fresco, NON semi di finocchio o tisana -- "Arance" = frutta fresca, NON aranciata o succo - -Rispondi SOLO con il numero (indice 0-based) del prodotto migliore, oppure -1 se nessun prodotto è appropriato.`; - -function saveShoppingPrices() { - try { - // Only save items that have been searched (not loading state) - const toSave = {}; - for (const [k, v] of Object.entries(shoppingPrices)) { - if (v.searched) toSave[k] = v; - } - // Persist to shared DB - api('app_settings_save', {}, 'POST', { settings: { shopping_prices: toSave } }).catch(() => {}); - } catch (e) { /* ignore */ } -} - -async function loadShoppingPrices() { - try { - const res = await api('app_settings_get'); - if (res.success && res.settings && res.settings.shopping_prices) { - shoppingPrices = res.settings.shopping_prices; - } - } catch (e) { shoppingPrices = {}; } -} - // Build a better search query from item name + specification function buildSearchQuery(item) { // Only use the item name for search - specification confuses the search engine @@ -8037,18 +7976,6 @@ async function loadShoppingList() { } shoppingItems = newItems; - // Clean up shoppingPrices for items no longer on the list - const currentKeys = new Set(shoppingItems.map(i => i.name.toLowerCase())); - let pricesChanged = false; - for (const key of Object.keys(shoppingPrices)) { - if (!currentKeys.has(key)) { - delete shoppingPrices[key]; - pricesChanged = true; - } - } - if (pricesChanged) saveShoppingPrices(); - - loadShoppingPrices(); // Sync urgente local tags from Bring specification (items marked urgent by us or manually) _syncTagsFromBringSpec(); renderShoppingItems(); @@ -8102,26 +8029,10 @@ async function renderShoppingItems() { if (shoppingItems.length === 0) { container.innerHTML = `

${t('shopping.empty')}

`; - updateSpesaTotal(); return; } const s = getSettings(); - let hasSpesa = s.spesa_logged_in && s.spesa_token; - - // If not logged in locally, check server-side token - if (!hasSpesa) { - try { - const status = await api('dupliclick_status'); - if (status.logged_in) { - hasSpesa = true; - s.spesa_logged_in = true; - s.spesa_token = 'server'; - s.spesa_user = status.email || ''; - saveSettings(s); - } - } catch (e) { /* ignore */ } - } // Build section groups, sorted by urgency weight within each section const TAG_LABELS = { urgente: t('shopping.tag_urgent'), prio: t('shopping.tag_priority'), check: t('shopping.tag_check') }; @@ -8175,8 +8086,6 @@ async function renderShoppingItems() { for (const { item, idx, smartData, urgency } of group.items) { const catIcon = CATEGORY_ICONS[guessCategoryFromName(item.name)] || '🛒'; - const priceKey = item.name.toLowerCase(); - const priceData = shoppingPrices[priceKey]; const bgStyle = urgency && URGENCY_BG[urgency] ? ` style="background:${URGENCY_BG[urgency]}"` : ''; const localTags = getShoppingTags(item.name); @@ -8203,44 +8112,8 @@ async function renderShoppingItems() { ).join('')} `; - let detailHtml = ''; - let priceTag = ''; - let spesaBar = ''; - if (hasSpesa) { - if (priceData && priceData.loading) { - detailHtml = `
🔍 ${t('shopping.price_searching')}
`; - } else if (priceData && priceData.product) { - const p = priceData.product; - const promoHtml = p.promo - ? `${escapeHtml(p.promo.label)} -${Math.round(p.promo.discountPerc)}%` - : ''; - const est = estimateItemPrice(p, item.specification || priceData.spec || ''); - priceTag = est - ? `
~€${est.estimated.toFixed(2)}
` - : `
€${p.price.toFixed(2)}
`; - detailHtml = `
- ${escapeHtml(p.name)} - ${escapeHtml(p.packageDescr)}${est ? ' · ' + escapeHtml(String(p.priceUm || '')) + '/kg' : ''} - ${promoHtml} -
`; - spesaBar = `
- - 🔗 ${t('shopping.open_action')} -
`; - } else if (priceData && priceData.searched && !priceData.product) { - detailHtml = `
${t('shopping.not_found')}
`; - spesaBar = `
- -
`; - } else { - spesaBar = `
- -
`; - } - } - html += ` -
+
${catIcon}
@@ -8251,15 +8124,12 @@ async function renderShoppingItems() {
${_specDisplayText(item.specification) ? `
${escapeHtml(_specDisplayText(item.specification))}
` : ''} ${(urgencyBadge || freqBadge || localTagHtml) ? `
${urgencyBadge}${freqBadge}${localTagHtml}
` : ''} - ${detailHtml}
- ${priceTag}
- ${spesaBar}
`; @@ -8267,7 +8137,6 @@ async function renderShoppingItems() { } container.innerHTML = html; - updateSpesaTotal(); } function toggleShoppingTagMenu(btn) { @@ -8279,157 +8148,6 @@ function toggleShoppingTagMenu(btn) { container.style.display = isOpen ? 'none' : 'block'; } -function updateSpesaTotal() { - const banner = document.getElementById('spesa-total-banner'); - const valueEl = document.getElementById('spesa-total-value'); - const detailEl = document.getElementById('spesa-total-detail'); - - let total = 0; - let found = 0; - let promoSaved = 0; - - for (const item of shoppingItems) { - const pd = shoppingPrices[item.name.toLowerCase()]; - if (pd && pd.product) { - const est = estimateItemPrice(pd.product, item.specification || pd.spec || ''); - total += est ? est.estimated : pd.product.price; - found++; - if (pd.product.promo) { - promoSaved += pd.product.promo.discount; - } - } - } - - if (found === 0) { - banner.style.display = 'none'; - return; - } - - banner.style.display = 'block'; - valueEl.textContent = `€ ${total.toFixed(2)}`; - - let detail = t('shopping.found_count').replace('{found}', found).replace('{total}', shoppingItems.length); - if (promoSaved > 0) { - detail += ` ${t('shopping.savings_offers').replace('{amount}', promoSaved.toFixed(2))}`; - } - detailEl.textContent = detail; -} - -async function searchItemPrice(idx, force = false) { - const item = shoppingItems[idx]; - if (!item) return; - - const priceKey = item.name.toLowerCase(); - const cached = shoppingPrices[priceKey]; - // Invalidate cache if spec changed (e.g. item was updated in Bring) - if (!force && cached && cached.searched) { - const cachedSpec = (cached.spec || '').toLowerCase(); - const currentSpec = (item.specification || '').toLowerCase(); - if (cachedSpec === currentSpec) return; - } - - const s = getSettings(); - const provider = s.spesa_provider || 'dupliclick'; - - // Show loading state - shoppingPrices[priceKey] = { searched: false, loading: true, product: null }; - renderShoppingItems(); - - try { - // Send item name as query, spec separately for AI selection (strip urgency markers) - const searchQ = item.name; - const spec = _cleanSpecForSearch(item.specification || ''); - - const s2 = getSettings(); - const aiPrompt = s2.spesa_ai_prompt || ''; - const res = await api(`${provider}_search`, { - q: searchQ, - spec: spec, - prompt: aiPrompt - }); - if (res.success && res.product) { - shoppingPrices[priceKey] = { searched: true, product: res.product, spec: _cleanSpecForSearch(item.specification || '') }; - } else { - shoppingPrices[priceKey] = { searched: true, product: null }; - } - } catch (e) { - shoppingPrices[priceKey] = { searched: true, product: null }; - } - - saveShoppingPrices(); - renderShoppingItems(); -} - -async function searchAllPrices() { - const s = getSettings(); - if (!s.spesa_logged_in && !s.spesa_token) { - // Try server-side check - try { - const status = await api('dupliclick_status'); - if (!status.logged_in) { - showToast(t('settings.spesa.configure_first'), 'error'); - return; - } - s.spesa_logged_in = true; - s.spesa_token = 'server'; - saveSettings(s); - } catch (e) { - showToast(t('settings.spesa.configure_first'), 'error'); - return; - } - } - - const btn = document.getElementById('btn-search-prices'); - const toSearch = shoppingItems.filter(item => { - const pd = shoppingPrices[item.name.toLowerCase()]; - return !pd || !pd.searched; - }); - - if (toSearch.length === 0) { - showToast(t('shopping.all_searched'), 'info'); - return; - } - - btn.disabled = true; - const totalToSearch = toSearch.length; - - for (let i = 0; i < toSearch.length; i++) { - const item = toSearch[i]; - btn.innerHTML = `⏳ ${t('shopping.searching_progress').replace('{current}', i + 1).replace('{total}', totalToSearch)}`; - - const priceKey = item.name.toLowerCase(); - const provider = s.spesa_provider || 'dupliclick'; - - try { - const aiPrompt = s.spesa_ai_prompt || ''; - const res = await api(`${provider}_search`, { - q: item.name, - spec: _cleanSpecForSearch(item.specification || ''), - prompt: aiPrompt - }); - if (res.success && res.product) { - shoppingPrices[priceKey] = { searched: true, product: res.product, spec: _cleanSpecForSearch(item.specification || '') }; - } else { - shoppingPrices[priceKey] = { searched: true, product: null }; - } - } catch (e) { - shoppingPrices[priceKey] = { searched: true, product: null }; - } - - saveShoppingPrices(); - renderShoppingItems(); - - // Small delay to not overwhelm the API - if (i < toSearch.length - 1) { - await new Promise(r => setTimeout(r, 300)); - } - } - - btn.disabled = false; - btn.innerHTML = `🔍 ${t('shopping.search_prices')}`; - showToast(t('shopping.search_complete').replace('{count}', totalToSearch), 'success'); -} - async function removeBringItem(idx) { const item = shoppingItems[idx]; if (!item) return; @@ -10884,18 +10602,6 @@ function generateScreensaverFact() { // Greeting based on time const greeting = hour < 12 ? 'Buongiorno' : hour < 18 ? 'Buon pomeriggio' : 'Buonasera'; - // Estimated shopping total - let spesaTotal = 0; - let spesaPriced = 0; - for (const item of shop) { - const pd = shoppingPrices[item.name.toLowerCase()]; - if (pd && pd.product) { - const est = estimateItemPrice(pd.product, item.specification || pd.spec || ''); - spesaTotal += est ? est.estimated : pd.product.price; - spesaPriced++; - } - } - // Random item picker const rItem = (arr) => arr.length ? arr[Math.floor(Math.random() * arr.length)] : null; @@ -10959,12 +10665,6 @@ function generateScreensaverFact() { const names = shop.slice(0, 4).map(i => i.name); return `Nella spesa: ${names.join(', ')}${shop.length > 4 ? '...' : ''}`; }); - if (spesaTotal > 0) { - facts.push(() => `Il totale previsto per la spesa è circa €${spesaTotal.toFixed(2)}.`); - if (spesaPriced < shop.length) { - facts.push(() => `Spesa stimata: €${spesaTotal.toFixed(2)} (${spesaPriced} di ${shop.length} prodotti con prezzo).`); - } - } } if (shop.length === 0) { facts.push(() => `La lista della spesa è vuota. Tutto a posto!`); @@ -11719,133 +11419,3 @@ async function _backgroundBringSync() { } catch (e) { /* silent — best effort */ } } -// ===== DUPLICLICK (SPESA ONLINE) ===== - -function selectSpesaProvider(btn, provider) { - document.querySelectorAll('.provider-btn').forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - const s = getSettings(); - s.spesa_provider = provider; - saveSettingsToStorage(s); -} - -async function spesaLogin() { - const email = document.getElementById('setting-spesa-email').value.trim(); - const password = document.getElementById('setting-spesa-password').value.trim(); - const s = getSettings(); - const provider = s.spesa_provider || 'dupliclick'; - - if (!email || !password) { - showToast(t('settings.spesa.missing_credentials'), 'error'); - return; - } - - const btn = document.getElementById('spesa-login-btn'); - const statusEl = document.getElementById('spesa-login-status'); - const resultEl = document.getElementById('spesa-login-result'); - - btn.disabled = true; - btn.innerHTML = `⏳ ${t('settings.spesa.login_in_progress')}`; - statusEl.style.display = 'none'; - resultEl.style.display = 'none'; - - try { - const res = await api(`${provider}_login`, {}, 'POST', { email, password }); - - if (res.error) { - statusEl.className = 'dupliclick-status error'; - statusEl.innerHTML = `❌ ${t('settings.spesa.login_error_prefix')} ${escapeHtml(res.error)}`; - statusEl.style.display = 'block'; - btn.disabled = false; - btn.innerHTML = t('settings.spesa.login_btn'); - return; - } - - // Save credentials and session data persistently - s.spesa_email = email; - s.spesa_password = password; - s.spesa_token = res.token_full || ''; - s.spesa_provider = provider; - s.spesa_logged_in = true; - s.spesa_user = res.user || (res.data && res.data.user) || {}; - s.spesa_data = res.data || {}; - // Save AI prompt too - const promptEl = document.getElementById('setting-spesa-ai-prompt'); - if (promptEl) s.spesa_ai_prompt = promptEl.value.trim(); - saveSettingsToStorage(s); - - statusEl.className = 'dupliclick-status success'; - const welcomeMsg = (res.infos && res.infos[0]) ? res.infos[0].info : t('settings.spesa.login_success_default'); - statusEl.innerHTML = `✅ ${escapeHtml(welcomeMsg)}`; - statusEl.style.display = 'block'; - - // Display key info only - const user = res.user || (res.data && res.data.user) || {}; - const data = res.data || {}; - const shipping = data.shippingAddress || {}; - const points = user.userPoints || data.userPoints || {}; - const fidelityPts = Array.isArray(points) ? points[0] : points['0']; - - let html = '
'; - html += '
'; - - if (user.firstName) html += `
👤 ${t('settings.spesa.result_name_label')}${escapeHtml(user.firstName)} ${escapeHtml(user.lastName || '')}
`; - if (user.fidelityCard) html += `
💳 ${t('settings.spesa.result_card_label')}${escapeHtml(user.fidelityCard)}
`; - if (shipping.addressName) html += `
🏪 ${t('settings.spesa.result_pickup_label')}${escapeHtml(shipping.addressName)}
`; - if (fidelityPts) html += `
⭐ ${t('settings.spesa.result_points_label')}${fidelityPts.value || 0}
`; - - html += '
'; - resultEl.innerHTML = html; - resultEl.style.display = 'block'; - - } catch (e) { - statusEl.className = 'dupliclick-status error'; - statusEl.innerHTML = `❌ ${t('settings.spesa.login_network_error_prefix')} ${escapeHtml(e.message)}`; - statusEl.style.display = 'block'; - } - - btn.disabled = false; - btn.innerHTML = t('settings.spesa.login_btn'); -} - -function loadSpesaSettings() { - const s = getSettings(); - const emailEl = document.getElementById('setting-spesa-email'); - const passEl = document.getElementById('setting-spesa-password'); - const promptEl = document.getElementById('setting-spesa-ai-prompt'); - if (emailEl) emailEl.value = s.spesa_email || s.dupliclick_email || ''; - if (passEl) passEl.value = s.spesa_password || s.dupliclick_password || ''; - if (promptEl) promptEl.value = s.spesa_ai_prompt || DEFAULT_SPESA_AI_PROMPT; - - // Show saved login state - if (s.spesa_logged_in && s.spesa_user) { - const statusEl = document.getElementById('spesa-login-status'); - const resultEl = document.getElementById('spesa-login-result'); - const loginBtn = document.getElementById('spesa-login-btn'); - - if (loginBtn) { - loginBtn.innerHTML = t('settings.spesa.connected_relogin'); - loginBtn.className = 'btn btn-large btn-secondary full-width mt-2'; - } - if (statusEl) { - statusEl.className = 'dupliclick-status success'; - statusEl.innerHTML = `✅ ${t('settings.spesa.connected_as').replace('{name}', `${escapeHtml(s.spesa_user.firstName || '')} ${escapeHtml(s.spesa_user.lastName || '')}`.trim())}`; - statusEl.style.display = 'block'; - } - if (resultEl) { - const user = s.spesa_user; - const shipping = (s.spesa_data && s.spesa_data.shippingAddress) || {}; - const points = user.userPoints || (s.spesa_data && s.spesa_data.userPoints) || {}; - const fidelityPts = Array.isArray(points) ? points[0] : points['0']; - - let html = '
'; - if (user.firstName) html += `
👤 ${t('settings.spesa.result_name_label')}${escapeHtml(user.firstName)} ${escapeHtml(user.lastName || '')}
`; - if (user.fidelityCard) html += `
💳 ${t('settings.spesa.result_card_label')}${escapeHtml(user.fidelityCard)}
`; - if (shipping.addressName) html += `
🏪 ${t('settings.spesa.result_pickup_label')}${escapeHtml(shipping.addressName)}
`; - if (fidelityPts) html += `
⭐ ${t('settings.spesa.result_points_label')}${fidelityPts.value || 0}
`; - html += '
'; - resultEl.innerHTML = html; - resultEl.style.display = 'block'; - } - } -} diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 96f4aba..2ea9973 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -500,39 +500,6 @@ paths: "200": description: Recipe deleted - /index.php?action=dupliclick_login: - post: - summary: Login to DupliClick (online shopping) - tags: [DupliClick] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - email: - type: string - password: - type: string - responses: - "200": - description: Login successful - - /index.php?action=dupliclick_search: - get: - summary: Search DupliClick product catalog - tags: [DupliClick] - parameters: - - name: q - in: query - required: true - schema: - type: string - responses: - "200": - description: Search results - /index.php?action=tts_proxy: post: summary: Proxy TTS request to external endpoint @@ -685,7 +652,5 @@ tags: description: Recipe storage - name: Settings description: Application and server settings - - name: DupliClick - description: DupliClick online shopping integration - name: TTS description: Text-to-Speech proxy diff --git a/index.html b/index.html index 565c50e..ca49874 100644 --- a/index.html +++ b/index.html @@ -565,14 +565,6 @@
- -
- @@ -697,7 +686,6 @@ - @@ -822,44 +810,6 @@
- -
-
-

🛍️ Spesa Online

-

Configura il provider per la spesa online.

-
- -
- -
-
-
-
- - -
-
- - - -
- - - -
- - -

L'AI usa questo prompt per scegliere il prodotto più appropriato tra i risultati. Lascia vuoto per il comportamento predefinito.

-
-
-
-