Compare commits

...

10 Commits

Author SHA1 Message Date
dependabot[bot] f72dc1fe54 ci: bump actions/setup-java from 4 to 5
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-16 16:29:33 +00:00
dadaloop82 68f7756e2c Merge develop → main: fix Brave TTS voice proxy crash (#63) 2026-05-16 16:28:10 +00:00
dadaloop82 b82b4d9d94 fix: guard against Brave user-script fake SpeechSynthesisVoice proxy (#63)
Brave on iOS injects a user-script that wraps SpeechSynthesisVoice objects
with a fake proxy. Accessing v.lang on the proxy threw 'undefined is not an
object (evaluating Object.getPrototypeOf(voice))'.

Fix: wrap the v.lang access in _initBrowserTtsVoices filter() inside its
own try/catch — bad proxies are silently discarded.
2026-05-16 16:28:06 +00:00
dependabot[bot] 91b4ecd670 ci: bump actions/checkout from 4 to 6 (#47)
chore: dependabot CI dependency update
2026-05-16 18:27:43 +02:00
dadaloop82 380fa8ee99 Merge develop → main: roadmap → GitHub Project 2026-05-16 16:21:26 +00:00
dadaloop82 89b8686f4f docs: replace static roadmap with link to GitHub Project 2026-05-16 16:21:22 +00:00
dadaloop82 b6aa07a1fd Merge develop → main: v1.7.15 settings centralization 2026-05-16 16:09:59 +00:00
dadaloop82 47c26ffdc8 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)
2026-05-16 16:09:49 +00:00
dadaloop82 12357db933 v1.7.15 — i18n audit, appliance translation, splash min 3s, demo GIF, decimal precision, gemini key fix 2026-05-16 15:48:53 +00:00
dadaloop82 6def94948b v1.7.15 — appliance translation, gemini key preserve on save
- _applianceDisplayName(): reverse lookup from canonical Italian names
  to settings.appliances.* i18n keys, with emoji stripping — appliance
  chips now show 'Air fryer', 'Heißluftfritteuse', etc. in EN/DE
- renderAppliances(): uses translated display name; remove button title
  uses t('btn.delete') instead of hardcoded 'Rimuovi'
- addApplianceQuick(): toast now uses t('toast.appliance_added') instead
  of hardcoded Italian ' aggiunto'
- saveSettings(): gemini_key in localStorage preserved when input is empty
  (key is not pre-populated for security — blank != user deleted the key)
- saveSettings(): _geminiAvailable re-synced from server after each save
  so recipe buttons immediately reflect correct state without page reload
2026-05-16 15:48:37 +00:00
9 changed files with 230 additions and 69 deletions
+94 -17
View File
@@ -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.
+2 -2
View File
@@ -17,10 +17,10 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
java-version: '17' java-version: '17'
distribution: 'temurin' distribution: 'temurin'
+2 -2
View File
@@ -20,10 +20,10 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
java-version: '17' java-version: '17'
distribution: 'temurin' distribution: 'temurin'
+6 -6
View File
@@ -11,7 +11,7 @@ jobs:
name: PHP Syntax Check name: PHP Syntax Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
@@ -27,7 +27,7 @@ jobs:
name: JavaScript Lint name: JavaScript Lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Check JS syntax - name: Check JS syntax
run: | run: |
@@ -37,7 +37,7 @@ jobs:
name: Docker Build Test name: Docker Build Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Build Docker image - name: Build Docker image
run: docker build -t evershelf-test . run: docker build -t evershelf-test .
@@ -53,7 +53,7 @@ jobs:
name: Validate Translation Files name: Validate Translation Files
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Validate JSON syntax - name: Validate JSON syntax
run: | run: |
@@ -99,7 +99,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout (full history) - name: Checkout (full history)
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
@@ -133,7 +133,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout main - name: Checkout main
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: main ref: main
fetch-depth: 0 fetch-depth: 0
+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
contents: read contents: read
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Build Docker image - name: Build Docker image
run: docker build -t evershelf:scan . run: docker build -t evershelf:scan .
@@ -51,7 +51,7 @@ jobs:
contents: read contents: read
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Run Trivy filesystem scanner - name: Run Trivy filesystem scanner
uses: aquasecurity/trivy-action@v0.36.0 uses: aquasecurity/trivy-action@v0.36.0
+2
View File
@@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Quantity decimal precision** — `qtyNum` in recipe/cooking ingredient buttons and `conf` fallback display in inventory cards now limited to 1 decimal place (was showing 7+ decimal places from raw AI output, e.g. `0.25353223 conf`). - **Quantity decimal precision** — `qtyNum` in recipe/cooking ingredient buttons and `conf` fallback display in inventory cards now limited to 1 decimal place (was showing 7+ decimal places from raw AI output, e.g. `0.25353223 conf`).
- **"Errore" / "Error" fallback strings** — All remaining Italian hardcoded `'Errore'` fallbacks in `showToast()` calls replaced with `t('error.generic')`. Italian fallback strings removed from buttons that already used `t()`. - **"Errore" / "Error" fallback strings** — All remaining Italian hardcoded `'Errore'` fallbacks in `showToast()` calls replaced with `t('error.generic')`. Italian fallback strings removed from buttons that already used `t()`.
- **README Italian phrases** — "La quantità è giusta (2 pz)", "🤖 Spiega", "Latte / Affettato / Panna da cucina", "Buon appetito!", "L'ho buttato" replaced with English equivalents in the README. - **README Italian phrases** — "La quantità è giusta (2 pz)", "🤖 Spiega", "Latte / Affettato / Panna da cucina", "Buon appetito!", "L'ho buttato" replaced with English equivalents in the README.
- **Appliance chips translated** — `renderAppliances()` now shows translated names (e.g. "Air fryer" in EN, "Heißluftfritteuse" in DE) for all known canonical Italian appliance names via `_applianceDisplayName()` lookup. `addApplianceQuick` toast no longer hardcoded Italian. Remove-button title translated.
- **Gemini API key not preserved on settings save** — `saveSettings()` was overwriting `s.gemini_key = ""` when the Gemini input field was empty (it is intentionally not pre-populated for security). Key is now preserved if the input is blank. `_geminiAvailable` is re-fetched from the server after every settings save so the recipe buttons reflect the real state immediately.
## [1.7.14] - 2026-05-16 ## [1.7.14] - 2026-05-16
+1 -29
View File
@@ -352,35 +352,7 @@ The application uses no build tools — edit files directly and refresh.
## 📋 Roadmap ## 📋 Roadmap
### High Priority Feature requests, bug reports and planned work are tracked in the [**EverShelf Roadmap**](https://github.com/users/dadaloop82/projects/2) GitHub Project.
- [ ] **Cooking mode — 3D wheel JS** — swipe navigation, gyroscope tilt, haptic feedback
- [ ] **Cooking mode — step timers** — auto-detect "X minutes" in recipe steps, countdown + alert
- [ ] **Push notifications** — daily expiry alerts via PWA Service Worker + VAPID
- [ ] **Quick search / quick-add bar** — always-visible search above the nav, PWA shortcuts
### Medium Priority
- [ ] **Receipt OCR → bulk add** — photo of receipt → Gemini Vision → auto-fill inventory
- [ ] **CSV/JSON export & import** — download/upload inventory from Settings
- [ ] **Custom storage locations** — user-defined locations beyond Fridge/Freezer/Pantry
- [ ] **Multi-user support** — PIN-based user distinction, action log with user label
- [ ] **AI optimal purchase prediction** — suggest "buy X units of Y within Z days"
- [ ] **Price history sparklines** — per-product price chart from the AI cache data
### Low Priority / Nice to Have
- [ ] **Dark mode** — CSS custom properties are already structured to support it
- [ ] **Full offline mode** — Service Worker cache to show inventory read-only when server is down
- [ ] **French & Spanish translations** (`fr.json`, `es.json`)
- [ ] **Swipe actions on inventory rows** — swipe left to use/discard, right to edit
- [ ] **PHP unit tests** — PHPUnit coverage for shelf-life, price calc, and key helpers
### Completed ✅
- ✅ AI price estimation in shopping list
- ✅ Server heartbeat + offline banner
- ✅ In-app bug reporter → automatic GitHub issue creation
- ✅ Cooking mode (start, steps, 3D wheel CSS)
- ✅ Kiosk ⚙️ Settings overlay button (replaces Android native button)
- ✅ Adaptive consumption anomaly detection
- ✅ CI/CD pipeline (PHP lint, JS lint, Docker build, Trivy security scan)
--- ---
+25 -5
View File
@@ -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'];
+96 -6
View File
@@ -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;
@@ -2599,6 +2613,53 @@ function _injectKioskOverlay() {
headerLeft.appendChild(wrap); headerLeft.appendChild(wrap);
} }
const _APPLIANCE_KEY_MAP = {
'forno': 'settings.appliances.oven',
'oven': 'settings.appliances.oven',
'backofen': 'settings.appliances.oven',
'microonde': 'settings.appliances.microwave',
'microwave': 'settings.appliances.microwave',
'mikrowelle': 'settings.appliances.microwave',
'friggitrice ad aria': 'settings.appliances.air_fryer',
'air fryer': 'settings.appliances.air_fryer',
'heißluftfritteuse': 'settings.appliances.air_fryer',
'macchina del pane': 'settings.appliances.bread_maker',
'macchina pane': 'settings.appliances.bread_maker',
'bread maker': 'settings.appliances.bread_maker',
'bread machine': 'settings.appliances.bread_maker',
'brotbackmaschine': 'settings.appliances.bread_maker',
'brotbackautomat': 'settings.appliances.bread_maker',
'bimby/moulinex cookeo': 'settings.appliances.bimby',
'moulinex cookeo': 'settings.appliances.bimby',
'bimby/cookeo': 'settings.appliances.bimby',
'bimby': 'settings.appliances.bimby',
'thermomix': 'settings.appliances.bimby',
'thermomix/cookeo': 'settings.appliances.bimby',
'planetaria': 'settings.appliances.mixer',
'stand mixer': 'settings.appliances.mixer',
'küchenmaschine': 'settings.appliances.mixer',
'vaporiera': 'settings.appliances.steamer',
'steamer': 'settings.appliances.steamer',
'dampfgarer': 'settings.appliances.steamer',
'pentola a pressione': 'settings.appliances.pressure_cooker',
'pentola pressione': 'settings.appliances.pressure_cooker',
'pressure cooker': 'settings.appliances.pressure_cooker',
'schnellkochtopf': 'settings.appliances.pressure_cooker',
'tostapane': 'settings.appliances.toaster',
'toaster': 'settings.appliances.toaster',
'frullatore/mixer': 'settings.appliances.blender',
'frullatore': 'settings.appliances.blender',
'blender': 'settings.appliances.blender',
'mixer': 'settings.appliances.blender',
};
function _applianceDisplayName(name) {
const key = _APPLIANCE_KEY_MAP[name.toLowerCase().trim()];
if (!key) return name;
// Strip leading emoji/symbols from the translated button label (e.g. "🔥 Oven" → "Oven")
return t(key).replace(/^[^\p{L}]+/u, '').trim() || name;
}
function renderAppliances(appliances) { function renderAppliances(appliances) {
const container = document.getElementById('appliances-list'); const container = document.getElementById('appliances-list');
if (!appliances || appliances.length === 0) { if (!appliances || appliances.length === 0) {
@@ -2607,8 +2668,8 @@ function renderAppliances(appliances) {
} }
container.innerHTML = appliances.map((a, i) => ` container.innerHTML = appliances.map((a, i) => `
<div class="appliance-item"> <div class="appliance-item">
<span>🔌 ${escapeHtml(a)}</span> <span>🔌 ${escapeHtml(_applianceDisplayName(a))}</span>
<button class="appliance-remove" onclick="removeAppliance(${i})" title="Rimuovi"></button> <button class="appliance-remove" onclick="removeAppliance(${i})" title="${t('btn.delete')}"></button>
</div> </div>
`).join(''); `).join('');
} }
@@ -2657,7 +2718,7 @@ function addApplianceQuick(name) {
s.appliances.push(name); s.appliances.push(name);
saveSettingsToStorage(s); saveSettingsToStorage(s);
renderAppliances(s.appliances); renderAppliances(s.appliances);
showToast(`${name} aggiunto`, 'success'); showToast(t('toast.appliance_added'), 'success');
} }
function removeAppliance(idx) { function removeAppliance(idx) {
@@ -2670,7 +2731,9 @@ function removeAppliance(idx) {
async function saveSettings() { async function saveSettings() {
const s = getSettings(); const s = getSettings();
s.gemini_key = document.getElementById('setting-gemini-key').value.trim(); // Only update gemini_key if user actually typed something; preserve existing key otherwise
const _newGeminiKey = document.getElementById('setting-gemini-key').value.trim();
if (_newGeminiKey) s.gemini_key = _newGeminiKey;
s.bring_email = document.getElementById('setting-bring-email').value.trim(); s.bring_email = document.getElementById('setting-bring-email').value.trim();
s.bring_password = document.getElementById('setting-bring-password').value.trim(); s.bring_password = document.getElementById('setting-bring-password').value.trim();
s.default_persons = parseInt(document.getElementById('setting-default-persons').value) || 1; s.default_persons = parseInt(document.getElementById('setting-default-persons').value) || 1;
@@ -2765,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,
@@ -2790,6 +2859,21 @@ async function saveSettings() {
statusEl.style.display = 'block'; statusEl.style.display = 'block';
setTimeout(() => statusEl.style.display = 'none', 4000); setTimeout(() => statusEl.style.display = 'none', 4000);
} }
// Re-sync _geminiAvailable after save (key may have been set/confirmed on server)
try {
const refreshed = await api('get_settings');
if (refreshed && refreshed.gemini_key_set !== undefined) {
_geminiAvailable = !!(refreshed.gemini_key_set);
_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 // Re-init screensaver watcher in case it was just enabled
initInactivityWatcher(); initInactivityWatcher();
} }
@@ -11413,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();
@@ -11420,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 =====
@@ -12576,7 +12663,10 @@ function _initBrowserTtsVoices(selectedVoice) {
const populate = () => { const populate = () => {
let voices = []; let voices = [];
try { try {
voices = (window.speechSynthesis.getVoices() || []).filter(v => v != null && v.lang); voices = (window.speechSynthesis.getVoices() || []).filter(v => {
try { return v != null && typeof v.lang === 'string' && v.lang.length > 0; }
catch (_) { return false; }
});
} catch (_) { return false; } } catch (_) { return false; }
if (!voices.length) return false; if (!voices.length) return false;
// Italian voices first, then others // Italian voices first, then others