From 47c26ffdc8630c0683073f2b20029e156da3b415 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Sat, 16 May 2026 16:09:49 +0000 Subject: [PATCH] =?UTF-8?q?v1.7.15=20=E2=80=94=20centralize=20all=20settin?= =?UTF-8?q?gs=20to=20server=20(.env=20+=20SQLite)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TTS: tts_engine, tts_rate, tts_pitch, tts_auth_header_name, tts_auth_header_value, tts_extra_fields now stored in .env and synced across devices via get_settings/save_settings - meal_plan: persisted to SQLite app_settings table on every edit (selectMealPlanType, resetMealPlan) and restored on startup via syncSettingsFromDB — all devices stay in sync - tts_voice: also synced to SQLite for best-effort cross-device restore - saveSettings() sends meal_plan + tts_voice to app_settings_save after env write - Remove deprecated SPESA_PROVIDER and SPESA_AI_PROMPT from .env - .env.example: full rewrite documenting all 30+ keys in labelled sections (AI, Shopping, TTS, Preferences, Appliances, Scale, Meal Plan, Screensaver, Prices, Security, Developer) --- .env.example | 111 +++++++++++++++++++++++++++++++++++++++-------- api/index.php | 30 ++++++++++--- assets/js/app.js | 32 +++++++++++++- 3 files changed, 150 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index dfb5881..3c7baeb 100644 --- a/.env.example +++ b/.env.example @@ -1,25 +1,102 @@ -# EverShelf - Configuration -# Copy this file to .env and fill in your values -# cp .env.example .env +# EverShelf — Configuration +# Copy this file to .env and fill in your values: +# cp .env.example .env +# +# All settings here can also be changed from the in-app Settings screen and +# will be written back to this file automatically. +# ───────────────────────────────────────────────────────────────────────────── -# Google Gemini AI API Key (required for AI features) -# Get one at: https://aistudio.google.com/app/apikey +# ── AI ──────────────────────────────────────────────────────────────────────── +# Google Gemini API key (required for AI features: expiry reading, recipe gen, …) +# Get one free at: https://aistudio.google.com/app/apikey GEMINI_API_KEY= -# Bring! Shopping List credentials (optional) -# Sign up at: https://www.getbring.com/ +# ── Shopping list (Bring!) ──────────────────────────────────────────────────── +# Credentials for the Bring! app (optional — app works without it) BRING_EMAIL= BRING_PASSWORD= -# TTS (Text-to-Speech) for cooking mode voice guidance (optional) -# Works with Home Assistant, or any HTTP endpoint that accepts text -TTS_URL= -TTS_TOKEN= -TTS_METHOD=POST -TTS_AUTH_TYPE=bearer -TTS_CONTENT_TYPE=application/json -TTS_PAYLOAD_KEY=message +# ── Text-to-Speech (TTS) ───────────────────────────────────────────────────── +# Works with Home Assistant, a local TTS server, or any HTTP endpoint. +# TTS_ENABLED: master switch (true/false) TTS_ENABLED=false +# TTS_URL: endpoint that receives the text payload +TTS_URL= +# TTS_TOKEN: Authorization token sent as Bearer header (or empty) +TTS_TOKEN= +# TTS_METHOD: HTTP method (POST or GET) +TTS_METHOD=POST +# TTS_AUTH_TYPE: how the token is sent (bearer | basic | none) +TTS_AUTH_TYPE=bearer +# TTS_CONTENT_TYPE: request Content-Type header +TTS_CONTENT_TYPE=application/json +# TTS_PAYLOAD_KEY: JSON key that carries the text (e.g. "message", "text") +TTS_PAYLOAD_KEY=message +# TTS_ENGINE: preferred browser TTS engine ('browser', 'server', 'custom') — optional +TTS_ENGINE= +# TTS_RATE / TTS_PITCH: speech rate and pitch multipliers (1 = normal) +TTS_RATE=1 +TTS_PITCH=1 +# TTS_AUTH_HEADER_NAME / VALUE: custom HTTP header for authentication (optional) +TTS_AUTH_HEADER_NAME= +TTS_AUTH_HEADER_VALUE= +# TTS_EXTRA_FIELDS: additional JSON fields as key=value pairs, comma-separated (optional) +TTS_EXTRA_FIELDS= -# GitHub Error Reporting: token is hardcoded in api/index.php (same for all clients). -# No .env entry needed — update GH_ISSUE_TOKEN constant in api/index.php to rotate. +# ── User preferences ───────────────────────────────────────────────────────── +# These mirror the toggle switches in the Settings screen. +DEFAULT_PERSONS=1 +PREF_VELOCE=false +PREF_POCAFAME=false +PREF_SCADENZE=true +PREF_HEALTHY=false +PREF_OPENED=true +PREF_ZEROWASTE=false +# Dietary restrictions shown to the AI (e.g. "vegetariano,senza glutine") +DIETARY= + +# ── Appliances ──────────────────────────────────────────────────────────────── +# Comma-separated list of appliances available in your kitchen. +# Used by the AI when generating recipes. +APPLIANCES=Forno,Microonde,Friggitrice ad aria,Pentola a pressione + +# ── Camera ─────────────────────────────────────────────────────────────────── +# Default camera for barcode scanning ('environment' = rear, 'user' = front) +CAMERA_FACING=environment + +# ── Smart Kitchen Scale ─────────────────────────────────────────────────────── +# SCALE_ENABLED: enables the scale integration +SCALE_ENABLED=false +# SCALE_GATEWAY_URL: address of the EverShelf Scale Gateway (Android app) +SCALE_GATEWAY_URL= + +# ── Meal Plan ──────────────────────────────────────────────────────────────── +# MEAL_PLAN_ENABLED: show the weekly meal planner tab in Settings +MEAL_PLAN_ENABLED=false + +# ── Screensaver (kiosk / tablet mode) ──────────────────────────────────────── +SCREENSAVER_ENABLED=false +# SCREENSAVER_TIMEOUT: inactivity seconds before screensaver activates (default 5 min) +SCREENSAVER_TIMEOUT=300 + +# ── Price estimates ─────────────────────────────────────────────────────────── +# PRICE_ENABLED: show AI-estimated price column on the shopping list +PRICE_ENABLED=false +# PRICE_COUNTRY: country used for price context (e.g. "Italia", "Germany") +PRICE_COUNTRY=Italia +# PRICE_CURRENCY: ISO 4217 currency code (e.g. EUR, USD, GBP) +PRICE_CURRENCY=EUR +# PRICE_UPDATE_MONTHS: how many months to cache a price before re-fetching (default 3) +PRICE_UPDATE_MONTHS=3 + +# ── Security ───────────────────────────────────────────────────────────────── +# SETTINGS_TOKEN: if set, the Settings screen requires this token to save changes. +# Leave empty to allow anyone with access to the server to change settings. +SETTINGS_TOKEN= + +# ── Developer / demo ───────────────────────────────────────────────────────── +# DEMO_MODE: when true, all write operations are blocked (for public demos) +DEMO_MODE=false + +# NOTE: GitHub error reporting uses a token hardcoded in api/index.php. +# To rotate it, update the GH_ISSUE_TOKEN constant there. diff --git a/api/index.php b/api/index.php index 743a586..7977f7b 100644 --- a/api/index.php +++ b/api/index.php @@ -2273,6 +2273,12 @@ function getServerSettings(): void { 'tts_content_type' => env('TTS_CONTENT_TYPE', 'application/json'), 'tts_payload_key' => env('TTS_PAYLOAD_KEY', 'message'), 'tts_enabled' => env('TTS_ENABLED', 'false') === 'true', + 'tts_engine' => env('TTS_ENGINE', ''), + 'tts_rate' => (float)env('TTS_RATE', '1'), + 'tts_pitch' => (float)env('TTS_PITCH', '1'), + 'tts_auth_header_name' => env('TTS_AUTH_HEADER_NAME', ''), + 'tts_auth_header_value' => env('TTS_AUTH_HEADER_VALUE', ''), + 'tts_extra_fields' => env('TTS_EXTRA_FIELDS', ''), // User preferences (now server-side) 'default_persons' => intval(env('DEFAULT_PERSONS', '1')), 'pref_veloce' => env('PREF_VELOCE', 'false') === 'true', @@ -2323,11 +2329,15 @@ function saveSettings(): void { 'tts_auth_type' => 'TTS_AUTH_TYPE', 'tts_content_type'=> 'TTS_CONTENT_TYPE', 'tts_payload_key' => 'TTS_PAYLOAD_KEY', - 'camera_facing' => 'CAMERA_FACING', - 'dietary' => 'DIETARY', - 'scale_gateway_url' => 'SCALE_GATEWAY_URL', - 'price_country' => 'PRICE_COUNTRY', - 'price_currency' => 'PRICE_CURRENCY', + 'camera_facing' => 'CAMERA_FACING', + 'dietary' => 'DIETARY', + 'scale_gateway_url' => 'SCALE_GATEWAY_URL', + 'price_country' => 'PRICE_COUNTRY', + 'price_currency' => 'PRICE_CURRENCY', + 'tts_engine' => 'TTS_ENGINE', + 'tts_auth_header_name' => 'TTS_AUTH_HEADER_NAME', + 'tts_auth_header_value' => 'TTS_AUTH_HEADER_VALUE', + 'tts_extra_fields' => 'TTS_EXTRA_FIELDS', ]; // Boolean keys $boolMap = [ @@ -2349,6 +2359,11 @@ function saveSettings(): void { 'screensaver_timeout' => 'SCREENSAVER_TIMEOUT', 'price_update_months' => 'PRICE_UPDATE_MONTHS', ]; + // Float keys + $floatMap = [ + 'tts_rate' => 'TTS_RATE', + 'tts_pitch' => 'TTS_PITCH', + ]; foreach ($keyMap as $inKey => $envKey) { if (array_key_exists($inKey, $input)) { @@ -2365,6 +2380,11 @@ function saveSettings(): void { $envVars[$envKey] = (string)intval($input[$inKey]); } } + foreach ($floatMap as $inKey => $envKey) { + if (array_key_exists($inKey, $input)) { + $envVars[$envKey] = (string)(float)$input[$inKey]; + } + } // Arrays stored as comma-separated if (array_key_exists('appliances', $input)) { $envVars['APPLIANCES'] = is_array($input['appliances']) ? implode(',', $input['appliances']) : (string)$input['appliances']; diff --git a/assets/js/app.js b/assets/js/app.js index 87c3b1e..6c3d4f3 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -2038,12 +2038,25 @@ async function syncSettingsFromDB() { // Primary: load from server .env (only when not already done via _applySyncedSettings) const serverSettings = await api('get_settings'); _applySyncedSettings(serverSettings); - // Also load review_confirmed from DB + // Also load review_confirmed, meal_plan, tts_voice from DB (cross-device shared) const res = await api('app_settings_get'); if (res.success && res.settings) { if (res.settings.review_confirmed) { _reviewConfirmedCache = res.settings.review_confirmed; } + // meal_plan is stored in SQLite app_settings so all devices stay in sync + if (res.settings.meal_plan) { + const s = getSettings(); + s.meal_plan = res.settings.meal_plan; + _settingsCache = s; + localStorage.setItem('evershelf_settings', JSON.stringify(s)); + if (document.getElementById('meal-plan-grid')) renderMealPlanEditor(); + } + // tts_voice preference (best-effort cross-device — falls back if voice unavailable) + if (res.settings.tts_voice) { + const s = getSettings(); + if (!s.tts_voice) { s.tts_voice = res.settings.tts_voice; _settingsCache = s; localStorage.setItem('evershelf_settings', JSON.stringify(s)); } + } } } catch(e) { /* offline, use local */ } } @@ -2064,6 +2077,7 @@ function _applySyncedSettings(serverSettings) { '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', + 'tts_engine','tts_rate','tts_pitch','tts_auth_header_name','tts_auth_header_value','tts_extra_fields', 'screensaver_enabled','screensaver_timeout', 'price_enabled','price_country','price_currency','price_update_months']; let changed = false; @@ -2814,6 +2828,12 @@ async function saveSettings() { tts_auth_type: s.tts_auth_type, tts_content_type: s.tts_content_type, tts_payload_key: s.tts_payload_key, + tts_engine: s.tts_engine || '', + tts_rate: s.tts_rate || 1, + tts_pitch: s.tts_pitch || 1, + tts_auth_header_name: s.tts_auth_header_name || '', + tts_auth_header_value: s.tts_auth_header_value || '', + tts_extra_fields: s.tts_extra_fields || '', price_enabled: s.price_enabled, price_country: s.price_country, price_currency: s.price_currency, @@ -2847,6 +2867,13 @@ async function saveSettings() { _updateGeminiButtonState(); } } catch(e) {} + // Persist meal_plan and tts_voice to SQLite for cross-device sync + try { + const appData = {}; + if (s.meal_plan) appData.meal_plan = s.meal_plan; + if (s.tts_voice) appData.tts_voice = s.tts_voice; + if (Object.keys(appData).length) await api('app_settings_save', {}, 'POST', { settings: appData }); + } catch(e) {} // Re-init screensaver watcher in case it was just enabled initInactivityWatcher(); } @@ -11470,6 +11497,8 @@ function selectMealPlanType(dow, slot, typeId) { saveSettingsToStorage(s); closeMealPlanPicker(); renderMealPlanEditor(); + // Persist to server for cross-device sync + api('app_settings_save', {}, 'POST', { settings: { meal_plan: s.meal_plan } }).catch(() => {}); } function resetMealPlan() { const s = getSettings(); @@ -11477,6 +11506,7 @@ function resetMealPlan() { saveSettingsToStorage(s); renderMealPlanEditor(); showToast(t('meal_plan.reset_success'), 'success'); + api('app_settings_save', {}, 'POST', { settings: { meal_plan: s.meal_plan } }).catch(() => {}); } // ===== RECIPE GENERATION =====