From a85390b498b2a109e617ea3c58f0af36bec3a626 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Mon, 4 May 2026 05:50:30 +0000 Subject: [PATCH] feat(ai): guard all Gemini features when API key is not configured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added _geminiAvailable global flag (false by default): - Set in _initApp() from serverSettings.gemini_key_set after app loads - Updated in syncSettingsFromDB() so it stays current if key is added later Added _requireGemini() helper: - Returns true if Gemini key is configured → proceed normally - Returns false + shows a warning toast if key is missing → abort Added _updateGeminiButtonState(): - Adds .header-btn-no-ai CSS class to Gemini button when key is missing: greyed out, slight grayscale filter, amber dot badge in corner - Updates button tooltip to explain what to do - Removes class/restores normal appearance when key is present All 6 AI entry points now call _requireGemini() as first line: captureForAI() — AI product identification from scan page captureForAIFormFill() — AI product fill in manual add form scanExpiryWithAI() — AI expiry date reader openRecipeDialog() — recipe generation dialog generateRecipe() — recipe generation (direct call path) quickRecipeSuggestion() — quick expiring-products recipe (→ chat) showPage('chat') — Gemini chat page Previously: user would click the button, camera would open, API call would fail, and only THEN see an error message deep in the flow. Now: blocked immediately at the entry point with a clear toast. --- assets/css/style.css | 20 ++++++++++++++++++++ assets/js/app.js | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/assets/css/style.css b/assets/css/style.css index 9d643c4..ed8dcd0 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -274,6 +274,26 @@ body { background: rgba(99, 102, 241, 0.55); } +/* When Gemini API key is not configured */ +.header-gemini-btn.header-btn-no-ai { + opacity: 0.45; + filter: grayscale(0.7); + border-color: rgba(255, 255, 255, 0.25); + background: rgba(255, 255, 255, 0.1); + position: relative; +} +.header-gemini-btn.header-btn-no-ai::after { + content: ''; + position: absolute; + bottom: 2px; + right: 2px; + width: 8px; + height: 8px; + background: #f59e0b; + border-radius: 50%; + border: 1.5px solid rgba(0,0,0,0.3); +} + .gemini-icon { filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2)); } diff --git a/assets/js/app.js b/assets/js/app.js index 4cf74d7..6d303fc 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -80,6 +80,35 @@ function reportError(payload) { // Fires on tab focus and every 5 minutes. const _loadedVersion = (document.querySelector('.header-version')?.textContent?.trim() || '').replace(/^v/, ''); +// ── Gemini AI availability ──────────────────────────────────────────────────── +// Set to true in _initApp / syncSettingsFromDB once server confirms key is set. +// All AI entry points call _requireGemini() before opening camera / API calls. +let _geminiAvailable = false; + +function _requireGemini() { + if (_geminiAvailable) return true; + showToast( + '🤖 ' + t('error.no_api_key'), + 'warning', + 6000 + ); + return false; +} + +// Update Gemini button visual state to signal no key configured +function _updateGeminiButtonState() { + const btn = document.querySelector('.header-gemini-btn'); + if (!btn) return; + if (_geminiAvailable) { + btn.classList.remove('header-btn-no-ai'); + btn.removeAttribute('title'); + btn.setAttribute('title', 'Chat con Gemini'); + } else { + btn.classList.add('header-btn-no-ai'); + btn.setAttribute('title', '🤖 Gemini non configurato — imposta GEMINI_API_KEY nelle impostazioni'); + } +} + function _checkWebappUpdate() { const STORAGE_KEY = '_evershelf_update_checked_at'; const SEEN_KEY = '_evershelf_update_seen_ts'; @@ -1779,6 +1808,8 @@ async function syncSettingsFromDB() { try { // Primary: load from server .env const serverSettings = await api('get_settings'); + _geminiAvailable = !!(serverSettings.gemini_key_set || serverSettings.gemini_key); + _updateGeminiButtonState(); const s = getSettings(); const serverKeys = ['default_persons','pref_veloce','pref_pocafame','pref_scadenze', 'pref_healthy','pref_opened','pref_zerowaste','dietary','appliances', @@ -2265,7 +2296,7 @@ function showPage(pageId, param = null) { case 'log': loadLog(); break; case 'ai': initAICamera(); break; case 'settings': loadSettingsUI(); break; - case 'chat': initChat(); break; + case 'chat': if (_requireGemini()) initChat(); break; } // Auto-refresh banner notifications while on dashboard (every 5 min) @@ -2865,6 +2896,7 @@ function openedFraction(item) { } function quickRecipeSuggestion() { + if (!_requireGemini()) return; // Navigate to chat and auto-send a prompt about expiring products showPage('chat'); setTimeout(() => { @@ -7090,6 +7122,7 @@ async function submitUse(e) { // ===== AI IDENTIFICATION ===== async function captureForAI() { + if (!_requireGemini()) return; stopScanner(); showPage('ai'); } @@ -7399,6 +7432,7 @@ async function saveAIProductDirect() { let _pfAiStream = null; async function captureForAIFormFill() { + if (!_requireGemini()) return; document.getElementById('modal-content').innerHTML = `