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
|
||||
# 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.
|
||||
|
||||
+25
-5
@@ -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'];
|
||||
|
||||
+31
-1
@@ -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 =====
|
||||
|
||||
Reference in New Issue
Block a user