feat(ai): guard all Gemini features when API key is not configured

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.
This commit is contained in:
dadaloop82
2026-05-04 05:50:30 +00:00
parent d635635577
commit a85390b498
2 changed files with 60 additions and 1 deletions
+40 -1
View File
@@ -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 = `
<div class="modal-header">
<h3>📷 ${t('scan.ai_identify')}</h3>
@@ -8781,6 +8815,7 @@ async function addSelectedSuggestions() {
let expiryStream = null;
async function scanExpiryWithAI() {
if (!_requireGemini()) return;
// Create modal for camera capture
document.getElementById('modal-content').innerHTML = `
<div class="modal-header">
@@ -9435,6 +9470,7 @@ let _recipeVariationCount = {}; // { 'pranzo': 0, 'cena': 1, ... }
let _rejectedRecipeIngredients = []; // ingredient names from previously rejected recipes
function openRecipeDialog() {
if (!_requireGemini()) return;
const meal = getMealType();
const settings = getSettings();
document.getElementById('recipe-overlay').style.display = 'flex';
@@ -10600,6 +10636,7 @@ function regenerateRecipe() {
}
async function generateRecipe() {
if (!_requireGemini()) return;
const meal = getSelectedMealType();
const persons = parseInt(document.getElementById('recipe-persons').value) || 1;
const settings = getSettings();
@@ -11719,6 +11756,8 @@ async function _initApp() {
// are taken into account before deciding which wizard steps to show.
let serverSettings = {};
try { serverSettings = await api('get_settings'); } catch(e) {}
_geminiAvailable = !!(serverSettings.gemini_key_set || serverSettings.gemini_key);
_updateGeminiButtonState();
const missing = _getMissingSetupSteps(serverSettings);
if (missing.length > 0) {
showSetupWizard(missing);