v1.7.15 — centralize all settings to server (.env + SQLite)
- 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)
This commit is contained in:
+94
-17
@@ -1,25 +1,102 @@
|
|||||||
# EverShelf - Configuration
|
# EverShelf — Configuration
|
||||||
# Copy this file to .env and fill in your values
|
# Copy this file to .env and fill in your values:
|
||||||
# cp .env.example .env
|
# 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)
|
# ── AI ────────────────────────────────────────────────────────────────────────
|
||||||
# Get one at: https://aistudio.google.com/app/apikey
|
# 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=
|
GEMINI_API_KEY=
|
||||||
|
|
||||||
# Bring! Shopping List credentials (optional)
|
# ── Shopping list (Bring!) ────────────────────────────────────────────────────
|
||||||
# Sign up at: https://www.getbring.com/
|
# Credentials for the Bring! app (optional — app works without it)
|
||||||
BRING_EMAIL=
|
BRING_EMAIL=
|
||||||
BRING_PASSWORD=
|
BRING_PASSWORD=
|
||||||
|
|
||||||
# TTS (Text-to-Speech) for cooking mode voice guidance (optional)
|
# ── Text-to-Speech (TTS) ─────────────────────────────────────────────────────
|
||||||
# Works with Home Assistant, or any HTTP endpoint that accepts text
|
# Works with Home Assistant, a local TTS server, or any HTTP endpoint.
|
||||||
TTS_URL=
|
# TTS_ENABLED: master switch (true/false)
|
||||||
TTS_TOKEN=
|
|
||||||
TTS_METHOD=POST
|
|
||||||
TTS_AUTH_TYPE=bearer
|
|
||||||
TTS_CONTENT_TYPE=application/json
|
|
||||||
TTS_PAYLOAD_KEY=message
|
|
||||||
TTS_ENABLED=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).
|
# ── User preferences ─────────────────────────────────────────────────────────
|
||||||
# No .env entry needed — update GH_ISSUE_TOKEN constant in api/index.php to rotate.
|
# 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.
|
||||||
|
|||||||
+25
-5
@@ -2273,6 +2273,12 @@ function getServerSettings(): void {
|
|||||||
'tts_content_type' => env('TTS_CONTENT_TYPE', 'application/json'),
|
'tts_content_type' => env('TTS_CONTENT_TYPE', 'application/json'),
|
||||||
'tts_payload_key' => env('TTS_PAYLOAD_KEY', 'message'),
|
'tts_payload_key' => env('TTS_PAYLOAD_KEY', 'message'),
|
||||||
'tts_enabled' => env('TTS_ENABLED', 'false') === 'true',
|
'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)
|
// User preferences (now server-side)
|
||||||
'default_persons' => intval(env('DEFAULT_PERSONS', '1')),
|
'default_persons' => intval(env('DEFAULT_PERSONS', '1')),
|
||||||
'pref_veloce' => env('PREF_VELOCE', 'false') === 'true',
|
'pref_veloce' => env('PREF_VELOCE', 'false') === 'true',
|
||||||
@@ -2323,11 +2329,15 @@ function saveSettings(): void {
|
|||||||
'tts_auth_type' => 'TTS_AUTH_TYPE',
|
'tts_auth_type' => 'TTS_AUTH_TYPE',
|
||||||
'tts_content_type'=> 'TTS_CONTENT_TYPE',
|
'tts_content_type'=> 'TTS_CONTENT_TYPE',
|
||||||
'tts_payload_key' => 'TTS_PAYLOAD_KEY',
|
'tts_payload_key' => 'TTS_PAYLOAD_KEY',
|
||||||
'camera_facing' => 'CAMERA_FACING',
|
'camera_facing' => 'CAMERA_FACING',
|
||||||
'dietary' => 'DIETARY',
|
'dietary' => 'DIETARY',
|
||||||
'scale_gateway_url' => 'SCALE_GATEWAY_URL',
|
'scale_gateway_url' => 'SCALE_GATEWAY_URL',
|
||||||
'price_country' => 'PRICE_COUNTRY',
|
'price_country' => 'PRICE_COUNTRY',
|
||||||
'price_currency' => 'PRICE_CURRENCY',
|
'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
|
// Boolean keys
|
||||||
$boolMap = [
|
$boolMap = [
|
||||||
@@ -2349,6 +2359,11 @@ function saveSettings(): void {
|
|||||||
'screensaver_timeout' => 'SCREENSAVER_TIMEOUT',
|
'screensaver_timeout' => 'SCREENSAVER_TIMEOUT',
|
||||||
'price_update_months' => 'PRICE_UPDATE_MONTHS',
|
'price_update_months' => 'PRICE_UPDATE_MONTHS',
|
||||||
];
|
];
|
||||||
|
// Float keys
|
||||||
|
$floatMap = [
|
||||||
|
'tts_rate' => 'TTS_RATE',
|
||||||
|
'tts_pitch' => 'TTS_PITCH',
|
||||||
|
];
|
||||||
|
|
||||||
foreach ($keyMap as $inKey => $envKey) {
|
foreach ($keyMap as $inKey => $envKey) {
|
||||||
if (array_key_exists($inKey, $input)) {
|
if (array_key_exists($inKey, $input)) {
|
||||||
@@ -2365,6 +2380,11 @@ function saveSettings(): void {
|
|||||||
$envVars[$envKey] = (string)intval($input[$inKey]);
|
$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
|
// Arrays stored as comma-separated
|
||||||
if (array_key_exists('appliances', $input)) {
|
if (array_key_exists('appliances', $input)) {
|
||||||
$envVars['APPLIANCES'] = is_array($input['appliances']) ? implode(',', $input['appliances']) : (string)$input['appliances'];
|
$envVars['APPLIANCES'] = is_array($input['appliances']) ? implode(',', $input['appliances']) : (string)$input['appliances'];
|
||||||
|
|||||||
+31
-1
@@ -2038,12 +2038,25 @@ async function syncSettingsFromDB() {
|
|||||||
// Primary: load from server .env (only when not already done via _applySyncedSettings)
|
// Primary: load from server .env (only when not already done via _applySyncedSettings)
|
||||||
const serverSettings = await api('get_settings');
|
const serverSettings = await api('get_settings');
|
||||||
_applySyncedSettings(serverSettings);
|
_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');
|
const res = await api('app_settings_get');
|
||||||
if (res.success && res.settings) {
|
if (res.success && res.settings) {
|
||||||
if (res.settings.review_confirmed) {
|
if (res.settings.review_confirmed) {
|
||||||
_reviewConfirmedCache = 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 */ }
|
} catch(e) { /* offline, use local */ }
|
||||||
}
|
}
|
||||||
@@ -2064,6 +2077,7 @@ function _applySyncedSettings(serverSettings) {
|
|||||||
'camera_facing','scale_enabled','scale_gateway_url',
|
'camera_facing','scale_enabled','scale_gateway_url',
|
||||||
'meal_plan_enabled','tts_enabled','tts_url','tts_token',
|
'meal_plan_enabled','tts_enabled','tts_url','tts_token',
|
||||||
'tts_method','tts_auth_type','tts_content_type','tts_payload_key',
|
'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',
|
'screensaver_enabled','screensaver_timeout',
|
||||||
'price_enabled','price_country','price_currency','price_update_months'];
|
'price_enabled','price_country','price_currency','price_update_months'];
|
||||||
let changed = false;
|
let changed = false;
|
||||||
@@ -2814,6 +2828,12 @@ async function saveSettings() {
|
|||||||
tts_auth_type: s.tts_auth_type,
|
tts_auth_type: s.tts_auth_type,
|
||||||
tts_content_type: s.tts_content_type,
|
tts_content_type: s.tts_content_type,
|
||||||
tts_payload_key: s.tts_payload_key,
|
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_enabled: s.price_enabled,
|
||||||
price_country: s.price_country,
|
price_country: s.price_country,
|
||||||
price_currency: s.price_currency,
|
price_currency: s.price_currency,
|
||||||
@@ -2847,6 +2867,13 @@ async function saveSettings() {
|
|||||||
_updateGeminiButtonState();
|
_updateGeminiButtonState();
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} 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
|
// Re-init screensaver watcher in case it was just enabled
|
||||||
initInactivityWatcher();
|
initInactivityWatcher();
|
||||||
}
|
}
|
||||||
@@ -11470,6 +11497,8 @@ function selectMealPlanType(dow, slot, typeId) {
|
|||||||
saveSettingsToStorage(s);
|
saveSettingsToStorage(s);
|
||||||
closeMealPlanPicker();
|
closeMealPlanPicker();
|
||||||
renderMealPlanEditor();
|
renderMealPlanEditor();
|
||||||
|
// Persist to server for cross-device sync
|
||||||
|
api('app_settings_save', {}, 'POST', { settings: { meal_plan: s.meal_plan } }).catch(() => {});
|
||||||
}
|
}
|
||||||
function resetMealPlan() {
|
function resetMealPlan() {
|
||||||
const s = getSettings();
|
const s = getSettings();
|
||||||
@@ -11477,6 +11506,7 @@ function resetMealPlan() {
|
|||||||
saveSettingsToStorage(s);
|
saveSettingsToStorage(s);
|
||||||
renderMealPlanEditor();
|
renderMealPlanEditor();
|
||||||
showToast(t('meal_plan.reset_success'), 'success');
|
showToast(t('meal_plan.reset_success'), 'success');
|
||||||
|
api('app_settings_save', {}, 'POST', { settings: { meal_plan: s.meal_plan } }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== RECIPE GENERATION =====
|
// ===== RECIPE GENERATION =====
|
||||||
|
|||||||
Reference in New Issue
Block a user