Compare commits

...

111 Commits

Author SHA1 Message Date
dadaloop82 c7f3c95d75 release: v1.7.29 2026-05-29 06:34:50 +00:00
dadaloop82 a6f90a07e5 feat: buy-cycle consumption prediction for untracked products
Products like salt/spices that are never marked per-use now get
consumption rate estimated from the average time between restocks:
  avgCycleDays = (lastIn - firstIn) / (buyCount - 1)
  estimatedDaysLeft = avgCycleDays - daysSinceLastBuy

Requirements: buyCount >= 3, dailyRate == 0, avgCycle >= 7 days.
Appears in smart shopping list with reason 'Finisce tra ~Ngg (ciclo medio Mgg)'.
Also marks buy-cycle products as isRegular so stock checks apply.
2026-05-29 06:34:40 +00:00
dadaloop82 2d07001c5b release: v1.7.28 2026-05-29 06:02:53 +00:00
dadaloop82 faa55eda93 chore: CHANGELOG v1.7.28 2026-05-29 06:02:51 +00:00
dadaloop82 0b902d7c19 fix: issue reporter — local FP cache prevents duplicate issues
- Add _getFpCachePath(), _loadFpCache(), _saveFpCache() helpers
- Check data/reported_issue_fps.json before GitHub Search API
  (falls back to /tmp/ when data/ is not writable)
- Save new issue number to cache immediately after creation
- Apply 30-minute comment throttle per fingerprint
- Fall back to GitHub Search on first run / cache miss

Fixes root cause of ~50 duplicate issues (#134 duplicates #135-#183)
caused by GitHub Search API indexing delay.
2026-05-29 06:02:27 +00:00
dadaloop82 d80199e4f1 release: v1.7.27 2026-05-29 05:43:47 +00:00
dadaloop82 1637cc1020 feat: HA sensor enrichment, inventory edit guard, recipe ingredient fix, bread shelf-life
- HA sensor: expiring_list now includes full product details (location, brand,
  category, days_remaining, opened_at, vacuum_sealed, default_quantity, etc.)
- HA sensor: new expired_list attribute with full product details per expired item
- HA sensor: new low_stock_list attribute (items with quantity ≤ 1, full details)
- HA sensor: new sensor=product endpoint (?action=ha_sensor&sensor=product)
  with optional filters: &id=, &name=, &location=
- HA cron webhook: expiry alert items now carry full product details
- Inventory edit: confirm dialog when quantity exceeds unit-specific threshold
  (prevents data loss from unit-confusion typos, e.g. 183 conf instead of 0.183)
- Recipe AI: explicit rule against ingredient form substitution
  (fresh tomatoes ≠ passata, fresh milk ≠ UHT ≠ cream, etc.)
- Shelf-life: opened bread rules (piadina 2d, bauletto/pancarrè 4d, pane 3d)
- docs/wiki: HA page updated with new schema, examples, product endpoint

Closes #125
2026-05-29 05:40:25 +00:00
github-actions[bot] 904a398009 chore: auto-merge develop → main
Triggered by: bc39361 feat: barcode scan visual feedback + EAN checksum validation
2026-05-27 05:28:31 +00:00
dadaloop82 bc39361246 feat: barcode scan visual feedback + EAN checksum validation
- Add scan-status-bar overlay inside scanner viewport showing:
  - Active scan method (Native API / Quagga / Native + Quagga)
  - Scanning state: ready, scanning, partial read, invalid, confirmed
- Validate EAN-8/EAN-13/UPC checksums in Quagga path before confirming
  (native BarcodeDetector validates internally; Quagga can return false positives)
- Show 'invalid barcode, retrying' message with invalid code highlighted
- Reset invalid barcode confidence counter on invalid read so scanner retries
- Spawn parallel Quagga scan with 'combined scan active' status message
- Add 6 translation keys (scan.status_*) in all 5 language files
2026-05-27 05:26:47 +00:00
github-actions[bot] 7f173770fc chore: auto-merge develop → main
Triggered by: b83db76 fix: remove background/border from persons +/- buttons
2026-05-26 18:23:27 +00:00
dadaloop82 b83db76a8d fix: remove background/border from persons +/- buttons 2026-05-26 18:21:53 +00:00
github-actions[bot] cfd089a0a3 chore: auto-merge develop → main
Triggered by: ade121f fix: align recipe persons-ctrl inline, enlarge fav star
2026-05-26 18:20:23 +00:00
dadaloop82 ade121f43f fix: align recipe persons-ctrl inline, enlarge fav star 2026-05-26 18:18:42 +00:00
github-actions[bot] 2f665f777b chore: auto-merge develop → main
Triggered by: f46b12e feat: recipe favorites (#124), portion rescaler (#123), financial waste report (#117), macronutrient panel (#118)
2026-05-26 18:11:28 +00:00
dadaloop82 f46b12e3ad feat: recipe favorites (#124), portion rescaler (#123), financial waste report (#117), macronutrient panel (#118)
- #124: star toggle on recipe view + favorites shown first in archive with gold border
- #123: +/- persons buttons on recipe to scale ingredient quantities
- #117: wasted value in EUR displayed in monthly stats section
- #118: macronutrient breakdown panel (P/C/F/fiber bars) with 4th insight rotation phase
- DB: is_favorite column on recipes, nutriments_json on products (auto-migrated)
- OFF API: nutriments fields fetched and stored per product
- Translations: it/en/de/fr/es updated with new keys
2026-05-26 18:09:32 +00:00
github-actions[bot] a932d3de11 chore: auto-merge develop → main
Triggered by: 6120fad chore: update CHANGELOG for v1.7.26
2026-05-26 17:31:48 +00:00
dadaloop82 6120fad40b chore: update CHANGELOG for v1.7.26 2026-05-26 17:30:17 +00:00
github-actions[bot] 8ac6fec5a2 chore: auto-merge develop → main
Triggered by: 4f68925 fix: insight banner rotation 30s → 60s (1 minute per panel)
2026-05-26 17:29:50 +00:00
github-actions[bot] fe7587e9e4 chore: auto-merge develop → main
Triggered by: f4ea9e7 fix: banner rotation (30s interval, skip empty phases) + normalize OFF categories
2026-05-26 17:28:44 +00:00
dadaloop82 4f68925a7c fix: insight banner rotation 30s → 60s (1 minute per panel) 2026-05-26 17:28:22 +00:00
dadaloop82 f4ea9e74e6 fix: banner rotation (30s interval, skip empty phases) + normalize OFF categories
- _startInsightAlternation: 3_600_000ms → 30_000ms so rotation is actually
  visible without waiting an hour
- _applyInsightPhase: smart phase-skip instead of always falling back to waste;
  if current phase has no content, advance to the next non-empty phase
- PHP: add _normalizeCat() mapping OpenFoodFacts slug format ('en:dairies',
  'en:plant-based-foods-and-beverages', …) to Italian app categories; also
  re-aggregates counts after normalization so 'en:dairies' + 'en:milk' both
  count toward 'latticini' correctly
- JS _renderMonthlyStatsSection: guard t() fallback — if t() returns the key
  itself (not found), display a cleaned-up slug instead of the raw key
2026-05-26 17:26:57 +00:00
github-actions[bot] 8f217fd166 chore: auto-merge develop → main
Triggered by: b985247 feat: monthly stats panel in rotating insight banner (closes #100)
2026-05-26 17:21:45 +00:00
dadaloop82 b985247b95 feat: monthly stats panel in rotating insight banner (closes #100)
- Add PHP action 'monthly_stats' + getMonthlyStats() function:
  items consumed/added/wasted this month, trend vs prev month,
  top 5 categories and top 3 products by transaction count
- Expand insight rotation from 2 → 3 phases (waste → nutrition → monthly)
- Add _renderMonthlyStatsSection() following nutrition section styling:
  large indigo number, trend arrow with %, horizontal category bars
  animated on show, badges for added/wasted/top-product
- Add ms-* CSS classes (bar chart layout matching nutr-* design)
- Add stats_monthly translations in it/en/de/fr/es (10 keys each)
2026-05-26 17:19:54 +00:00
github-actions[bot] efbed479df chore: auto-merge develop → main
Triggered by: 695c23f feat: smart shopping extended horizon for staple items (closes #98)
2026-05-26 17:10:43 +00:00
dadaloop82 695c23fc21 feat: smart shopping extended horizon for staple items (closes #98)
Products used ≥ 4x/month (daily staples: latte, pane, uova) now appear in
the smart shopping list when stock will deplete within 28 days (instead of 14).
Products used ≥ 2x/month (weekly staples: yogurt, frutta, carne) appear
within 21 days. The existing ≥1.5x/month rule at 14 days is unchanged.

No manual pin or UI needed: frequency is detected automatically from
transaction history. justRestocked guard prevents noise for freshly
bought items.
2026-05-26 17:09:04 +00:00
github-actions[bot] 7a34406b07 chore: auto-merge develop → main
Triggered by: 50660f6 fix: TTS test ask user if heard + PHP 8 end() ref bug + DB migration guard for fresh volumes
2026-05-26 15:01:40 +00:00
dadaloop82 50660f634f fix: TTS test ask user if heard + PHP 8 end() ref bug + DB migration guard for fresh volumes
- app.js: TTS kiosk timeout 4s → 10s; fires interactive 'Hai sentito?' YES/NO
  prompt instead of showing error (TTS can take 6-8s; UtteranceProgressListener
  may not fire on all firmware); YES → success, NO → troubleshooting steps
- translations: add heard_question/heard_yes/heard_no/test_ok_kiosk/test_fail_steps
  to all 5 languages (it/en/de/fr/es) under settings.tts
- api/index.php: fix end() PHP 8.0+ reference error in _offFetchProduct()
  (categories_hierarchy stored in temp var before calling end())  (fixes #130)
- api/database.php: migrateDB() now checks sqlite_master for 'products' table;
  if missing, calls initializeDB() and returns — no ALTER on nonexistent table
  (fixes #133, covers #131)
- api/index.php: health_check db_row_count query guarded against missing
  inventory table (fixes #131)
2026-05-26 14:59:17 +00:00
github-actions[bot] fb06b42107 chore: auto-merge develop → main
Triggered by: c16067d fix(kiosk): scale retry restores nav buttons + settings link to webapp
2026-05-26 14:44:57 +00:00
dadaloop82 c16067d9e5 fix(kiosk): scale retry restores nav buttons + settings link to webapp
SetupActivity:
- btnTestRetry click handler: add step3NextButtons.visibility = VISIBLE
  so the Indietro/Avanti buttons reappear after pressing No-retry
  (previously they stayed hidden → user was stuck with no way to go back)
- onDisconnected(): always re-enable btnTestRetry so user is never stuck
  when scale drops unexpectedly before a weight reading arrives

SettingsActivity / activity_settings.xml:
- Add 'IMPOSTAZIONI AVANZATE' section explaining that HA, Gemini AI,
- Add '← Torna all'app per le impostazioni avanzate' button (finish())
2026-05-26 14:42:27 +00:00
github-actions[bot] 605d8590f6 chore: auto-merge develop → main
Triggered by: 149cff3 fix(kiosk): force STREAM_MUSIC for TTS + JS feedback callbacks
2026-05-26 14:04:23 +00:00
dadaloop82 149cff3ca5 fix(kiosk): force STREAM_MUSIC for TTS + JS feedback callbacks
Android TTS was silently failing because engine.speak() used the default
audio stream, which may be muted while the media stream is not.

Changes:
- KioskActivity: speak() now passes Bundle with KEY_PARAM_STREAM=STREAM_MUSIC
  so TTS plays on the same channel as beep/media audio
- KioskActivity: add UtteranceProgressListener that calls window._kioskTtsDone()
  or window._kioskTtsError(uid, code) back into the WebView on completion/failure
- app.js testTTS(): kiosk path now shows real result from Android callbacks
  (success/error) with a 4s fallback timeout showing actionable troubleshooting hints
2026-05-26 14:02:31 +00:00
github-actions[bot] ec7d172ed9 chore: auto-merge develop → main
Triggered by: 0479e34 ci: retry after GitHub CDN outage
2026-05-26 13:53:56 +00:00
dadaloop82 0479e34c7f ci: retry after GitHub CDN outage 2026-05-26 13:52:15 +00:00
dadaloop82 730efe4d87 ci: force Node.js 24 for JS actions (deadline June 2, 2026)
Add FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true env var to ci.yml,
security.yml and build-kiosk.yml to prevent action failures when
GitHub removes Node 20 runner support on 2026-06-02.

Also re-triggers CI to bypass transient 403 infrastructure failure
that hit the previous run (GitHub CDN outage).
2026-05-26 12:31:37 +00:00
dadaloop82 be3dceeebb feat: add separate sound beep test button + always show move modal on open
- Add testSound() function: plays AudioContext beep independently of TTS
- Add '🔔 Esegui Test Suono' button in TTS settings (above voice test btn)
- Add translations: settings.tts.test_sound_btn (it/en/de)
- Remove hasOtherLocs guard in showMoveAfterUseModal: always show
  location-choice modal when opening a package, regardless of whether
  the product already exists in other locations (e.g. peas only in pantry
  → user can now choose to move opened portion to fridge)
2026-05-26 12:17:18 +00:00
dadaloop82 875250626d fix: prefer offline voices in TTS; add beep + real diagnostics to test
_speakBrowser: voice fallback now prefers localService=true (offline)
voices over cloud voices. On Android Chrome, cloud voices fail silently
when no internet. New priority: local Italian → any Italian → local
any-lang → first available → lang-only.

testTTS (browser engine):
- Plays _playCookingTimerSound('done') beep FIRST (AudioContext, always
  works) so user can confirm audio hardware/volume is working
- Checks if only cloud voices exist → shows actionable error
- Builds utterance with onerror → shows error code on failure
- Adds onstart → shows actual voice name/lang/online status on success
- 2s watchdog: if onstart never fires → 'nessuna risposta' warning
- Shows '🔊 Beep + TTS in corso...' during the test
2026-05-26 12:11:52 +00:00
dadaloop82 245d007e29 fix: Chrome speechSynthesis paused-state bug + better TTS diagnostics
_speakBrowser: Chrome sets speechSynthesis.paused=true after tab is
backgrounded/minimized. Calling cancel() does NOT clear this state.
Adding resume() before speak() inside the setTimeout fixes silent
failure when user returns to the tab (e.g. switch app → back to browser).

testTTS: add diagnostic messages for:
- Android kiosk: check isTtsReady() and show actionable error if
  the native TTS engine hasn't initialized yet
- Web browser: check getVoices().length before speaking; if empty,
  show error asking user to install a voice pack in OS settings
  (instead of always showing ' Riproduzione in corso' even when silent)
2026-05-26 12:04:31 +00:00
github-actions[bot] 63a9f70f86 chore: auto-merge develop → main
Triggered by: 1a6e0c8 fix: robust browser TTS voice selection and Chrome cancel+speak workaround
2026-05-25 17:56:33 +00:00
dadaloop82 1a6e0c87ce fix: robust browser TTS voice selection and Chrome cancel+speak workaround
_speakBrowser() was silently failing in two scenarios:

1. Chrome cancel+speak bug: calling speechSynthesis.speak() immediately
   after cancel() is silently dropped in Chrome. Fixed by adding a 50ms
   setTimeout between cancel() and speak().

2. No voice fallback: when tts_voice preference is empty (never saved)
   and the browser has no default 'it-IT' voice, speech fails silently.
   Now tries: preferred by name → first Italian voice → first any voice
   → lang-only as last resort.

3. Async voice loading: Chrome/Android load voices asynchronously.
   When getVoices() returns empty, now waits for onvoiceschanged (with
   a 500ms safety timeout) before speaking.
2026-05-25 17:55:04 +00:00
github-actions[bot] 73f43cb296 chore: auto-merge develop → main
Triggered by: baed815 fix: translate CATEGORY_LABELS and #pf-category after i18n load
2026-05-25 17:43:55 +00:00
dadaloop82 baed815a48 fix: translate CATEGORY_LABELS and #pf-category after i18n load
CATEGORY_LABELS was built at parse time before translations were
available, causing raw keys like 'categories.verdura' to appear
in all category labels, dropdowns and inventory filters.

_applyI18nToLabels() now also:
- Rebuilds each CATEGORY_LABELS entry using the loaded i18n strings
  and the corresponding CATEGORY_ICONS icon prefix
- Regenerates the static #pf-category <select> options so the
  product-add form also shows translated category names
2026-05-25 17:42:10 +00:00
github-actions[bot] 8aa934f5ca chore: auto-merge develop → main
Triggered by: 83b5eb3 fix: browser TTS ignored when HA TTS configured; wrong ha_hint i18n key
2026-05-25 10:38:53 +00:00
dadaloop82 83b5eb3063 fix: browser TTS ignored when HA TTS configured; wrong ha_hint i18n key
- speakCookingStep: respect tts_engine='browser' explicitly; if user
  chose browser engine, always use _speakBrowser regardless of HA.
  Also add per-engine try/catch so HA or server TTS failures fall back
  to browser TTS instead of silently doing nothing.
- index.html: fix data-i18n='settings.tts.ha_hint' → 'settings.ha.ha_hint'
  (key lived in settings.ha not settings.tts)
2026-05-25 10:37:17 +00:00
github-actions[bot] 59c6f9d76c chore: auto-merge develop → main
Triggered by: bac9485 fix: hide ingredients panel in cooking mode
2026-05-25 10:21:51 +00:00
dadaloop82 bac9485e4e fix: hide ingredients panel in cooking mode
Ingredients are already shown (with use buttons) in the recipe
result screen. In cooking mode only steps should appear.
2026-05-25 10:20:17 +00:00
github-actions[bot] 11178af001 chore: auto-merge develop → main
Triggered by: 4e4a736 feat: ask replace vs save-to-archive on regenerate recipe
2026-05-25 10:13:43 +00:00
dadaloop82 4e4a736dba feat: ask replace vs save-to-archive on regenerate recipe
When user clicks 'Generate another', show a choice:
- Replace (discard current, generate new) — former behavior
- Save to archive & generate new — saves current recipe first

All 5 languages (it/en/de/fr/es) with regen_choice_title,
regen_replace, regen_save_new keys.
2026-05-25 10:11:56 +00:00
dadaloop82 52afdd6bfa fix(recipes): steps shown as raw JSON when AI uses instruction/appliance_function objects
- _stepStr: parse JSON-string steps; handle s.instruction key (backward-compat with already-saved recipes)
- _stepAppliance: new helper to extract appliance_function hint; returns null for 'Nessuno'/'None'
- renderRecipe steps list: shows appliance badge inline after step text when present
- CSS: .recipe-step-appliance badge (green chip, dark-mode variant)
- Prompt (both generateRecipe + generateRecipeStream): rule 9/10 explicitly forbids step objects;
  appliance info must be embedded in the step text string directly
2026-05-25 10:01:10 +00:00
github-actions[bot] 3d27433eb3 chore: auto-merge develop → main
Triggered by: eddb622 feat(offline): full offline mode — cache sync, write queue, startup recovery
2026-05-25 09:27:09 +00:00
dadaloop82 eddb622c85 feat(offline): full offline mode — cache sync, write queue, startup recovery
- Full-screen network error overlay (z-index 300000, above screensaver)
- 'Continue offline' button after 3s, auto-enter after 8s
- Inventory + settings synced to localStorage at startup (during health check)
- inventory_summary and stats computed from local cache while offline
- Write queue (add/use/update/delete): optimistic UI + sync on reconnect
- Pending ops survive page refresh — detected and re-synced at next startup
- Buffered remoteLog/reportError flushed to server (GitHub issues) on restore
- AI/network sections hidden in offline mode (CSS body.offline-mode)
- Banner: pulsing dot while loading cache, item count when ready
- Broken external images replaced with grey SVG placeholder
- Fix: opened items marked is_edible:true offline (was flooding banner)
- Fix: _semverGt() prevents update badge for older GitHub releases
- Bump version to v1.7.25
2026-05-25 09:05:19 +00:00
github-actions[bot] 95c20adbbd chore: auto-merge develop → main
Triggered by: 6fa2e4d docs: HA integration section first in README, with HACS buttons and full feature table
2026-05-23 20:43:02 +00:00
dadaloop82 6fa2e4d830 docs: HA integration section first in README, with HACS buttons and full feature table
- Replaces the old 'New' callout with a proper dedicated section at the top of Features
- Shows all 16 sensors, 6 binary sensors, 5 buttons, todo, calendar, text, 6 services
- HACS and config_flow_start my.home-assistant.io badge buttons

Also: fix dark mode not persisting — _setThemeMode now saves immediately to server .env
2026-05-23 20:41:29 +00:00
github-actions[bot] 6ff1dfe0cc chore: auto-merge develop → main
Triggered by: 43e0ac9 feat(ha): v1.1.0 backend — haCalendar, haSuggestRecipe, haRefreshPrices, haClearExpired + enriched haInventorySensor
2026-05-23 20:24:46 +00:00
dadaloop82 43e0ac9da3 feat(ha): v1.1.0 backend — haCalendar, haSuggestRecipe, haRefreshPrices, haClearExpired + enriched haInventorySensor
New endpoints:
- ha_calendar: returns all expiry dates as calendar events
- ha_suggest_recipe: AI recipe suggestion from expiring items (Gemini)
- ha_refresh_prices: recompute shopping total from price cache only
- ha_clear_expired: delete zero-stock expired rows

haInventorySensor now returns:
- items_dispensa, items_frigo, items_freezer, items_other
- low_stock_items, zero_stock_items
- ai_calls_month, last_backup_at
- days_to_next_expiry, next_expiry_name, next_expiry_date
- bring_connected, shopping_total, price_tracking_enabled, price_currency
2026-05-23 20:22:51 +00:00
github-actions[bot] 1ce32cb5f0 chore: auto-merge develop → main
Triggered by: d75cde7 feat(ha): add expiring_today, next_expiry_name/date, expires_today flag
2026-05-23 20:09:32 +00:00
dadaloop82 d75cde7eb6 feat(ha): add expiring_today, next_expiry_name/date, expires_today flag 2026-05-23 20:07:54 +00:00
github-actions[bot] 43fe1c7bb5 chore: auto-merge develop → main
Triggered by: b2c87ae feat(ha): enrich ha_sensor with opened_items, shopping_total, price_tracking, expiring_3d
2026-05-23 20:00:36 +00:00
dadaloop82 b2c87ae343 feat(ha): enrich ha_sensor with opened_items, shopping_total, price_tracking, expiring_3d 2026-05-23 19:58:49 +00:00
github-actions[bot] fbdae35516 chore: auto-merge develop → main
Triggered by: d9ebc51 fix: ha_sensor JOIN products for name/unit columns (was HTTP 500)
2026-05-23 14:39:37 +00:00
dadaloop82 d9ebc51e71 fix: ha_sensor JOIN products for name/unit columns (was HTTP 500) 2026-05-23 14:38:01 +00:00
github-actions[bot] 56ca58bc18 chore: auto-merge develop → main
Triggered by: b2e0f6d feat: add ha_info, ha_shopping_items endpoints and avahi mDNS service file for HACS integration
2026-05-23 13:25:19 +00:00
dadaloop82 b2e0f6d683 feat: add ha_info, ha_shopping_items endpoints and avahi mDNS service file for HACS integration
- api/index.php: new haGetInfo() endpoint (unique_id, version, instance, items count)
- api/index.php: new haGetShoppingItems() endpoint (Bring! + internal shopping list)
- api/index.php: haInventorySensor() now accepts ?expiry_days=N query param
- api/cron_smart_shopping.php: auto-register avahi mDNS service if avahi-daemon present
- docker/avahi-evershelf.xml: Zeroconf _evershelf._tcp service declaration
- .env.example: add INSTANCE_NAME variable (used by HA integration for device label)
2026-05-23 13:23:28 +00:00
github-actions[bot] ddb9bd9f75 chore: auto-merge develop → main
Triggered by: 965a672 feat: full Home Assistant integration
2026-05-23 12:30:02 +00:00
dadaloop82 965a672abe feat: full Home Assistant integration
- PHP: _fireHaWebhook(), _sendHaNotify(), haInventorySensor(), haTestConnection()
- PHP: ha_sensor + ha_test routing actions
- PHP: getServerSettings() exposes ha_token (consistent with tts_token)
- PHP: saveSettings() handles all HA_* env keys (url, token, tts_entity, webhook_id, events, notify_service, expiry_days)
- PHP: bringAddItems(), shoppingAdd(), updateInventory() fire shopping_add / stock_update webhooks
- Cron: daily HA expiry/expired webhook + push notify with flag-file guard
- HTML: 🏠 Settings tab button + full HA panel (connection, TTS, webhook, notify, sensor cards)
- JS: serverKeys + loadSettingsUI extended with HA fields
- JS: _applyHaSettingsUI(), _loadHaTab(), _renderHaSensorYaml()
- JS: onHaEnabledChange(), testHaConnection(), applyHaTtsPreset()
- JS: saveHaSettings(), copyHaSensorYaml(), showHaWebhookHelp()
- JS: _buildHaTtsRequest() for HA media_player TTS
- JS: speakCookingStep() now supports HA TTS as first-priority path
- JS: onTtsEngineChange() fixed to show server section for both 'server' and 'custom'
- Translations: settings.ha.* (52 keys) in all 5 languages (it/en/de/fr/es)
- .env.example: HA_ENABLED/URL/TOKEN/TTS_ENTITY/WEBHOOK_ID/EVENTS/NOTIFY_SERVICE/EXPIRY_DAYS
- docs/wiki/Home-Assistant.md: new wiki page (REST sensors, webhooks, TTS, push notify, troubleshooting)
- README: HA integration highlighted as first feature block
2026-05-23 12:28:09 +00:00
github-actions[bot] 7249daa8eb chore: auto-merge develop → main
Triggered by: ec53f75 docs: update README
2026-05-23 12:11:11 +00:00
dadaloop82 ec53f7529c docs: update README 2026-05-23 12:09:37 +00:00
github-actions[bot] 1074dff87d chore: auto-merge develop → main
Triggered by: 3989d11 fix: step.replace is not a function when Gemini returns steps as objects
2026-05-23 11:57:33 +00:00
dadaloop82 3989d11094 fix: step.replace is not a function when Gemini returns steps as objects
- PHP generateRecipeStream: normalize recipe.steps to plain strings after
  parsing Gemini JSON (handles [{text:'...'}, ...] objects gracefully)
- JS: add _stepStr(s) helper near cooking mode — safely extracts text from
  a step regardless of type (string or object {text/description/step key})
  and strips leading 'Passo N:' prefix in one place
- JS: replace all 7 manual step.replace(/^Passo.../) calls with _stepStr()
  across renderRecipe, renderCookingStep, startCookingMode, replayCookingTTS,
  toggleCookingTTS, navigateCookingStep — no more crash if Gemini schema drifts
2026-05-23 11:55:55 +00:00
github-actions[bot] b010ced1a6 chore: auto-merge develop → main
Triggered by: cc0fa09 fix: recipe errors now show specific cause instead of generic 'connection error'
2026-05-23 11:47:00 +00:00
dadaloop82 cc0fa09219 fix: recipe errors now show specific cause instead of generic 'connection error'
- PHP generateRecipeStream: wrap entire body in try/catch(\Throwable) to catch
  any PHP fatal/exception mid-stream and send it as a proper SSE error event
- PHP: curl timeout raised 60s→90s; capture curl errno/errmsg on failure
- PHP: HTTP error messages now include a human-readable status label
  (e.g. 'Quota API esaurita (429)', 'Nessuna risposta da Gemini (cURL: ...)')
- JS catch block: show err.message alongside error.connection so the actual
  JS network error (NetworkError, AbortError, etc.) is visible
- JS no-recipe+no-error path: show recipes.stream_interrupted instead of
  generic error.connection
- Translation: added recipes.stream_interrupted in it/en/de
2026-05-23 11:45:26 +00:00
github-actions[bot] c0a076749e chore: auto-merge develop → main
Triggered by: 6a41b53 feat: shopping list pantry hints, barcode multi-API fallback (OPF/beauty/Gemini), README disclaimer
2026-05-23 09:54:56 +00:00
dadaloop82 6a41b53174 feat: shopping list pantry hints, barcode multi-API fallback (OPF/beauty/Gemini), README disclaimer
- Shopping list: each item now shows 'Hai già Xg in dispensa' for same-family inventory stock
  - Lazy-loads inventory once per shopping page visit (_getShoppingInventoryCache)
  - Matches by first significant token (same logic as related-stock on action page)
  - Green hint below item badges, dark-mode aware (.shopping-pantry-hint)
- Barcode lookup: added Open Products Facts + Open Beauty Facts as step 3;
  Gemini AI (_barcodeLookupGemini) as final step 4 fallback
- Added stockForName PHP endpoint (stock_for_name action) for future use
- Restored missing function signatures for _offFetchProduct() and saveProduct()
  that were accidentally lost when stockForName was added in a previous session
- Translation: added shopping.pantry_hint in it/en/de
2026-05-23 09:53:17 +00:00
github-actions[bot] 1d04236bc0 chore: auto-merge develop → main
Triggered by: 561c6e9 ci: fix auto-merge — clear checkout extraheader so WORKFLOW_PAT actually reaches git push
2026-05-23 09:37:26 +00:00
dadaloop82 561c6e9809 ci: fix auto-merge — clear checkout extraheader so WORKFLOW_PAT actually reaches git push 2026-05-23 09:35:53 +00:00
dadaloop82 6857c20893 ci: fix checkout to use github.token, WORKFLOW_PAT only for push 2026-05-23 09:23:48 +00:00
dadaloop82 964de98203 ci: test auto-merge with WORKFLOW_PAT secret 2026-05-23 09:18:03 +00:00
dadaloop82 e28a6e4e39 ci: use WORKFLOW_PAT for auto-merge to allow pushing workflow file changes 2026-05-23 08:37:30 +00:00
dadaloop82 fd9e2471e0 fix: remove invalid 'workflows' permission from ci.yml 2026-05-23 08:30:57 +00:00
dadaloop82 3c8a9693b2 fix: isSuspiciousDefaultQty — use package_unit threshold for pz+g/ml products 2026-05-23 08:28:58 +00:00
dadaloop82 b38bdc45f5 fix: add workflows:write permission to auto-merge job 2026-05-23 08:27:04 +00:00
dadaloop82 83a0df272a fix: CI auto-merge push auth — set remote URL with GITHUB_TOKEN before push 2026-05-23 08:21:26 +00:00
dadaloop82 6320b575e0 v1.7.25 — partial throw from banner, barcode fallback, related stock, Bring! re-add fix
- Fix: Bring! items re-appearing after manual removal (missing _markBringPurchased call in removeBringItem / confirmShoppingItemFound; autoAddCriticalItems now respects blocklist for qty=0 items)
- Fix: barcode false 'not found' — new _offFetchProduct() helper tries UPC-A↔EAN-13 candidates × 2 locales with auto-retry; UPCItemDB fallback also iterates candidates
- Fix: bannerThrowAway() now opens partial-throw modal (location + qty input + throw-all button) instead of immediately discarding everything
- Add: related stock card on action page — shows same-family inventory items when scanning a branded product
2026-05-23 08:17:20 +00:00
github-actions[bot] 8ccd218c5a chore: auto-merge develop → main
Triggered by: 5c1afaa feat: redesign action-page hero card (match use-page style)
2026-05-22 21:25:12 +00:00
dadaloop82 5c1afaaaf5 feat: redesign action-page hero card (match use-page style) 2026-05-22 21:23:18 +00:00
github-actions[bot] 6245b15420 chore: auto-merge develop → main
Triggered by: 02f673a fix: include dark_mode in save_settings payload (was never written to .env)
2026-05-22 21:15:51 +00:00
dadaloop82 02f673a164 fix: include dark_mode in save_settings payload (was never written to .env) 2026-05-22 21:14:16 +00:00
github-actions[bot] 61bb1b5552 chore: auto-merge develop → main
Triggered by: cbf4bd5 feat: show expiry + qty pills on use-page product hero card; redesign card with accent border
2026-05-22 21:11:53 +00:00
dadaloop82 cbf4bd54da feat: show expiry + qty pills on use-page product hero card; redesign card with accent border 2026-05-22 21:10:19 +00:00
github-actions[bot] 1cdbdb3b25 chore: auto-merge develop → main
Triggered by: 837d62c fix: add missing _ensureAudioUnlocked() definition; use shared AudioContext in _playCookingTimerSound
2026-05-22 13:15:59 +00:00
dadaloop82 837d62c335 fix: add missing _ensureAudioUnlocked() definition; use shared AudioContext in _playCookingTimerSound 2026-05-22 13:14:21 +00:00
github-actions[bot] fa36ba83bf chore: auto-merge develop → main
Triggered by: 1efeaf9 fix: skip use-all confirm when only one item row and no open packages
2026-05-22 04:51:02 +00:00
dadaloop82 1efeaf9236 fix: skip use-all confirm when only one item row and no open packages 2026-05-22 04:49:27 +00:00
dadaloop82 573bcd1102 Merge branch 'main' of github-evershelf:dadaloop82/EverShelf 2026-05-21 18:45:46 +00:00
github-actions[bot] d3eb82eee2 chore: auto-merge develop → main
Triggered by: 98426bf fix: dark_mode persisted in server .env (not localStorage) — add to saveSettings, getServerSettings, applySyncedSettings
2026-05-21 18:42:49 +00:00
github-actions[bot] 264b1f648e chore: auto-merge develop → main
Triggered by: b89df96 fix: dark mode resets to auto on reload — bootstrap dark_mode from localStorage in getSettings()
2026-05-21 18:35:19 +00:00
github-actions[bot] 5e34bc90b3 chore: auto-merge develop → main
Triggered by: 3b100df fix: cooking timer sound/TTS — shared pre-unlocked AudioContext; always speak on alarm regardless of TTS toggle; kiosk bridge TTS check
2026-05-21 18:20:24 +00:00
github-actions[bot] 2ecb3cbac6 chore: auto-merge develop → main
Triggered by: c2004fd fix: scale use — auto-switch conf→sub (g/ml) when scale is active; show scale btn for conf+weight products
2026-05-21 06:02:55 +00:00
github-actions[bot] fba0947945 chore: auto-merge develop → main
Triggered by: 3a1f6cf fix: cooking timer — AudioContext.resume() on mobile; always play beep; show done card 3s before dismiss
2026-05-20 18:32:10 +00:00
github-actions[bot] 37fb522e8b chore: auto-merge develop → main
Triggered by: 66f5a03 fix: wrap updateInventory DB writes in a transaction to prevent concurrent lock errors (#109 #110)
2026-05-20 15:40:15 +00:00
github-actions[bot] 47197d0d66 chore: auto-merge develop → main
Triggered by: 1496216 fix: depleted items urgency based on usesPerMonth only (not recency)
2026-05-20 13:47:41 +00:00
github-actions[bot] b5a6daa557 chore: auto-merge develop → main
Triggered by: ccc2f89 fix: depleted items urgency — use buyCount/useCount from internal history to assign medium/low
2026-05-20 13:42:45 +00:00
github-actions[bot] 9e80915a61 chore: auto-merge develop → main
Triggered by: 7b60f1d fix: 0.5 conf use page (default conf mode + fraction btns); depleted items always in shopping; conf decimals in history log
2026-05-20 13:37:35 +00:00
github-actions[bot] 7019160704 chore: auto-merge develop → main
Triggered by: ac8b5ac fix: restore Bring! health check; token warning only when truly invalid
2026-05-19 17:27:50 +00:00
github-actions[bot] 34df755ba3 chore: auto-merge develop → main
Triggered by: 87eac17 fix: recipe quantities for conf+weight; move modal remembers location
2026-05-19 16:53:15 +00:00
github-actions[bot] ef15f3536c chore: auto-merge develop → main
Triggered by: f77b325 refactor: remove localStorage for settings — all settings server-centralised
2026-05-19 16:42:55 +00:00
github-actions[bot] 5ad24ed73b chore: auto-merge develop → main
Triggered by: 84934c1 fix: sync shopping settings across clients (serverKeys was missing shopping_* keys)
2026-05-19 16:37:41 +00:00
github-actions[bot] dd0625b253 chore: auto-merge develop → main
Triggered by: fa0442e feat: native shopping list — decouple from Bring! (#105)
2026-05-19 16:07:33 +00:00
github-actions[bot] a85414d790 chore: auto-merge develop → main
Triggered by: c07439f docs: add Contributing + Community section to README
2026-05-18 19:12:04 +00:00
github-actions[bot] 8f6934485a chore: auto-merge develop → main
Triggered by: d7aadff fix(kiosk): target SDK 35 + setInstallReason for Android 16 compatibility
2026-05-18 19:06:15 +00:00
github-actions[bot] d8aff8ac04 chore: auto-merge develop → main
Triggered by: 7364e75 feat: Google Drive OAuth via http://localhost redirect (no public domain required)
2026-05-18 18:44:00 +00:00
github-actions[bot] ff25307662 chore: auto-merge develop → main
Triggered by: 4515ff7 i18n: replace all hardcoded Italian strings with English
2026-05-18 07:34:28 +00:00
24 changed files with 11783 additions and 6918 deletions
+27
View File
@@ -129,6 +129,33 @@ GDRIVE_RETENTION_DAYS=30
# Leave empty to allow anyone with access to the server to change settings.
SETTINGS_TOKEN=
# INSTANCE_NAME: display name for this EverShelf instance (used by the HA integration
# for Zeroconf discovery label and device name in Home Assistant).
# Defaults to the server hostname if left empty.
INSTANCE_NAME=
# ── Home Assistant Integration ────────────────────────────────────────────────
# All HA settings can also be configured from the Settings → 🏠 tab.
#
# HA_ENABLED: master switch for all HA features (webhooks, TTS, sensors)
HA_ENABLED=false
# HA_URL: base URL of your HA instance — no trailing slash
# Examples: http://homeassistant.local:8123 or http://192.168.1.50:8123
HA_URL=
# HA_TOKEN: Long-Lived Access Token (HA Profile → Security → Long-Lived Access Tokens)
HA_TOKEN=
# HA_TTS_ENTITY: media_player entity for recipe step TTS (e.g. media_player.living_room)
HA_TTS_ENTITY=
# HA_WEBHOOK_ID: ID of an HA automation's Webhook trigger
HA_WEBHOOK_ID=
# HA_WEBHOOK_EVENTS: comma-separated events to fire webhooks for
# Available: expiry, shopping_add, stock_update, barcode_scan
HA_WEBHOOK_EVENTS=expiry,shopping_add,stock_update
# HA_NOTIFY_SERVICE: HA notify service for push alerts (e.g. notify.mobile_app_my_phone)
HA_NOTIFY_SERVICE=
# HA_EXPIRY_DAYS: days before expiry to trigger expiry alert (default 3)
HA_EXPIRY_DAYS=3
# ── Developer / demo ─────────────────────────────────────────────────────────
# DEMO_MODE: when true, all write operations are blocked (for public demos)
DEMO_MODE=false
+3
View File
@@ -1,5 +1,8 @@
name: Build & Release Kiosk APK
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
on:
push:
branches: [main]
+35 -1
View File
@@ -6,6 +6,9 @@ on:
pull_request:
branches: [main]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
lint-php:
name: PHP Syntax Check
@@ -102,7 +105,9 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
# Always use the built-in GITHUB_TOKEN for checkout (read-only fetch).
# WORKFLOW_PAT is only needed for the push step below.
token: ${{ github.token }}
- name: Configure git bot identity
run: |
@@ -111,6 +116,15 @@ jobs:
- name: Merge develop → main
run: |
# ── ROOT CAUSE FIX ──────────────────────────────────────────────────
# actions/checkout writes an http.extraheader (AUTHORIZATION: basic …)
# that silently overrides any credentials embedded in git remote URLs.
# We must clear it BEFORE setting the remote URL with WORKFLOW_PAT,
# otherwise GITHUB_TOKEN is always used for the push and workflow-file
# changes are rejected.
# ────────────────────────────────────────────────────────────────────
git config --local --unset-all http."https://github.com/".extraheader 2>/dev/null || true
LAST=$(git log --oneline -1 origin/develop)
git checkout main
git pull --ff-only origin main
@@ -118,6 +132,26 @@ jobs:
-m "chore: auto-merge develop → main
Triggered by: $LAST"
# ── PUSH STRATEGY ───────────────────────────────────────────────────
# Priority 1: WORKFLOW_PAT (classic PAT, repo+workflow scopes)
# → can push workflow file changes; set as a repo secret.
# Priority 2: GITHUB_TOKEN fallback
# → cannot push workflow files; strip them from the merge commit.
# ────────────────────────────────────────────────────────────────────
PUSH_TOKEN="${{ secrets.WORKFLOW_PAT }}"
if [ -z "$PUSH_TOKEN" ]; then
WF=$(git diff --name-only origin/main -- .github/workflows/ 2>/dev/null || echo "")
if [ -n "$WF" ]; then
echo "::warning::WORKFLOW_PAT not set — stripping workflow changes from merge commit:"
echo "$WF"
git checkout origin/main -- .github/workflows/
git diff --cached --quiet || git commit --amend --no-edit
fi
PUSH_TOKEN="${{ github.token }}"
fi
git remote set-url origin "https://x-access-token:${PUSH_TOKEN}@github.com/${{ github.repository }}.git"
git push origin main
# ── Auto-create GitHub Release on main ───────────────────────────────────
+3
View File
@@ -1,5 +1,8 @@
name: Security Scan (Trivy)
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
on:
push:
branches: [main, develop]
+57
View File
@@ -11,6 +11,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Recipe scraps tips** — During cooking steps, detect "waste" generated (peels, cores, bones, eggshells, coffee grounds, citrus zest, etc.) and surface AI-powered tips on how to reuse them (compost, natural cleaner, broth, candied peel, etc.). Could be shown as an optional collapsible hint card below the step that generates the scrap.
## [1.7.29] - 2026-05-29
### Added
- **Buy-cycle consumption prediction** — Products that are never tracked per-use (salt, spices, cleaning supplies, etc.) now use the average time between restocks as a proxy for consumption rate. When a product has ≥ 3 purchase events and no individual `out` events, EverShelf calculates the average buy cycle (`(lastBuy - firstBuy) / (buyCount - 1)`) and estimates how many days of stock remain in the current cycle. The product appears in the smart shopping list with a reason like "Finisce tra ~12gg (ciclo medio 75gg)" before it runs out, rather than only after. These products are now also treated as `isRegular` so all stock-level urgency checks apply correctly.
## [1.7.28] - 2026-05-30
### Fixed
- **Duplicate auto-reported issues** — The GitHub issue reporter was relying solely on the GitHub Search API for deduplication. Because search indexing has a several-minutes lag, rapid error recurrences each created a new issue before the previous one was indexed, producing ~50 duplicate issues. The reporter now uses a local file cache (`data/reported_issue_fps.json`, with `/tmp/` fallback when `data/` is not writable) as the primary deduplication store. A 30-minute per-fingerprint comment throttle is also applied to prevent flooding an existing issue. GitHub Search is used only on first run or after a cache miss. Closes [#134](https://github.com/dadaloop82/EverShelf/issues/134) (and all duplicates #135#183).
## [1.7.27] - 2026-05-29
### Added
- **HA sensor enrichment** — All HA sensor attributes that list products now include full product details: `location`, `brand`, `category`, `days_remaining`, `opened_at`, `vacuum_sealed`, `default_quantity`, `package_unit`, `product_id`, `inventory_id`. Applies to `expiring_list`, the new `expired_list`, and the new `low_stock_list`.
- **HA `expired_list` attribute** — `sensor.evershelf_overview` now exposes `expired_list` (full details for all expired items, not just a count).
- **HA `low_stock_list` attribute** — New attribute listing all items with quantity ≤ 1 with full product info.
- **HA `sensor=product` endpoint** — New `GET /api/?action=ha_sensor&sensor=product` returns the full inventory with all product details. Optional filters: `&id=N`, `&name=...`, `&location=...`.
- **Inventory edit safety guard** — Confirm dialog when saving a quantity that is unusually large for its unit (e.g. 183 conf), preventing accidental data loss from unit-confusion typos.
- **Bread shelf-life in fridge** — Opened shelf-life rules added for piadina/crescia (2 days), packaged sliced bread/bauletto (4 days), and generic bread (3 days).
### Fixed
- **Recipe AI ingredient substitution** — Added explicit rule to both recipe prompts preventing Gemini from substituting ingredient forms (e.g. fresh tomatoes ↔ passata, fresh milk ↔ UHT ↔ cream, flour 00 ↔ wholemeal).
- **HA cron webhook payload** — Expiry alert webhook items now include full product details (brand, category, location, days_remaining, opened_at, vacuum_sealed) instead of only name/qty/unit/expiry_date.
### Docs
- `docs/wiki/Home-Assistant.md` — Documented new `sensor=product` endpoint, full product schema table, enriched webhook payload example, and Lovelace/automation template examples using `location` and `days_remaining`.
## [1.7.26] - 2026-05-26
### Added
- **Monthly stats panel** — Third rotating card in the insight banner (anti-waste → nutrition → monthly stats, 1 minute each). Shows products consumed this month with a trend vs. the previous calendar month (↑/↓/→ with % delta), animated horizontal category bars, and badges for items added, wasted, and top-used product. Falls back gracefully when the current month has no transactions. Closes [#100](https://github.com/dadaloop82/EverShelf/issues/100).
- **Extended smart-shopping horizon for staples** — Items consumed ≥ 4 times/month now get a 28-day look-ahead window; ≥ 2 times/month get 21 days. Frequently used staples no longer disappear from the smart list between restocks. Closes [#98](https://github.com/dadaloop82/EverShelf/issues/98).
### Fixed
- **TTS test interactive confirmation** — Test timeout raised from 4 s to 10 s; instead of an error, the UI shows a YES/NO prompt ("Did you hear it?") so users can confirm or report failure explicitly.
- **`end()` PHP 8 reference error** — `_offFetchProduct()` passed the result of `??` directly to `end()`, which requires a variable. Fixed with a temporary variable.
- **Database migration crash on fresh installs** — `migrateDB()` tried to rename the `transactions` table before it existed. A `sqlite_master` guard now calls `initializeDB()` and returns early when the schema is absent. Closes [#131](https://github.com/dadaloop82/EverShelf/issues/131), [#133](https://github.com/dadaloop82/EverShelf/issues/133).
- **Health-check crash on empty database** — `db_row_count` query was executed even when the `inventory` table was missing, causing a fatal PDO error. The query is now skipped until the schema is fully initialised. Closes [#132](https://github.com/dadaloop82/EverShelf/issues/132).
- **Insight banner stuck on one panel** — Rotation interval was 1 hour (effectively invisible); now 60 seconds. `_applyInsightPhase` also now skips empty panels instead of always falling back to the anti-waste card, so the rotation works correctly even when a panel has no data.
- **Untranslated OpenFoodFacts category labels** — Categories stored as OFF slugs (`en:plant-based-foods-and-beverages`, `en:dairies`, …) were shown raw. A new `_normalizeCat()` PHP function maps ~60 OFF slugs to Italian app categories; counts are re-aggregated after normalisation so `en:dairies` + `en:milk` both contribute to `latticini`.
## [1.7.25] - 2026-05-25
### Added
- **Home Assistant integration** — Full bidirectional HA support: inventory sensor (`sensor.evershelf_*`) exposes item counts, expiring items, shopping total, opened items and next-expiry info. Webhooks fire on inventory changes (add/use/shopping). Daily cron alert notifies via HA for items expiring within the configured threshold. TTS announces cooking steps through HA Media Player. New Settings tab 🏠 with connection test, TTS preset (Piper, Google, Nabu Casa), webhook config, and YAML snippet for `configuration.yaml`. Resolves [#111](https://github.com/dadaloop82/EverShelf/issues/111).
- **Offline mode** — Full offline-first support. Full-screen overlay on network loss; "Continue offline" button after 3 s, auto-enter after 8 s. Inventory and settings are synced to `localStorage` at startup and cached on every successful API call. Writes (add/use/update/delete) are queued and synced on reconnect with optimistic UI updates. Pending operations survive page refresh and are re-synced automatically at next startup. AI/network-dependent sections (anti-waste chart, nutrition analysis, recipe generator, price fetching, Gemini chat) are hidden in offline mode. `remoteLog` and `reportError` are buffered offline and flushed on restore. Broken external images replaced with a grey placeholder.
- **Offline-computed dashboard** — While offline, `inventory_summary` and `stats` (expiring/expired/opened) are derived client-side from the local cache so all dashboard stat cards and expiry alerts show accurate data.
### Fixed
- **Offline banner flood** — Opened items in the offline `stats` response lacked `is_edible`; `!undefined` evaluated to `true`, causing every opened item to be shown as "not edible" in the dashboard banner. Field is now set to `true` (client-side shelf-life check already handles genuinely expired items).
- **Version update badge showing older versions** — `_checkWebappUpdate` used `latestTag !== _loadedVersion` (inequality only), so running a newer dev build triggered an "update available" badge for an older GitHub release. Now uses `_semverGt(latest, current)` so only genuinely newer releases trigger the badge.
- **Bring! items re-appearing after manual purchase removal** — `removeBringItem` and `confirmShoppingItemFound` now call `_markBringPurchased` immediately, and `autoAddCriticalItems` respects the blocklist for depleted items.
- **Barcode lookup false "not found"** — New `_offFetchProduct()` tries three barcode candidates (given, UPC-A↔EAN-13 conversion) across two Open Food Facts locales with auto-retry.
- **Partial throw from expired-items banner** — "Butta" now opens the throw modal (qty + location) instead of silently deleting the entire inventory row.
- **Related stock display when scanning branded products** — When scanning a product, the action page now shows a green card listing any inventory items from the same generic family already at home.
## [1.7.24] - 2026-05-21
### Fixed
+42 -7
View File
@@ -25,7 +25,7 @@
[![SQLite](https://img.shields.io/badge/SQLite-3-blue.svg)](https://www.sqlite.org/)
[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED.svg)](Dockerfile)
[![i18n](https://img.shields.io/badge/i18n-IT%20%7C%20EN%20%7C%20DE%20%7C%20FR%20%7C%20ES-orange.svg)](translations/)
[![Version](https://img.shields.io/badge/version-1.7.24-brightgreen.svg)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-1.7.25-brightgreen.svg)](CHANGELOG.md)
[![GitHub stars](https://img.shields.io/github/stars/dadaloop82/EverShelf?style=social)](https://github.com/dadaloop82/EverShelf/stargazers)
[![Last commit](https://img.shields.io/github/last-commit/dadaloop82/EverShelf/main)](https://github.com/dadaloop82/EverShelf/commits/main)
[![Contributors](https://img.shields.io/github/contributors/dadaloop82/EverShelf)](https://github.com/dadaloop82/EverShelf/graphs/contributors)
@@ -36,13 +36,39 @@
---
> **⚠️ Name disambiguation:** There is an unrelated iOS app also called **EverShelf**, developed and published by [Joshumi Technologies LLC](https://evershelf.joshumi.com/) on the [Apple App Store](https://apps.apple.com/app/evershelf/id6759439940). That application is a **completely separate, independent product** with no affiliation, association, or collaboration with this open-source project. This repository has no connection to Joshumi Technologies LLC, its products, or its services.
---
## ✨ Features
> ⚙️ **New in v1.7.23 — Global settings tab, DB auto-cleanup, vacuum-sealed expiry**
> A new **General** tab groups all global settings (language, currency, theme, screensaver, zero-waste, export) in one place.
> Recipes older than `RECIPE_RETENTION_DAYS` and transactions older than `TRANSACTION_RETENTION_DAYS` are deleted automatically every cron cycle, followed by a SQLite `VACUUM` to keep the database small.
> Vacuum-sealed products get an extended grace period (`VACUUM_EXPIRY_EXTENSION_DAYS`, default 30 days) before being flagged as expired.
> Auto theme now follows **time of day** (dark 20:0007:00) instead of the OS setting, making it server-friendly.
### 🏠 NEW — Home Assistant Integration
EverShelf has a **native Home Assistant integration** available on HACS.
Connect your pantry to your smart home in minutes — no YAML, no manual sensor setup.
[![Install via HACS](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=dadaloop82&repository=ha-evershelf&category=integration)
&nbsp;
[![Add Integration](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=evershelf)
**What you get:**
| | |
|---|---|
| **16 sensors** | Expiry counts, stock levels by location (pantry / fridge / freezer), shopping list total, AI API usage, last backup timestamp, days to next expiry |
| **6 binary sensors** | Expired items, expiring items, expiring today, shopping list active, backup overdue, Bring! connected |
| **5 action buttons** | Refresh data, Refresh prices, **Suggest Recipe** (AI — result as HA notification), Sync smart shopping, Clear expired rows |
| **Shopping list todo** | Bidirectional sync — add, remove, check off items directly from HA |
| **Expiry calendar** | Every product's expiry date as a native HA calendar event — works with the calendar card and any calendar automation |
| **Quick-add text entity** | Type a product name in HA to instantly add it to the shopping list (great for voice assistants / Assist) |
| **6 services** | `add_to_shopping`, `mark_used`, `refresh`, `suggest_recipe`, `refresh_prices`, `clear_expired` |
| **Auto-discovery** | Detected automatically via Zeroconf/mDNS when `avahi-daemon` runs on the EverShelf host |
| **5 languages** | English, Italian, German, French, Spanish |
> **Requires a self-hosted EverShelf instance.** The integration talks directly to your server — no cloud involved.
> Full documentation: [ha-evershelf on GitHub](https://github.com/dadaloop82/ha-evershelf)
---
### 📦 Inventory Management
- **Export inventory** — Download the full inventory as a UTF-8 CSV (Excel-compatible) or open a print-ready page to save as PDF; export button always visible in the inventory page header
@@ -111,7 +137,16 @@
- **Mobile-first design** — Optimized for phones, works on tablets and desktop
- **Installable** — Add to home screen for a native app experience
- **Multi-device** — All user data (shopping tags, pinned items, location preferences, scan history) is stored server-side in SQLite and shared across every device on the same instance; no data is siloed in a single browser's localStorage
### 📶 Offline Mode
- **Automatic detection** — Full-screen overlay appears immediately on network loss; shows a "Continue offline" button after 3 s, and auto-enters offline mode after 8 s
- **Local inventory cache** — Inventory is synced to `localStorage` at every startup and on each successful API call; the offline view always reflects the last known state
- **Write queue** — Add, use, update and delete operations performed while offline are queued locally and synced to the server automatically on reconnect (including after a page refresh)
- **Optimistic UI** — Queued writes are applied immediately to the local cache so the interface stays responsive
- **Offline-computed stats** — Expiring and expired items are derived client-side from the cache; dashboard stat cards show real counts instead of zeros
- **AI/network sections hidden** — Anti-waste chart, nutrition analysis, recipe generator, price fetching, and Gemini chat are hidden in offline mode; the inventory, history, and manually-managed shopping list remain fully functional
- **Broken image fallback** — External product images (Open Food Facts, etc.) that fail to load are replaced with a neutral grey placeholder, keeping the layout intact
- **Startup recovery** — If the page is refreshed while operations are queued, they are detected and synced automatically on the next successful startup
- **Buffered error reporting** — `remoteLog` and `reportError` calls made while offline are stored locally and flushed to the server (and to GitHub issues) when the connection is restored
### ⚖️ Smart Scale Integration (Add-on)
- **Bluetooth gateway** — Connects a BLE smart scale to EverShelf via local WebSocket
- **SSE relay** — Server-side relay avoids mixed-content (HTTPS→WS) issues
+117
View File
@@ -133,3 +133,120 @@ try {
_phpErrorReport($msg, $e->getFile(), $e->getLine(), $e->getTraceAsString(), get_class($e));
exit(1);
}
// ── Home Assistant: expiry alerts ─────────────────────────────────────────────
// Fire one HA webhook per expiring item (once per day guard via a simple flag file).
if (env('HA_ENABLED', 'false') === 'true' && env('HA_WEBHOOK_ID', '') !== '') {
try {
$haFlagFile = __DIR__ . '/../data/ha_expiry_notified_' . date('Y-m-d') . '.json';
if (!file_exists($haFlagFile)) {
$expiryDays = max(1, (int)env('HA_EXPIRY_DAYS', '3'));
$expiringItems = $db->query(
"SELECT p.id AS product_id, i.id AS inventory_id,
p.name, p.brand, p.category, p.unit, p.default_quantity, p.package_unit,
i.quantity, i.location, i.expiry_date, i.opened_at, i.vacuum_sealed
FROM inventory i JOIN products p ON i.product_id = p.id
WHERE i.quantity > 0 AND i.expiry_date IS NOT NULL
AND i.expiry_date BETWEEN date('now') AND date('now', '+{$expiryDays} days')
ORDER BY i.expiry_date ASC LIMIT 20"
)->fetchAll(PDO::FETCH_ASSOC);
$expiredItems = $db->query(
"SELECT p.id AS product_id, i.id AS inventory_id,
p.name, p.brand, p.category, p.unit, p.default_quantity, p.package_unit,
i.quantity, i.location, i.expiry_date, i.opened_at, i.vacuum_sealed
FROM inventory i JOIN products p ON i.product_id = p.id
WHERE i.quantity > 0 AND i.expiry_date IS NOT NULL
AND i.expiry_date < date('now')
ORDER BY i.expiry_date ASC LIMIT 10"
)->fetchAll(PDO::FETCH_ASSOC);
// Normalise rows to full product format
if (!function_exists('_haFormatProduct')) {
function _haFormatProduct(array $row): array {
$daysRemaining = null;
if (!empty($row['expiry_date'])) {
$diff = (new DateTime(date('Y-m-d')))->diff(new DateTime($row['expiry_date']));
$daysRemaining = (int)$diff->format('%r%a');
}
return [
'product_id' => (int)($row['product_id'] ?? 0),
'inventory_id' => (int)($row['inventory_id'] ?? 0),
'name' => $row['name'],
'brand' => $row['brand'] ?? null,
'category' => $row['category'] ?? null,
'quantity' => (float)($row['quantity'] ?? 0),
'unit' => $row['unit'] ?? '',
'default_quantity' => (float)($row['default_quantity'] ?? 0),
'package_unit' => $row['package_unit'] ?? null,
'location' => $row['location'] ?? null,
'expiry_date' => $row['expiry_date'] ?? null,
'days_remaining' => $daysRemaining,
'opened_at' => $row['opened_at'] ?? null,
'vacuum_sealed' => !empty($row['vacuum_sealed']),
];
}
}
$expiringItems = array_map('_haFormatProduct', $expiringItems);
$expiredItems = array_map('_haFormatProduct', $expiredItems);
if (!empty($expiringItems)) {
$names = implode(', ', array_column($expiringItems, 'name'));
_fireHaWebhook('expiry_alert', [
'count' => count($expiringItems),
'items' => $expiringItems,
'type' => 'expiring_soon',
'days' => $expiryDays,
'summary' => $names,
]);
// Also send HA notification if service configured
if (env('HA_NOTIFY_SERVICE', '') !== '') {
$msg = count($expiringItems) . ' product(s) expiring within ' . $expiryDays . ' days: ' . $names;
_sendHaNotify($msg, ['expiring_items' => $expiringItems]);
}
echo '[' . date('Y-m-d H:i:s') . '] HA expiry_alert fired: ' . count($expiringItems) . " items\n";
}
if (!empty($expiredItems)) {
$expNames = implode(', ', array_column($expiredItems, 'name'));
_fireHaWebhook('expiry_alert', [
'count' => count($expiredItems),
'items' => $expiredItems,
'type' => 'expired',
'summary' => $expNames,
]);
echo '[' . date('Y-m-d H:i:s') . '] HA expired fired: ' . count($expiredItems) . " items\n";
}
// Mark as done for today
file_put_contents($haFlagFile, json_encode(['ts' => time(), 'expiring' => count($expiringItems ?? []), 'expired' => count($expiredItems ?? [])]));
// Clean up old flag files (keep last 7 days)
foreach (glob(__DIR__ . '/../data/ha_expiry_notified_*.json') as $oldFlag) {
$flagDate = str_replace([__DIR__ . '/../data/ha_expiry_notified_', '.json'], '', $oldFlag);
if ($flagDate < date('Y-m-d', strtotime('-7 days'))) @unlink($oldFlag);
}
}
} catch (Throwable $haE) {
echo '[' . date('Y-m-d H:i:s') . '] HA expiry hook warning: ' . $haE->getMessage() . "\n";
}
}
// ── Avahi/mDNS discovery registration ─────────────────────────────────────────
// If avahi-daemon is running on this host, register the _evershelf._tcp service
// so that Home Assistant can auto-discover this instance via Zeroconf.
if (function_exists('shell_exec')) {
try {
$avahiService = '/etc/avahi/services/evershelf.xml';
// Only create/update if avahi-daemon is installed and the file doesn't exist yet
if (!file_exists($avahiService) && (shell_exec('which avahi-daemon 2>/dev/null') || shell_exec('which avahi-publish 2>/dev/null'))) {
$template = __DIR__ . '/../docker/avahi-evershelf.xml';
if (file_exists($template)) {
$xml = file_get_contents($template);
@file_put_contents($avahiService, $xml);
echo '[' . date('Y-m-d H:i:s') . '] Avahi mDNS service registered at ' . $avahiService . "\n";
}
}
} catch (Throwable $avahiE) {
// Non-fatal: avahi not available
}
}
+32
View File
@@ -126,6 +126,16 @@ function initializeDB(PDO $db): void {
}
function migrateDB(PDO $db): void {
// Guard: if core tables don't exist yet (e.g. DB file present but empty / partial init),
// run initializeDB first so all tables are created, then return — no ALTER TABLE needed.
$productsExists = $db->query(
"SELECT name FROM sqlite_master WHERE type='table' AND name='products'"
)->fetchColumn();
if (!$productsExists) {
initializeDB($db);
return;
}
// Add package_unit column if missing
$cols = $db->query("PRAGMA table_info(products)")->fetchAll();
$colNames = array_column($cols, 'name');
@@ -267,6 +277,20 @@ function migrateDB(PDO $db): void {
");
$db->exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_shopping_list_name ON shopping_list(lower(name))");
}
// Add is_favorite column to recipes if missing (#124)
$recCols = array_column($db->query("PRAGMA table_info(recipes)")->fetchAll(), 'name');
if (!in_array('is_favorite', $recCols)) {
try { $db->exec("ALTER TABLE recipes ADD COLUMN is_favorite INTEGER NOT NULL DEFAULT 0"); }
catch (PDOException $e) { if (strpos($e->getMessage(), 'duplicate column') === false) throw $e; }
}
// Add nutriments_json column to products if missing (#118)
$prodCols2 = array_column($db->query("PRAGMA table_info(products)")->fetchAll(), 'name');
if (!in_array('nutriments_json', $prodCols2)) {
try { $db->exec("ALTER TABLE products ADD COLUMN nutriments_json TEXT DEFAULT NULL"); }
catch (PDOException $e) { if (strpos($e->getMessage(), 'duplicate column') === false) throw $e; }
}
}
/**
@@ -440,6 +464,14 @@ function estimateOpenedExpiryDaysPHP(string $name, string $category, string $loc
if (preg_match('/\b(patata|patate|tubero)\b/', $n)) return 4;
if (preg_match('/\baglio\b/', $n)) return 14;
// ── F.extra: Bread in fridge (opened) ──────────────────────────────────
// Thin flatbreads (piadina, crescia, tigella) get mold very quickly
if (preg_match('/\b(piadina|piadelle?|crescia|tigella)\b/', $n)) return 2;
// Packaged sliced bread — preservatives help a bit
if (preg_match('/\b(bauletto|pancarrè|pan\s+carr|tramezzin)\b/', $n)) return 4;
// Generic bread / sandwich bread in fridge
if (preg_match('/\bpane\b/', $cat)) return 3;
// ── G: Fridge condiments — medium shelf-life ─────────────────────────
if (preg_match('/maionese|mayo|mayon/', $n)) return 90;
if (preg_match('/\bketchup\b/', $n)) return 90;
+1522 -125
View File
File diff suppressed because it is too large Load Diff
+517 -5
View File
@@ -596,13 +596,37 @@ body {
}
.offline-banner-retry:hover { background: rgba(255,255,255,0.38); }
/* Pulsing dot shown in the banner while the offline cache is being read */
.offline-banner-dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
background: #f87171;
margin-right: 6px;
vertical-align: middle;
animation: offline-dot-pulse 1.1s ease-in-out infinite;
}
@keyframes offline-dot-pulse {
0%, 100% { opacity: 0.3; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1.15); }
}
/* When server is offline, block interactions with the main content */
body.server-offline .app-content {
body.server-offline:not(.offline-mode) .app-content {
opacity: 0.4;
pointer-events: none;
user-select: none;
transition: opacity 0.3s;
}
/* In offline-mode the app is usable; just a subtle left-border indicator */
body.offline-mode .app-content {
border-left: 3px solid rgba(239, 68, 68, 0.45);
}
/* Hide the "Retry" button in the banner when in offline mode — use the Continue button instead */
body.offline-mode .offline-banner-retry {
display: none;
}
body.server-offline .bottom-nav {
opacity: 0.4;
pointer-events: none;
@@ -1945,6 +1969,46 @@ body.server-offline .bottom-nav {
text-overflow: ellipsis;
}
/* — Scan status bar — */
.scan-status-bar {
position: absolute;
bottom: 38px;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
pointer-events: none;
z-index: 12;
}
.scan-status-method {
font-size: 0.58rem;
color: rgba(255,255,255,0.45);
text-transform: uppercase;
letter-spacing: 0.07em;
font-family: monospace;
}
.scan-status-msg {
font-size: 0.74rem;
color: rgba(255,255,255,0.9);
background: rgba(0,0,0,0.55);
padding: 3px 10px;
border-radius: 12px;
text-align: center;
max-width: 92%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.2s, background 0.2s;
backdrop-filter: blur(3px);
}
.scan-status-msg:empty { visibility: hidden; }
.scan-status-msg.state-partial { color: #fbbf24; }
.scan-status-msg.state-invalid { color: #f87171; background: rgba(239,68,68,0.28); }
.scan-status-msg.state-confirmed { color: #4ade80; background: rgba(74,222,128,0.22); }
.scan-status-msg.state-retry { color: #fb923c; }
/* — Viewport overlay controls (torch / zoom / flip) — */
.scan-viewport-controls {
position: absolute;
@@ -2567,6 +2631,17 @@ body.server-offline .bottom-nav {
color: var(--text-muted);
}
.shopping-pantry-hint {
font-size: 0.72rem;
color: #15803d;
font-weight: 500;
margin-top: 2px;
opacity: 0.85;
}
[data-theme="dark"] .shopping-pantry-hint {
color: #4ade80;
}
.shopping-item-right {
display: flex;
flex-direction: column;
@@ -3027,10 +3102,82 @@ body.server-offline .bottom-nav {
gap: 14px;
}
.product-preview-small {
padding: 12px;
/* Action and Use page hero card */
#page-action .product-preview-small,
#page-use .product-preview-small {
padding: 14px 16px;
gap: 14px;
border-left: 4px solid var(--primary);
align-items: flex-start;
position: relative;
}
/* action page: slightly larger name */
#page-action .use-hero-name {
font-size: 1.15rem;
}
/* barcode pill on action page */
.action-pill-barcode { background: #f1f5f9; color: #64748b; }
.use-hero-icon {
font-size: 2.4rem;
width: 52px;
text-align: center;
flex-shrink: 0;
line-height: 1;
padding-top: 2px;
}
.use-hero-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.use-hero-name {
font-size: 1.05rem;
font-weight: 700;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.use-hero-brand {
font-size: 0.8rem;
color: var(--text-light);
}
.use-hero-meta {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 5px;
}
.use-meta-pill {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 0.75rem;
font-weight: 600;
padding: 3px 8px;
border-radius: 99px;
line-height: 1.3;
white-space: nowrap;
}
/* Expiry pill colours */
.use-pill-ok { background: #dcfce7; color: #166534; }
.use-pill-warn { background: #fef9c3; color: #854d0e; }
.use-pill-soon { background: #fed7aa; color: #7c2d12; }
.use-pill-expired { background: #fee2e2; color: #991b1b; }
/* Quantity pill */
.use-pill-qty { background: #e0f2fe; color: #0c4a6e; }
.product-preview img, .product-preview-small img {
width: 60px;
height: 60px;
@@ -3040,8 +3187,11 @@ body.server-offline .bottom-nav {
}
.product-preview-small img {
width: 45px;
height: 45px;
width: 52px;
height: 52px;
border-radius: var(--radius-sm);
object-fit: cover;
flex-shrink: 0;
}
.product-preview-emoji {
@@ -4130,6 +4280,7 @@ body.server-offline .bottom-nav {
.recipe-result .recipe-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
@@ -4166,6 +4317,35 @@ body.server-offline .bottom-nav {
color: #3730a3;
white-space: nowrap;
}
/* Appliance/mode badge shown inline next to a step text */
.recipe-step-appliance {
display: inline-block;
margin-left: 6px;
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 12px;
padding: 1px 8px;
font-size: 0.72rem;
color: #15803d;
vertical-align: middle;
white-space: nowrap;
}
/* Regen choice panel */
.recipe-regen-choice {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 14px;
margin-top: 10px;
}
.recipe-regen-choice-title {
font-size: 0.9rem;
font-weight: 600;
color: #475569;
margin: 0 0 10px 0;
text-align: center;
}
/* Recipe ingredient use buttons */
.recipe-ingredients {
@@ -5583,6 +5763,26 @@ body.cooking-mode-active .app-header {
background: rgba(59, 130, 246, 0.15);
}
/* Related stock hint (same generic family, different brand/product) */
.action-related-stock-card {
background: #f0fdf4;
border: 1.5px solid #86efac;
border-radius: var(--radius);
padding: 10px 14px;
font-size: 0.82rem;
color: #166534;
margin-bottom: 12px;
line-height: 1.5;
}
.action-related-stock-card strong { color: #15803d; }
.related-stock-item { display: inline-block; margin-right: 8px; }
[data-theme="dark"] .action-related-stock-card {
background: rgba(21, 128, 61, 0.12);
border-color: #166534;
color: #86efac;
}
[data-theme="dark"] .action-related-stock-card strong { color: #4ade80; }
/* ===== ACTION BUTTONS GRID ===== */
.action-buttons-3col {
display: grid;
@@ -6376,6 +6576,117 @@ body.cooking-mode-active .app-header {
flex-wrap: wrap;
}
/* ===== RECIPE FAVORITES (#124) ===== */
.recipe-fav-badge {
margin-left: auto;
font-size: 1.1rem;
color: #f59e0b;
flex-shrink: 0;
}
.recipe-archive-card-fav {
border-left: 3px solid #f59e0b;
}
.btn-recipe-fav {
background: none;
border: none;
font-size: 1.8rem;
cursor: pointer;
color: var(--text-muted);
padding: 0 4px;
line-height: 1;
vertical-align: middle;
transition: color 0.2s, transform 0.15s;
margin-left: auto;
}
.btn-recipe-fav:hover { color: #f59e0b; transform: scale(1.2); }
.btn-recipe-fav.active { color: #f59e0b; }
/* ===== PORTION RESCALER (#123) ===== */
.recipe-persons-ctrl {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 0 6px;
vertical-align: middle;
line-height: 1;
}
.btn-persons-adj {
background: none;
border: none;
color: var(--text-primary);
border-radius: 50%;
width: 18px;
height: 18px;
font-size: 0.85rem;
line-height: 1;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
flex-shrink: 0;
transition: background 0.15s;
}
.btn-persons-adj:hover { background: var(--accent, #6366f1); color: #fff; }
#recipe-persons-display {
white-space: nowrap;
font-size: 0.82rem;
}
/* ===== MACRONUTRIENT PANEL (#118) ===== */
.macro-bars {
display: flex;
flex-direction: column;
gap: 8px;
margin: 12px 0 8px;
}
.macro-row {
display: flex;
align-items: center;
gap: 8px;
}
.macro-label {
font-size: 0.75rem;
color: var(--text-muted);
min-width: 70px;
flex-shrink: 0;
}
.macro-bar-wrap {
flex: 1;
height: 8px;
background: var(--bg-secondary, #1e2a3a);
border-radius: 4px;
overflow: hidden;
}
.macro-bar-fill {
height: 100%;
width: 0%;
border-radius: 4px;
transition: width 0.6s ease;
}
.macro-val {
font-size: 0.72rem;
color: var(--text-primary);
text-align: right;
min-width: 80px;
flex-shrink: 0;
}
.macro-val small {
color: var(--text-muted);
}
/* ===== SCREENSAVER ===== */
.screensaver-overlay {
position: fixed;
@@ -6765,6 +7076,82 @@ body.cooking-mode-active .app-header {
}
.nutr-score-val { flex: 0 0 32px; text-align: right; font-weight: 600; }
/* ===== MONTHLY STATS PANEL ===== */
.ms-main-row {
display: flex;
align-items: center;
gap: 14px;
margin: 12px 0 8px;
}
.ms-main-num {
font-size: 2.8rem;
font-weight: 700;
color: #6366f1;
line-height: 1;
letter-spacing: -0.02em;
}
.ms-main-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.ms-main-label {
font-size: .85rem;
color: #94a3b8;
}
.ms-trend {
font-size: .8rem;
font-weight: 500;
}
.ms-cats-section {
margin: 6px 0 4px;
}
.ms-cats-title {
font-size: .68rem;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
letter-spacing: .06em;
margin-bottom: 7px;
}
.ms-cat-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 5px;
}
.ms-cat-name {
font-size: .74rem;
color: #cbd5e1;
min-width: 78px;
max-width: 78px;
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ms-cat-bar-wrap {
flex: 1;
height: 8px;
background: #1e293b;
border-radius: 4px;
overflow: hidden;
}
.ms-cat-bar {
height: 100%;
border-radius: 4px;
width: 0;
}
.ms-cat-cnt {
font-size: .7rem;
color: #64748b;
min-width: 22px;
text-align: right;
}
.ms-badges-row {
margin-top: 6px;
}
/* ===== SETUP WIZARD ===== */
.setup-wizard-content {
max-width: 480px;
@@ -7510,11 +7897,22 @@ body.cooking-mode-active .app-header {
/* ── Use inventory info ── */
[data-theme="dark"] .use-inventory-info { background: #0c2a4e; color: #7dd3fc; }
[data-theme="dark"] #use-expiry-hint { background: #2a1e00; border-color: #78350f; color: #fde68a; }
[data-theme="dark"] #page-use .product-preview-small { border-left-color: var(--primary); }
[data-theme="dark"] #page-action .product-preview-small { border-left-color: var(--primary); }
[data-theme="dark"] .action-pill-barcode { background: #1e293b; color: #94a3b8; }
[data-theme="dark"] .use-pill-ok { background: #14532d; color: #86efac; }
[data-theme="dark"] .use-pill-warn { background: #422006; color: #fde68a; }
[data-theme="dark"] .use-pill-soon { background: #431407; color: #fdba74; }
[data-theme="dark"] .use-pill-expired { background: #450a0a; color: #fca5a5; }
[data-theme="dark"] .use-pill-qty { background: #0c2a4e; color: #7dd3fc; }
/* ── Recipe components ── */
[data-theme="dark"] .recipe-expiry-note { background: #2a1e00; color: #fde68a; }
[data-theme="dark"] .recipe-tools-banner { background: #1a1040; border-color: #3730a3; color: #c4b5fd; }
[data-theme="dark"] .recipe-tool-chip { background: #2e1a4a; color: #c4b5fd; }
[data-theme="dark"] .recipe-step-appliance { background: #052e16; border-color: #166534; color: #4ade80; }
[data-theme="dark"] .recipe-regen-choice { background: #1e293b; border-color: #334155; }
[data-theme="dark"] .recipe-regen-choice-title { color: #94a3b8; }
[data-theme="dark"] .recipe-subtype-chip { background: #1c1300; border-color: #78350f; color: var(--text); }
[data-theme="dark"] .recipe-subtype-chip:has(input:checked) { background: #2a1e00; border-color: #d97706; }
@@ -7541,3 +7939,117 @@ body.cooking-mode-active .app-header {
/* ── Appliance remove active ── */
[data-theme="dark"] .appliance-item .appliance-remove:active { background: #2a0808; }
/* ===== NETWORK ERROR OVERLAY ===== */
#network-error-overlay {
position: fixed;
inset: 0;
background: rgba(6, 8, 20, 0.97);
z-index: 300000; /* highest: above screensaver(10000), cooking(99999), preloader(200000) */
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
#network-error-overlay.visible {
opacity: 1;
pointer-events: auto;
}
.net-error-body {
text-align: center;
padding: 2.5rem 2rem;
display: flex;
flex-direction: column;
align-items: center;
}
.net-error-icon {
font-size: 5.5rem;
line-height: 1;
margin-bottom: 1.75rem;
animation: net-pulse 2.2s ease-in-out infinite;
display: block;
filter: drop-shadow(0 0 32px rgba(248, 113, 113, 0.35));
}
#network-error-overlay.restored .net-error-icon {
animation: none;
filter: drop-shadow(0 0 32px rgba(74, 222, 128, 0.45));
}
#network-error-overlay.checking .net-error-icon {
animation: net-spin 1.2s linear infinite;
}
@keyframes net-pulse {
0%, 100% { opacity: 0.45; transform: scale(0.92); }
50% { opacity: 1; transform: scale(1.06); }
}
@keyframes net-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.net-error-title {
font-size: 2.5rem;
font-weight: 700;
color: #f87171;
margin-bottom: 0.75rem;
letter-spacing: -0.02em;
transition: color 0.4s;
}
#network-error-overlay.restored .net-error-title {
color: #4ade80;
}
.net-error-subtitle {
font-size: 1.1rem;
color: #94a3b8;
max-width: 420px;
line-height: 1.6;
margin: 0 auto;
}
.net-error-status {
margin-top: 1.5rem;
font-size: 0.88rem;
color: #475569;
min-height: 1.3em;
letter-spacing: 0.01em;
}
/* "Continue in offline mode" button — appears after 3 s */
.net-error-continue-btn {
margin-top: 2.2rem;
background: rgba(255,255,255,0.07);
border: 1px solid rgba(255,255,255,0.22);
color: #94a3b8;
border-radius: 10px;
padding: 0.7rem 1.6rem;
font-size: 0.92rem;
cursor: pointer;
transition: background 0.2s, color 0.2s, transform 0.3s, opacity 0.3s;
opacity: 0;
transform: translateY(8px);
}
.net-error-continue-btn.visible {
opacity: 1;
transform: translateY(0);
}
.net-error-continue-btn:hover {
background: rgba(255,255,255,0.16);
color: #e2e8f0;
}
/* ─── Offline mode: hide AI and network-dependent UI ────────────────────────
Sections that require a live server response or AI are hidden so the user
isn't confronted with empty/broken widgets while offline. */
body.offline-mode #waste-chart-section,
body.offline-mode #nutrition-section,
body.offline-mode #quick-recipe-bar,
body.offline-mode .header-gemini-btn,
body.offline-mode #btn-suggest,
body.offline-mode #btn-fetch-prices,
body.offline-mode .recipe-generate-btn {
display: none !important;
}
/* Smart-shopping AI section: show as disabled rather than disappearing entirely */
body.offline-mode #smart-shopping {
opacity: 0.45;
pointer-events: none;
filter: grayscale(0.6);
}
+1762 -178
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name replace-wildcards="yes">EverShelf Pantry (%h)</name>
<service>
<type>_evershelf._tcp</type>
<port>80</port>
<txt-record>path=/api/</txt-record>
<txt-record>version=1.0</txt-record>
<txt-record>app=evershelf</txt-record>
</service>
</service-group>
+308
View File
@@ -0,0 +1,308 @@
# Home Assistant Integration
EverShelf integrates natively with [Home Assistant](https://www.home-assistant.io/) to bring your pantry data into your smart-home automations.
**Capabilities:**
- 📡 **REST sensors** — expose pantry counts as HA sensor entities (expiring, expired, shopping list, total items)
- 🔔 **Webhooks** — trigger HA automations on pantry events (expiry alerts, shopping additions, stock updates)
- 📣 **Push notifications** — send alerts to your phone via any HA `notify.*` service
- 🔊 **TTS on smart speakers** — read recipe steps aloud on any HA `media_player` entity
- ⚙️ **In-app config panel** — configure everything from Settings → 🏠 tab (no need to edit `.env` manually)
---
## Quick Setup
1. **Generate a Long-Lived Access Token** in Home Assistant:
- Open HA → your **Profile** (bottom-left avatar) → **Security****Long-Lived Access Tokens** → **Create Token**
- Copy the generated token — you won't see it again.
2. **Open EverShelf Settings** → tab **🏠 Home Assistant**.
3. Fill in **Home Assistant URL** (e.g. `http://homeassistant.local:8123`) and paste the token.
4. Click **Test connection** — you should see ✅.
5. Enable the features you want (TTS, Webhooks, REST Sensors) and click **Save HA settings**.
---
## REST Sensors
Add EverShelf pantry data as native HA sensor entities that update automatically.
### Endpoints
| URL | Returns | Sensor |
|-----|---------|--------|
| `/api/?action=ha_sensor` | Items expiring soon (≤`HA_EXPIRY_DAYS` days) | `sensor.evershelf_overview` |
| `/api/?action=ha_sensor&sensor=expired` | Expired items count | `sensor.evershelf_expired` |
| `/api/?action=ha_sensor&sensor=shopping` | Shopping list item count | `sensor.evershelf_shopping` |
| `/api/?action=ha_sensor&sensor=total` | Total pantry items | `sensor.evershelf_total` |
| `/api/?action=ha_sensor&sensor=product` | Full inventory — all items with complete details | `sensor.evershelf_products` |
| `/api/?action=ha_sensor&sensor=product&id=42` | Full details for inventory row `id=42` | — |
| `/api/?action=ha_sensor&sensor=product&name=milk` | Full details for items whose name contains "milk" | — |
| `/api/?action=ha_sensor&sensor=product&location=frigo` | All items in a specific location | — |
### Generate & Copy YAML
In Settings → 🏠 Home Assistant → **REST Sensors** card, click **Copy YAML** to get a ready-to-paste `configuration.yaml` block that already contains your EverShelf URL.
### Manual YAML example
```yaml
# configuration.yaml
sensor:
- platform: rest
name: "EverShelf Overview"
unique_id: evershelf_overview
resource: "http://YOUR_EVERSHELF_URL/api/?action=ha_sensor"
scan_interval: 300 # seconds
value_template: "{{ value_json.state }}"
json_attributes:
- expiring_soon
- expiring_3d
- expired_items
- total_items
- shopping_items
- expiring_list # full product details for expiring items
- expired_list # full product details for expired items
- low_stock_list # full product details for items with quantity ≤ 1
- next_expiry_name
- next_expiry_date
- days_to_next_expiry
- last_updated
unit_of_measurement: "items"
- platform: rest
name: "EverShelf Shopping Count"
unique_id: evershelf_shopping
resource: "http://YOUR_EVERSHELF_URL/api/?action=ha_sensor&sensor=shopping"
scan_interval: 180
value_template: "{{ value_json.state }}"
unit_of_measurement: "items"
# Full product inventory — each item includes all details (location, brand, category, …)
- platform: rest
name: "EverShelf Products"
unique_id: evershelf_products
resource: "http://YOUR_EVERSHELF_URL/api/?action=ha_sensor&sensor=product"
scan_interval: 600
value_template: "{{ value_json.state }}"
json_attributes:
- items
- last_updated
unit_of_measurement: "items"
```
Restart Home Assistant after editing `configuration.yaml`.
Every product entry inside `expiring_list`, `expired_list`, `low_stock_list`, and `sensor=product` responses follows the same schema:
```json
{
"product_id": 42,
"inventory_id": 7,
"name": "Latte intero",
"brand": "Parmalat",
"category": "Lattiero-caseari",
"quantity": 2.0,
"unit": "conf",
"default_quantity": 1000.0,
"package_unit": "ml",
"location": "frigo",
"expiry_date": "2025-06-15",
"days_remaining": 3,
"opened_at": "2025-06-10",
"vacuum_sealed": false
}
```
Field details:
| Field | Type | Description |
|-------|------|-------------|
| `product_id` | int | Products table ID |
| `inventory_id` | int | Inventory row ID |
| `name` | string | Product name |
| `brand` | string\|null | Brand (if set) |
| `category` | string\|null | Category (if set) |
| `quantity` | float | Current quantity in inventory |
| `unit` | string | Unit (`conf`, `g`, `ml`, `pz`, …) |
| `default_quantity` | float | Default package size (e.g. 1000 for 1-litre carton) |
| `package_unit` | string\|null | Unit of the default package (`g`, `ml`) |
| `location` | string\|null | Storage location (`frigo`, `freezer`, `dispensa`, …) |
| `expiry_date` | string\|null | ISO date `YYYY-MM-DD` |
| `days_remaining` | int\|null | Days until expiry (negative = already expired) |
| `opened_at` | string\|null | ISO date when the package was opened |
| `vacuum_sealed` | bool | Whether the item is vacuum-sealed |
---
## Webhook Automations
EverShelf fires an HTTP POST to your HA webhook URL when pantry events occur.
### Create the HA Webhook Automation
1. HA → **Settings****Automations & Scenes** → **Create Automation**
2. Click **Add Trigger** → choose **Webhook**
3. HA generates a **Webhook ID** — copy it
4. Paste the ID into **Settings → 🏠 Home Assistant → Webhook ID**
5. Select which events should trigger the webhook
### Supported Events
| Event key | When it fires |
|-----------|--------------|
| `expiry` | Daily cron — items expiring within `HA_EXPIRY_DAYS` days |
| `shopping_add` | Item added to the shopping list |
| `stock_update` | Inventory quantity changed |
| `barcode_scan` | (reserved for future use) |
### Webhook Payload (POST body)
```json
{
"event": "expiry_alert",
"timestamp": "2025-06-12T08:00:00+00:00",
"data": {
"type": "expiring_soon",
"count": 3,
"days": 3,
"summary": "Milk, Yogurt, Butter",
"items": [
{
"product_id": 42,
"inventory_id": 7,
"name": "Milk",
"brand": "Parmalat",
"category": "Dairy",
"quantity": 2.0,
"unit": "conf",
"default_quantity": 1000.0,
"package_unit": "ml",
"location": "frigo",
"expiry_date": "2025-06-14",
"days_remaining": 2,
"opened_at": "2025-06-10",
"vacuum_sealed": false
}
]
}
}
```
### Example: Expiry Alert → Telegram
```yaml
alias: EverShelf Expiry Alert
trigger:
- platform: webhook
webhook_id: "evershelf_webhook_abc123" # ← your Webhook ID
action:
- service: notify.telegram_bot
data:
message: >
🥫 EverShelf: {{ trigger.json.data.count }} product(s) expiring soon
{% for item in trigger.json.data.items %}
— {{ item.name }}{% if item.brand %} ({{ item.brand }}){% endif %} ·
{{ item.quantity }} {{ item.unit }} · 📍 {{ item.location }} ·
expires {{ item.expiry_date }} ({{ item.days_remaining }} days)
{% endfor %}
```
### Example: Automation on location
You can filter by location in the automation template to only alert for fridge items:
```yaml
condition:
- condition: template
value_template: >
{{ trigger.json.data.items | selectattr('location','eq','frigo') | list | length > 0 }}
```
---
## Push Notifications
If you prefer to receive push alerts without using webhooks, configure a **HA notify service** directly:
1. Find your notify service name in HA: **Developer Tools → Services** → search `notify`
2. Paste it into **Settings → 🏠 → Notify service** (e.g. `notify.mobile_app_my_phone`)
3. Save
EverShelf will call this service from the cron job whenever expiry alerts fire.
---
## TTS on Smart Speakers
Read recipe steps aloud on an Amazon Echo, Google Home, Sonos, or any HA `media_player`.
### Configuration
1. Enter the **Entity ID** of your media player (e.g. `media_player.kitchen_display`)
- Find it in HA: **Developer Tools → States**
2. Click **Apply HA preset to TTS tab** — this auto-fills the TTS tab with the correct HA endpoint and auth headers
3. Save settings
### How it Works
When recipe step TTS is triggered, EverShelf calls:
```
POST /api/services/tts/speak
Authorization: Bearer <HA_TOKEN>
{
"entity_id": "media_player.kitchen_display",
"message": "Add 200 g of flour and mix well."
}
```
The request is proxied through the EverShelf PHP backend (avoids CORS / mixed-content issues).
---
## Environment Variables Reference
All settings are configurable from `.env` or from the in-app Settings panel.
| Variable | Default | Description |
|----------|---------|-------------|
| `HA_ENABLED` | `false` | Master switch for all HA features |
| `HA_URL` | _(empty)_ | Base URL of HA instance, no trailing slash |
| `HA_TOKEN` | _(empty)_ | Long-Lived Access Token |
| `HA_TTS_ENTITY` | _(empty)_ | `media_player` entity for TTS |
| `HA_WEBHOOK_ID` | _(empty)_ | Webhook trigger ID from HA automation |
| `HA_WEBHOOK_EVENTS` | `expiry,shopping_add,stock_update` | Comma-separated list of events |
| `HA_NOTIFY_SERVICE` | _(empty)_ | HA notify service (e.g. `notify.mobile_app_phone`) |
| `HA_EXPIRY_DAYS` | `3` | Days before expiry to trigger the daily alert |
---
## Troubleshooting
**Test shows ❌ "Connection failed"**
- Verify the URL is reachable from the EverShelf server (not just your browser)
- If using HTTPS with a self-signed certificate, the server-side cURL request may fail — use HTTP on the local network instead
- Check that port 8123 (or your custom port) is open on the HA host
**Test shows ❌ "bad_token"**
- The Long-Lived Access Token may have expired or been revoked — generate a new one in HA Profile
**Webhook not firing**
- Confirm HA_ENABLED=true and the Webhook ID is exactly as shown in HA
- Check the EverShelf cron is running (`/api/cron_smart_shopping.php` every 5 minutes)
- For shopping/stock events: verify the event name is in `HA_WEBHOOK_EVENTS`
**TTS not speaking**
- Ensure the media player entity is online in HA (check its state in Developer Tools)
- Try the "Apply HA preset to TTS tab" button and send a test from the TTS tab
- Check HA logs for `tts.speak` errors (some platforms require `tts_options`)
**Sensors show unavailable in HA**
- The EverShelf URL must be reachable from the HA host
- If running EverShelf behind a reverse proxy, ensure `/api/` is accessible
- Use `scan_interval` ≥ 60 to avoid hammering the server
@@ -18,7 +18,9 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.media.AudioManager
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
@@ -144,6 +146,25 @@ class KioskActivity : AppCompatActivity() {
if (res == TextToSpeech.LANG_MISSING_DATA || res == TextToSpeech.LANG_NOT_SUPPORTED) {
tts?.language = Locale.getDefault()
}
tts?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
override fun onStart(utteranceId: String?) {}
override fun onDone(utteranceId: String?) {
runOnUiThread {
webView.evaluateJavascript("if(window._kioskTtsDone)window._kioskTtsDone('$utteranceId')", null)
}
}
@Deprecated("Deprecated in API 21")
override fun onError(utteranceId: String?) {
runOnUiThread {
webView.evaluateJavascript("if(window._kioskTtsError)window._kioskTtsError('$utteranceId','error')", null)
}
}
override fun onError(utteranceId: String?, errorCode: Int) {
runOnUiThread {
webView.evaluateJavascript("if(window._kioskTtsError)window._kioskTtsError('$utteranceId',$errorCode)", null)
}
}
})
ttsReady = true
}
}
@@ -466,7 +487,10 @@ class KioskActivity : AppCompatActivity() {
if (!ttsReady) return
engine.setSpeechRate(rate.coerceIn(0.1f, 4f))
engine.setPitch(pitch.coerceIn(0.1f, 4f))
engine.speak(text, android.speech.tts.TextToSpeech.QUEUE_FLUSH, null, "kiosk_tts")
val params = Bundle().apply {
putInt(TextToSpeech.Engine.KEY_PARAM_STREAM, AudioManager.STREAM_MUSIC)
}
engine.speak(text, TextToSpeech.QUEUE_FLUSH, params, "kiosk_tts")
}
@JavascriptInterface
fun stopSpeech() { tts?.stop() }
@@ -123,6 +123,9 @@ class SettingsActivity : AppCompatActivity() {
// Back
findViewById<android.widget.ImageButton>(R.id.btnBack).setOnClickListener { finish() }
// Advanced settings → back to webapp (where HA, Gemini, Bring! etc. are configured)
findViewById<MaterialButton>(R.id.btnOpenAppSettings).setOnClickListener { finish() }
// Test connection
findViewById<MaterialButton>(R.id.btnTestConnection).setOnClickListener { testConnection() }
@@ -400,6 +400,7 @@ class SetupActivity : AppCompatActivity() {
scaleTestCard.visibility = View.GONE
testWeightBox.visibility = View.GONE
bleSetupCard.visibility = View.VISIBLE
step3NextButtons.visibility = View.VISIBLE // restore nav buttons (back/next)
tvSelectedScale.text = ""
tvSelectedScale.visibility = View.GONE
tvScanStatus.text = getString(R.string.ble_not_confirmed)
@@ -960,6 +961,8 @@ class SetupActivity : AppCompatActivity() {
testWeightBox.visibility = View.GONE
testHasWeight = false
findViewById<MaterialButton>(R.id.btnTestConfirm).isEnabled = false
// Always re-enable retry so the user is never stuck
findViewById<MaterialButton>(R.id.btnTestRetry).isEnabled = true
}
override fun onWeightReceived(reading: WeightReading) {
if (!isInTestMode) return
@@ -224,6 +224,43 @@
</LinearLayout>
</LinearLayout>
<!-- Advanced / App Settings link -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="IMPOSTAZIONI AVANZATE"
android:textColor="#7c3aed"
android:textSize="12sp"
android:textStyle="bold"
android:letterSpacing="0.1"
android:layout_marginBottom="12dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/card_background"
android:padding="16dp"
android:layout_marginBottom="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Home Assistant, Gemini AI, Bring!, TTS, notifiche e tutte le altre funzionalità si configurano direttamente nell'app EverShelf."
android:textColor="#94a3b8"
android:textSize="13sp"
android:layout_marginBottom="12dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnOpenAppSettings"
android:layout_width="match_parent"
android:layout_height="44dp"
android:text="← Torna all'app per le impostazioni avanzate"
android:textSize="13sp"
android:textAllCaps="false"
android:backgroundTint="#7c3aed" />
</LinearLayout>
<!-- Danger Zone -->
<TextView
android:layout_width="match_parent"
+149 -6
View File
@@ -64,7 +64,7 @@
<div id="preloader-warnings" class="preloader-warnings" style="display:none"></div>
<div id="preloader-error-msg" class="preloader-error-msg" style="display:none"></div>
<button id="preloader-retry-btn" class="preloader-retry-btn" style="display:none" onclick="_startupRetry()">🔄 <span data-i18n="startup.retry">Riprova</span></button>
<span class="app-preloader-version" id="preloader-version">v1.7.23</span>
<span class="app-preloader-version" id="preloader-version">v1.7.25</span>
</div>
</div>
@@ -77,7 +77,7 @@
<!-- Title — left-aligned; grows to fill space -->
<div class="header-title-wrap">
<h1 class="header-title" onclick="showPage('dashboard')">
<img src="assets/img/logo/logo_icon.png" alt="" class="header-logo-icon" aria-hidden="true" /><span data-i18n="nav.title">EverShelf</span><span class="header-version">v1.7.23</span>
<img src="assets/img/logo/logo_icon.png" alt="" class="header-logo-icon" aria-hidden="true" /><span data-i18n="nav.title">EverShelf</span><span class="header-version">v1.7.25</span>
</h1>
<!-- Update badge — shown alongside title, never replaces it -->
<span class="header-update-badge" id="header-update-badge" style="display:none"></span>
@@ -169,10 +169,12 @@
<div id="expired-list"></div>
</div>
<!-- Anti-Waste Report Card + Nutrition Analysis (alternating, content rendered by JS) -->
<!-- Anti-Waste Report Card + Nutrition Analysis + Monthly Stats (alternating, content rendered by JS) -->
<div id="dashboard-insight-wrap" style="position:relative">
<div id="waste-chart-section" style="display:none"></div>
<div id="nutrition-section" style="display:none"></div>
<div id="monthly-stats-section" style="display:none"></div>
<div id="macros-section" style="display:none"></div>
</div>
<!-- Alert for soonest expiring items -->
@@ -249,6 +251,11 @@
</div>
<!-- Live partial code preview -->
<div class="scan-live-code" id="scan-live-code" style="display:none"></div>
<!-- Scan status bar -->
<div class="scan-status-bar" id="scan-status-bar">
<span id="scan-status-method" class="scan-status-method"></span>
<span id="scan-status-msg" class="scan-status-msg" data-i18n="scan.status_ready"></span>
</div>
<!-- Success flash overlay -->
<div class="scan-confirm-overlay" id="scan-confirm-overlay" style="display:none">
<div class="scan-confirm-check"></div>
@@ -331,8 +338,9 @@
</div>
<!-- Banner: shopping list scan context -->
<div id="shopping-scan-target-banner" class="shopping-scan-target-banner" style="display:none"></div>
<div class="product-preview product-preview-large" id="action-product-preview"></div>
<div class="product-preview product-preview-small" id="action-product-preview"></div>
<div class="inventory-status-bar" id="action-inventory-status" style="display:none"></div>
<div id="action-related-stock" style="display:none"></div>
<div class="action-buttons" id="action-buttons-container">
<button class="btn btn-huge btn-success" onclick="showAddForm()">
<span class="btn-icon">📥</span>
@@ -671,7 +679,7 @@
<h2 data-i18n="recipes.title">🍳 Ricette</h2>
</div>
<div class="recipe-page-container">
<button class="btn btn-large btn-success full-width" onclick="openRecipeDialog()" data-i18n="recipes.generate">
<button class="btn btn-large btn-success full-width recipe-generate-btn" onclick="openRecipeDialog()" data-i18n="recipes.generate">
✨ Genera nuova ricetta
</button>
<div id="recipe-archive" class="recipe-archive"></div>
@@ -840,6 +848,7 @@
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-camera')" data-tab="tab-camera" title="Fotocamera">📷</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-security')" data-tab="tab-security" title="Sicurezza">🔒</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-tts')" data-tab="tab-tts" title="Voce (TTS)" data-i18n-title="settings.tab_tts">🔊</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-ha'); _loadHaTab();" data-tab="tab-ha" title="Home Assistant" data-i18n-title="settings.ha.tab">🏠</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-scale')" data-tab="tab-scale" title="Bilancia Smart" data-i18n-title="settings.scale.tab">⚖️</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-backup'); _loadBackupTab();" data-tab="tab-backup" data-i18n-title="settings.backup.tab" title="Backup">💾</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-info'); _loadInfoTab();" data-tab="tab-info" data-i18n-title="settings.info.tab" title="Info"></button>
@@ -1312,10 +1321,127 @@
</div>
</div><!-- /tts-server-section -->
<button class="btn btn-large btn-secondary full-width mt-2" onclick="testSound()" data-i18n="settings.tts.test_sound_btn">🔔 Esegui Test Suono</button>
<button class="btn btn-large btn-accent full-width mt-2" onclick="testTTS()" data-i18n="settings.tts.test_btn">🔊 Invia Test Vocale</button>
<div id="tts-test-status" style="display:none;margin-top:8px"></div>
<!-- HA TTS quick-fill hint -->
<div style="margin-top:12px;padding:10px 12px;background:rgba(3,169,244,0.07);border:1px solid rgba(3,169,244,0.25);border-radius:8px;font-size:0.82rem">
<span data-i18n="settings.ha.ha_hint">🏠 Se usi Home Assistant, usa il tab <strong>Home Assistant</strong> per configurare TTS, webhook e sensori.</span>
</div>
</div>
</div>
<!-- Home Assistant Tab -->
<div class="settings-panel" id="tab-ha">
<!-- Connection card -->
<div class="settings-card">
<h4 data-i18n="settings.ha.title">🏠 Home Assistant</h4>
<p class="settings-hint" data-i18n="settings.ha.hint">Integra EverShelf con Home Assistant: TTS su speaker smart, webhook per automazioni, sensori per la dashboard.</p>
<div class="form-group" style="margin-bottom:10px">
<label class="toggle-row">
<span data-i18n="settings.ha.enabled">✅ Abilita integrazione Home Assistant</span>
<span class="toggle-switch">
<input type="checkbox" id="setting-ha-enabled" onchange="onHaEnabledChange()">
<span class="toggle-slider"></span>
</span>
</label>
</div>
<div id="ha-config-section">
<div class="form-group">
<label data-i18n="settings.ha.url_label">🌐 Home Assistant URL</label>
<input type="url" id="setting-ha-url" class="form-input" placeholder="http://192.168.1.50:8123">
<p class="settings-hint" data-i18n="settings.ha.url_hint">URL base della tua istanza HA (senza slash finale). Es: <code>http://homeassistant.local:8123</code></p>
</div>
<div class="form-group">
<label data-i18n="settings.ha.token_label">🔑 Long-Lived Access Token</label>
<div style="display:flex;gap:8px;align-items:center">
<input type="password" id="setting-ha-token" class="form-input" style="flex:1" placeholder="eyJhbGci...">
<button class="btn btn-secondary" style="flex-shrink:0" onclick="togglePasswordVisibility('setting-ha-token')" data-i18n="btn.toggle_password">👁️</button>
</div>
<p class="settings-hint" data-i18n="settings.ha.token_hint">Genera un token in HA → Profilo → Token di accesso a lungo termine.</p>
</div>
<button class="btn btn-secondary full-width" onclick="testHaConnection()" data-i18n="settings.ha.test_btn">🔗 Testa connessione HA</button>
<div id="ha-test-status" style="display:none;margin-top:8px" class="settings-status"></div>
</div>
</div>
<!-- TTS via HA card -->
<div class="settings-card">
<h4 data-i18n="settings.ha.tts_title">🔊 TTS su Speaker Smart</h4>
<p class="settings-hint" data-i18n="settings.ha.tts_hint">Leggi i passi della ricetta su un altoparlante gestito da HA (Sonos, Echo, Google Home, ecc.).</p>
<div class="form-group">
<label data-i18n="settings.ha.tts_entity_label">🔈 Entity ID del media player</label>
<input type="text" id="setting-ha-tts-entity" class="form-input" placeholder="media_player.living_room">
<p class="settings-hint" data-i18n="settings.ha.tts_entity_hint">Copia l'entity ID del media player da HA → Strumenti sviluppatore → Stati.</p>
</div>
<div class="form-group">
<label data-i18n="settings.ha.tts_platform_label">🎙️ Piattaforma TTS</label>
<select id="setting-ha-tts-platform" class="form-input">
<option value="tts.speak" data-i18n="settings.ha.tts_platform_speak">tts.speak (raccomandato)</option>
<option value="notify" data-i18n="settings.ha.tts_platform_notify">notify.* (servizio notifiche)</option>
</select>
</div>
<button class="btn btn-secondary full-width" onclick="applyHaTtsPreset()" data-i18n="settings.ha.tts_apply_btn">✅ Applica preset HA al TTS</button>
<p class="settings-hint mt-2" data-i18n="settings.ha.tts_apply_hint">Configura automaticamente il tab TTS con i parametri HA corretti.</p>
</div>
<!-- Webhook card -->
<div class="settings-card">
<h4 data-i18n="settings.ha.webhook_title">⚡ Automazioni Webhook</h4>
<p class="settings-hint" data-i18n="settings.ha.webhook_hint">EverShelf chiama il webhook HA quando si verificano eventi (prodotto in scadenza, aggiunto alla lista, ecc.).</p>
<div class="form-group">
<label data-i18n="settings.ha.webhook_id_label">🔗 Webhook ID</label>
<input type="text" id="setting-ha-webhook-id" class="form-input" placeholder="evershelf_events">
<p class="settings-hint" data-i18n="settings.ha.webhook_id_hint">Crea un'automazione in HA con trigger "Webhook" e copia qui l'ID. <a href="#" onclick="showHaWebhookHelp();return false" style="color:var(--accent)">Come farlo?</a></p>
</div>
<div class="form-group">
<label data-i18n="settings.ha.webhook_events_label">📋 Eventi da notificare</label>
<div style="display:flex;flex-direction:column;gap:6px;margin-top:4px">
<label style="display:flex;align-items:center;gap:8px;font-weight:normal">
<input type="checkbox" id="ha-event-expiry" value="expiry"> <span data-i18n="settings.ha.event_expiry">Prodotti in scadenza (cron giornaliero)</span>
</label>
<label style="display:flex;align-items:center;gap:8px;font-weight:normal">
<input type="checkbox" id="ha-event-shopping" value="shopping_add"> <span data-i18n="settings.ha.event_shopping">Aggiunta alla lista della spesa</span>
</label>
<label style="display:flex;align-items:center;gap:8px;font-weight:normal">
<input type="checkbox" id="ha-event-stock" value="stock_update"> <span data-i18n="settings.ha.event_stock">Aggiornamento scorte (quantità modificata)</span>
</label>
</div>
</div>
<div class="form-group">
<label data-i18n="settings.ha.expiry_days_label">📅 Giorni anticipo per scadenze</label>
<input type="number" id="setting-ha-expiry-days" class="form-input" min="1" max="30" value="3">
<p class="settings-hint" data-i18n="settings.ha.expiry_days_hint">Quanti giorni prima della scadenza inviare l'alert.</p>
</div>
</div>
<!-- Notify service card -->
<div class="settings-card">
<h4 data-i18n="settings.ha.notify_title">📱 Notifiche Push</h4>
<p class="settings-hint" data-i18n="settings.ha.notify_hint">EverShelf invia notifiche push tramite il servizio <code>notify.*</code> di HA (Telegram, Pushover, app mobile, ecc.).</p>
<div class="form-group">
<label data-i18n="settings.ha.notify_service_label">📣 Servizio notify</label>
<input type="text" id="setting-ha-notify-service" class="form-input" placeholder="notify.mobile_app_mio_telefono">
<p class="settings-hint" data-i18n="settings.ha.notify_service_hint">Formato: <code>notify.NOME_SERVIZIO</code>. Lascia vuoto per disabilitare. Richiede token HA configurato.</p>
</div>
</div>
<!-- Sensor card (read-only info) -->
<div class="settings-card">
<h4 data-i18n="settings.ha.sensor_title">📊 Sensori REST per HA</h4>
<p class="settings-hint" data-i18n="settings.ha.sensor_hint">HA può leggere i dati dell'inventario via REST polling. Aggiungi questo snippet a <code>configuration.yaml</code>:</p>
<div id="ha-sensor-yaml" style="background:var(--bg-secondary,#f1f5f9);border-radius:8px;padding:12px;font-family:monospace;font-size:0.75rem;white-space:pre;overflow-x:auto;max-height:220px;overflow-y:auto;border:1px solid var(--border,#e2e8f0)"></div>
<button class="btn btn-secondary full-width mt-2" onclick="copyHaSensorYaml()" data-i18n="settings.ha.sensor_copy_btn">📋 Copia YAML</button>
</div>
<!-- Save button -->
<button class="btn btn-large btn-accent full-width" onclick="saveHaSettings()" data-i18n="settings.ha.save_btn">💾 Salva impostazioni HA</button>
<div id="ha-save-status" style="display:none;margin-top:8px" class="settings-status"></div>
</div>
<!-- Scale Tab -->
<div class="settings-panel" id="tab-scale">
<div class="settings-card">
@@ -1698,9 +1824,15 @@
</div>
<div id="recipe-result" style="display:none" class="recipe-result">
<div id="recipe-content"></div>
<button class="btn btn-large btn-secondary full-width mt-2" onclick="regenerateRecipe()" data-i18n="recipes.regenerate">
<button id="recipe-regen-btn" class="btn btn-large btn-secondary full-width mt-2" onclick="showRegenChoice()" data-i18n="recipes.regenerate">
🔄 Generane un'altra
</button>
<div id="recipe-regen-choice" style="display:none" class="recipe-regen-choice">
<p class="recipe-regen-choice-title" data-i18n="recipes.regen_choice_title">Cosa vuoi fare con questa ricetta?</p>
<button class="btn btn-large btn-warning full-width" onclick="doRegenerateReplace()" data-i18n="recipes.regen_replace">🔄 Genera un'altra (scarta questa)</button>
<button class="btn btn-large btn-success full-width mt-2" onclick="doRegenerateSave()" data-i18n="recipes.regen_save_new">💾 Salva nell'archivio e genera nuova</button>
<button class="btn btn-large btn-ghost full-width mt-2" onclick="cancelRegenChoice()" data-i18n="action.cancel">Annulla</button>
</div>
<button class="btn btn-large btn-primary full-width mt-2" onclick="closeRecipeDialog()" data-i18n="recipes.close_btn">
✅ Chiudi
</button>
@@ -1757,6 +1889,17 @@
</div>
</div>
<!-- ===== NETWORK ERROR OVERLAY ===== -->
<div id="network-error-overlay" style="display:none" aria-live="assertive" role="alert">
<div class="net-error-body">
<div class="net-error-icon" id="net-error-icon">📡</div>
<div class="net-error-title" id="net-error-title" data-i18n="error.offline_title">Nessuna connessione</div>
<div class="net-error-subtitle" id="net-error-subtitle" data-i18n="error.offline_subtitle">L'app non riesce a raggiungere il server. Verifica la connessione Wi-Fi.</div>
<div class="net-error-status" id="net-error-status"></div>
<button class="net-error-continue-btn" id="net-error-continue-btn" onclick="_enterOfflineMode()" data-i18n="error.offline_continue" style="display:none">Continua in modalità offline</button>
</div>
</div>
<!-- ===== COOKING MODE OVERLAY ===== -->
<div id="cooking-overlay" class="cooking-overlay" style="display:none" aria-live="polite">
<div id="cooking-flash-overlay" class="cooking-flash-overlay"></div>
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "EverShelf",
"short_name": "EverShelf",
"description": "Gestione completa della dispensa di casa con scansione barcode",
"version": "1.7.24",
"version": "1.7.25",
"start_url": "/evershelf/",
"display": "standalone",
"background_color": "#f0f4e8",
+119 -10
View File
@@ -213,7 +213,14 @@
"barcode_acquired": "🔖 Barcode gescannt: {code}",
"scan_barcode": "🔖 Barcode scannen",
"create_named": "{name} erstellen",
"new_without_barcode": "Neues Produkt ohne Barcode"
"new_without_barcode": "Neues Produkt ohne Barcode",
"stock_in_pantry": "Bereits im Vorrat:",
"status_ready": "Kamera auf Barcode richten",
"status_scanning": "Scanne...",
"status_partial": "Erkannt: {code} — prüfe...",
"status_invalid": "Ungültig: {code} — versuche erneut",
"status_confirmed": "Bestätigt!",
"status_parallel": "Kombinierter Scan aktiv..."
},
"action": {
"title": "Was möchtest du tun?",
@@ -227,7 +234,8 @@
"throw_btn": "🗑️ ENTSORGEN",
"throw_sub": "wegwerfen",
"edit_sub": "Ablauf, Ort…",
"create_recipe_btn": "Rezept"
"create_recipe_btn": "Rezept",
"related_stock_title": "Auch zuhause"
},
"add": {
"title": "Zum Vorrat hinzufügen",
@@ -359,12 +367,16 @@
"loading_msg": "Rezept wird vorbereitet...",
"start_cooking": "👨‍🍳 Kochmodus",
"regenerate": "🔄 Noch eins generieren",
"regen_choice_title": "Was möchtest du mit diesem Rezept machen?",
"regen_replace": "🔄 Neues generieren (dieses verwerfen)",
"regen_save_new": "💾 Im Archiv speichern & neues generieren",
"close_btn": "✅ Schließen",
"ingredients_title": "🧾 Zutaten",
"tools_title": "Benötigte Geräte",
"steps_title": "👨‍🍳 Zubereitung",
"no_steps": "Keine Zubereitungsschritte verfügbar",
"generate_error": "Fehler bei der Generierung",
"stream_interrupted": "Generierung unterbrochen (unvollstaendige Antwort vom Server). Protokolle pruefen oder erneut versuchen.",
"persons_short": "Pers.",
"use_ingredient_title": "Zutat verwenden",
"recipe_qty_label": "Rezept",
@@ -378,7 +390,10 @@
"scale_wait_stable": "10s stabiles Gewicht für Auto-Ausfüllen abwarten…",
"ingredient_scaled_toast": "📦 Zutat vom Vorrat abgezogen!",
"finished_added_bring_toast": "🛒 Produkt aufgebraucht → zu Bring! hinzugefügt!",
"load_error": "Fehler beim Laden"
"load_error": "Fehler beim Laden",
"favorite": "Zu Favoriten hinzufügen",
"unfavorite": "Aus Favoriten entfernen",
"adjust_persons": "Personen"
},
"shopping": {
"title": "🛒 Einkaufsliste",
@@ -473,7 +488,8 @@
"priority_medium": "Mittel",
"priority_low": "Niedrig",
"smart_last_update": "Aktualisiert {time}",
"names_already_updated": "Alle Namen sind bereits aktuell"
"names_already_updated": "Alle Namen sind bereits aktuell",
"pantry_hint": "Bereits zuhause: {qty}"
},
"ai": {
"title": "🤖 KI-Identifikation",
@@ -686,6 +702,7 @@
"extra_fields_label": " Zusätzliche Felder (JSON)",
"extra_fields_placeholder": "{\"entity_id\": \"media_player.living_room\"}",
"extra_fields_hint": "Zusätzliche Felder im Payload, im JSON-Format. Leer lassen wenn nicht benötigt.",
"test_sound_btn": "🔔 Klangtest ausführen",
"test_btn": "🔊 Testansage senden",
"voices_loading": "Stimmen werden geladen…",
"voice_not_supported": "Stimme vom Browser nicht unterstützt",
@@ -693,7 +710,12 @@
"voices_hint": "Verfügbare Stimmen hängen vom Betriebssystem und Browser ab. Auf macOS/iOS ist die Stimme Paola (Italienisch) verfügbar. Drücken Sie ↺ wenn die Liste nicht lädt.",
"url_missing": "⚠️ Endpunkt-URL fehlt.",
"test_sending": "⏳ Wird gesendet…",
"test_ok": "✅ Antwort {code} — prüfe ob der Lautsprecher gesprochen hat."
"test_ok": "✅ Antwort {code} — prüfe ob der Lautsprecher gesprochen hat.",
"heard_question": "Hast du die Stimme gehört?",
"heard_yes": "Ja, ich habe es gehört",
"heard_no": "Nein, ich habe nichts gehört",
"test_ok_kiosk": "TTS funktioniert.",
"test_fail_steps": "Prüfe: 1) Medienvolume ist nicht 0; 2) Google Text-to-Speech installiert und aktualisiert; 3) Deutsches Sprachpaket in den Android TTS-Einstellungen heruntergeladen."
},
"language": {
"title": "🌐 Sprache",
@@ -864,6 +886,60 @@
"forecast_label": "Prognose für bald leere Produkte",
"auto_add_label": "Automatisch hinzufügen wenn",
"auto_add_suffix": "im Lager verbleibend (0 = nur wenn leer)"
},
"ha": {
"tab": "Home Assistant",
"title": "Home Assistant",
"hint": "Verbinde EverShelf mit Home Assistant für Automationen, Push-Benachrichtigungen und REST-Sensoren.",
"enabled": "Home Assistant-Integration aktivieren",
"connection_title": "Verbindung",
"url_label": "Home Assistant URL",
"url_placeholder": "http://192.168.1.50:8123",
"url_hint": "Basis-URL deiner Home Assistant-Instanz (z.B. http://homeassistant.local:8123).",
"token_label": "Long-Lived Access Token",
"token_hint": "Erstelle unter HA-Profil → Sicherheit → Langlebige Zugangstoken.",
"token_placeholder": "eyJhbGci...",
"token_saved": "Token gespeichert (aus Sicherheitsgründen verborgen)",
"test_btn": "Verbindung testen",
"test_ok": "Verbunden mit {version}",
"test_fail": "Verbindung fehlgeschlagen: {error}",
"test_bad_token": "HA erreichbar, aber Token ist ungültig",
"testing": "Teste…",
"error_no_url": "Bitte zuerst die Home Assistant URL eingeben.",
"tts_title": "TTS auf Smart Speaker",
"tts_hint": "Rezeptschritte auf einem Home Assistant Media Player vorlesen.",
"tts_entity_label": "Media Player Entity ID",
"tts_entity_placeholder": "media_player.wohnzimmer",
"tts_entity_hint": "Entity-ID des HA-Media-Players. Zu finden unter HA: Entwicklertools → Zustände.",
"tts_platform_label": "TTS-Plattform",
"tts_platform_speak": "tts.speak (empfohlen)",
"tts_platform_notify": "notify.* (Benachrichtigungsdienst)",
"tts_apply_btn": "HA-Voreinstellung auf TTS-Tab anwenden",
"tts_apply_hint": "Füllt den TTS-Tab mit der Home Assistant URL und dem Token aus.",
"tts_preset_applied": "HA-Voreinstellung auf TTS-Tab angewendet.",
"webhook_title": "Webhook-Automationen",
"webhook_hint": "Sende Daten an Home Assistant, wenn Ereignisse in der Vorratskammer auftreten.",
"webhook_id_label": "Webhook-ID",
"webhook_id_placeholder": "evershelf_webhook_abc123",
"webhook_id_hint": "ID des in HA erstellten Webhooks. Kopiere aus: HA → Einstellungen → Automationen → Erstellen → Webhook-Auslöser.",
"webhook_events_label": "Benachrichtige bei diesen Ereignissen",
"event_expiry": "Ablaufende Produkte (täglich)",
"event_shopping": "Artikel zur Einkaufsliste hinzugefügt",
"event_stock": "Lagerbestand aktualisiert",
"expiry_days_label": "Ablaufwarnung im Voraus (Tage)",
"expiry_days_hint": "Sende die Ablaufwarnung N Tage vor dem Ablaufdatum.",
"webhook_help": "In HA: Einstellungen → Automationen → Automation erstellen → Auslöser: Webhook → ID kopieren.",
"notify_title": "Push-Benachrichtigungen",
"notify_hint": "Sende Push-Benachrichtigungen über einen Home Assistant notify-Dienst.",
"notify_service_label": "Notify-Dienst",
"notify_service_placeholder": "notify.mobile_app_mein_handy",
"notify_service_hint": "Name des HA-notify-Dienstes (z.B. notify.mobile_app_phone). Leer lassen zum Deaktivieren.",
"sensor_title": "REST-Sensoren",
"sensor_hint": "Zur configuration.yaml hinzufügen, um EverShelf-Sensoren in Home Assistant zu erstellen.",
"sensor_copy_btn": "YAML kopieren",
"sensor_copied": "YAML in die Zwischenablage kopiert!",
"save_btn": "HA-Einstellungen speichern",
"ha_hint": "Wenn du Home Assistant verwendest, nutze den Home Assistant-Tab für TTS, Webhooks und Sensoren."
}
},
"expiry": {
@@ -998,13 +1074,25 @@
"server_retry": "Erneut versuchen",
"unknown": "Unbekannter Fehler",
"prefix": "Fehler",
"no_inventory_entry": "Kein Inventareintrag gefunden"
"no_inventory_entry": "Kein Inventareintrag gefunden",
"offline_title": "Keine Verbindung",
"offline_subtitle": "Die App kann den Server nicht erreichen. Überprüfe deine WLAN-Verbindung.",
"offline_checking": "Verbindung prüfen…",
"offline_restored": "Verbindung wiederhergestellt!",
"offline_continue": "Im Offline-Modus fortfahren",
"offline_reading_cache": "Lese aus lokalem Cache",
"offline_ops_pending": "{n} Aktionen ausstehend",
"offline_synced": "{n} Aktionen synchronisiert",
"offline_ai_disabled": "Offline nicht verfügbar",
"offline_cache_ready": "Offline — {n} Produkte im Cache"
},
"confirm_placeholder_search": null,
"confirm": {
"remove_item": "Möchtest du dieses Produkt wirklich aus dem Bestand entfernen?",
"kiosk_exit": "Kioskmodus verlassen?",
"cancel": "Abbrechen",
"proceed": "Bestätigen"
"proceed": "Bestätigen",
"discard_one": "1 Stück wegwerfen"
},
"location": {
"dispensa": "Vorratskammer",
@@ -1016,7 +1104,8 @@
"unknown_hint": "Produktname und Informationen eingeben",
"label_name": "🏷️ Produktname",
"choose_location_title": "Welchen Ort?",
"choose_location_hint": "Wähle den zu bearbeitenden Ort:"
"choose_location_hint": "Wähle den zu bearbeitenden Ort:",
"confirm_large_qty": "Du setzt die Menge auf {qty} {unit}. Das scheint ungewöhnlich hoch zu sein. Bestätigen?"
},
"screensaver": {
"recipe_btn": "Rezepte",
@@ -1170,7 +1259,13 @@
"source": "Basierend auf {n} Produkten in deiner Vorratskammer · EverShelf",
"products_count": "Produkte",
"today_title": "🥗 Deine Vorratskammer heute",
"products_n": "{n} Produkte"
"products_n": "{n} Produkte",
"macros_title": "Geschätzte Makronährstoffe",
"macros_proteins": "Proteine",
"macros_carbs": "Kohlenhydrate",
"macros_fat": "Fett",
"macros_fiber": "Ballaststoffe",
"macros_source": "Schätzung basierend auf {n} Vorratsprodukten"
},
"facts": {
"greeting_morning": "Guten Morgen",
@@ -1344,6 +1439,20 @@
"critical_error_intro": "Die App kann aufgrund folgender Probleme nicht gestartet werden:",
"error_network": "Server nicht erreichbar.",
"error_network_detail": "Der Browser kann den PHP-Server nicht erreichen.\n\nMögliche Ursachen:\n• Apache/PHP-Server läuft nicht\n• Netzwerk- oder Firewall-Problem\n• Falsche App-URL\n\nBitte Server starten und erneut versuchen.",
"retry": "Erneut versuchen"
"retry": "Erneut versuchen",
"syncing_local": "Lokale Daten synchronisieren...",
"sync_done": "Lokale Daten aktualisiert"
},
"stats_monthly": {
"title": "Monatsstatistik",
"consumed": "Produkte verbraucht",
"trend_up": "+{pct}% vs. {prev}",
"trend_down": "-{pct}% vs. {prev}",
"trend_same": "gleiches Tempo wie letzten Monat",
"added": "hinzugefügt",
"wasted": "verschwendet",
"top_used": "meistbenutzt",
"top_cats": "Hauptkategorien",
"source": "Transaktionsverlauf · aktueller Monat"
}
}
+119 -10
View File
@@ -213,7 +213,14 @@
"barcode_acquired": "🔖 Barcode scanned: {code}",
"scan_barcode": "🔖 Scan Barcode",
"create_named": "Create {name}",
"new_without_barcode": "New product without barcode"
"new_without_barcode": "New product without barcode",
"stock_in_pantry": "Already in pantry:",
"status_ready": "Point camera at barcode",
"status_scanning": "Scanning...",
"status_partial": "Detected: {code} — verifying...",
"status_invalid": "Invalid: {code} — retrying",
"status_confirmed": "Confirmed!",
"status_parallel": "Using combined scan methods..."
},
"action": {
"title": "What do you want to do?",
@@ -227,7 +234,8 @@
"throw_btn": "🗑️ DISCARD",
"throw_sub": "throw away",
"edit_sub": "expiry, location…",
"create_recipe_btn": "Recipe"
"create_recipe_btn": "Recipe",
"related_stock_title": "Also at home"
},
"add": {
"title": "Add to Pantry",
@@ -359,12 +367,16 @@
"loading_msg": "Preparing your recipe...",
"start_cooking": "👨‍🍳 Cooking Mode",
"regenerate": "🔄 Generate another one",
"regen_choice_title": "What do you want to do with this recipe?",
"regen_replace": "🔄 Generate another (discard this one)",
"regen_save_new": "💾 Save to archive & generate a new one",
"close_btn": "✅ Close",
"ingredients_title": "🧾 Ingredients",
"tools_title": "Equipment needed",
"steps_title": "👨‍🍳 Steps",
"no_steps": "No steps available",
"generate_error": "Generation error",
"stream_interrupted": "Generation interrupted (incomplete server response). Check logs or try again.",
"persons_short": "serv.",
"use_ingredient_title": "Use ingredient",
"recipe_qty_label": "Recipe",
@@ -378,7 +390,10 @@
"scale_wait_stable": "Wait 10s of stable weight for auto-fill…",
"ingredient_scaled_toast": "📦 Ingredient deducted from pantry!",
"finished_added_bring_toast": "🛒 Finished product → added to Bring!",
"load_error": "Loading error"
"load_error": "Loading error",
"favorite": "Add to favourites",
"unfavorite": "Remove from favourites",
"adjust_persons": "Persons"
},
"shopping": {
"title": "🛒 Shopping List",
@@ -473,7 +488,8 @@
"priority_medium": "Medium",
"priority_low": "Low",
"smart_last_update": "Updated {time}",
"names_already_updated": "All names are already up to date"
"names_already_updated": "All names are already up to date",
"pantry_hint": "Already at home: {qty}"
},
"ai": {
"title": "🤖 AI Identification",
@@ -686,6 +702,7 @@
"extra_fields_label": " Extra fields (JSON)",
"extra_fields_placeholder": "{\"entity_id\": \"media_player.living_room\"}",
"extra_fields_hint": "Additional fields to include in the payload, in JSON format. Leave empty if not needed.",
"test_sound_btn": "🔔 Run Sound Test",
"test_btn": "🔊 Send Test Voice",
"voices_loading": "Loading voices…",
"voice_not_supported": "Voice not supported by this browser",
@@ -693,7 +710,12 @@
"voices_hint": "Available voices depend on the OS and browser. On macOS/iOS the Paola (Italian) voice is available. Press ↺ if the list does not load.",
"url_missing": "⚠️ Endpoint URL missing.",
"test_sending": "⏳ Sending…",
"test_ok": "✅ Response {code} — check that the speaker has spoken."
"test_ok": "✅ Response {code} — check that the speaker has spoken.",
"heard_question": "Did you hear the voice?",
"heard_yes": "Yes, I heard it",
"heard_no": "No, I didn't hear it",
"test_ok_kiosk": "TTS is working.",
"test_fail_steps": "Check: 1) media volume is not 0; 2) Google Text-to-Speech is installed and updated; 3) Italian voice package is downloaded in Android TTS settings."
},
"language": {
"title": "🌐 Language",
@@ -864,6 +886,60 @@
"forecast_label": "Forecast low-stock products",
"auto_add_label": "Auto-add to list when",
"auto_add_suffix": "remaining in stock (0 = only when empty)"
},
"ha": {
"tab": "Home Assistant",
"title": "Home Assistant",
"hint": "Connect EverShelf to Home Assistant for automations, push notifications and REST sensors.",
"enabled": "Enable Home Assistant integration",
"connection_title": "Connection",
"url_label": "Home Assistant URL",
"url_placeholder": "http://192.168.1.50:8123",
"url_hint": "Base URL of your Home Assistant instance (e.g. http://homeassistant.local:8123).",
"token_label": "Long-Lived Access Token",
"token_hint": "Generate from HA Profile → Security → Long-Lived Access Tokens.",
"token_placeholder": "eyJhbGci...",
"token_saved": "Token saved (hidden for security)",
"test_btn": "Test connection",
"test_ok": "Connected to {version}",
"test_fail": "Connection failed: {error}",
"test_bad_token": "HA reachable but token is invalid",
"testing": "Testing…",
"error_no_url": "Please enter the Home Assistant URL first.",
"tts_title": "TTS on Smart Speaker",
"tts_hint": "Read recipe steps aloud on a Home Assistant media player.",
"tts_entity_label": "Media player entity ID",
"tts_entity_placeholder": "media_player.living_room",
"tts_entity_hint": "Entity ID of the HA media player. Find it in HA: Developer Tools → States.",
"tts_platform_label": "TTS platform",
"tts_platform_speak": "tts.speak (recommended)",
"tts_platform_notify": "notify.* (notification service)",
"tts_apply_btn": "Apply HA preset to TTS tab",
"tts_apply_hint": "Pre-fills the TTS tab with the Home Assistant URL and token.",
"tts_preset_applied": "HA preset applied to TTS tab.",
"webhook_title": "Webhook Automations",
"webhook_hint": "Send data to Home Assistant when pantry events occur. Create an HA automation with a Webhook trigger and paste the generated ID here.",
"webhook_id_label": "Webhook ID",
"webhook_id_placeholder": "evershelf_webhook_abc123",
"webhook_id_hint": "ID of the webhook created in HA. Copy from: HA → Settings → Automations → Create → Webhook Trigger.",
"webhook_events_label": "Notify on these events",
"event_expiry": "Expiring products (daily)",
"event_shopping": "Item added to shopping list",
"event_stock": "Stock level updated",
"expiry_days_label": "Expiry lead time (days)",
"expiry_days_hint": "Send the expiry alert N days before the expiry date.",
"webhook_help": "In HA: Settings → Automations → Create automation → Trigger: Webhook → copy the generated ID above.",
"notify_title": "Push Notifications",
"notify_hint": "Send push notifications to your phone via a Home Assistant notify service.",
"notify_service_label": "Notify service",
"notify_service_placeholder": "notify.mobile_app_my_phone",
"notify_service_hint": "HA notify service name (e.g. notify.mobile_app_phone). Leave empty to disable.",
"sensor_title": "REST Sensors",
"sensor_hint": "Add to configuration.yaml to create EverShelf sensors in Home Assistant.",
"sensor_copy_btn": "Copy YAML",
"sensor_copied": "YAML copied to clipboard!",
"save_btn": "Save HA settings",
"ha_hint": "If you use Home Assistant, use the Home Assistant tab to configure TTS, webhooks and sensors."
}
},
"expiry": {
@@ -998,13 +1074,25 @@
"server_retry": "Retry",
"unknown": "Unknown error",
"prefix": "Error",
"no_inventory_entry": "No inventory entry found"
"no_inventory_entry": "No inventory entry found",
"offline_title": "No connection",
"offline_subtitle": "The app cannot reach the server. Check your Wi-Fi connection.",
"offline_checking": "Checking connection…",
"offline_restored": "Connection restored!",
"offline_continue": "Continue in offline mode",
"offline_reading_cache": "Reading from local cache",
"offline_ops_pending": "{n} operations pending",
"offline_synced": "{n} operations synced",
"offline_ai_disabled": "Not available offline",
"offline_cache_ready": "Offline — {n} items cached"
},
"confirm_placeholder_search": null,
"confirm": {
"remove_item": "Do you really want to remove this product from inventory?",
"kiosk_exit": "Exit kiosk mode?",
"cancel": "Cancel",
"proceed": "Confirm"
"proceed": "Confirm",
"discard_one": "Discard 1 piece"
},
"location": {
"dispensa": "Pantry",
@@ -1016,7 +1104,8 @@
"unknown_hint": "Enter the product name and information",
"label_name": "🏷️ Product name",
"choose_location_title": "Which location?",
"choose_location_hint": "Choose the location to edit:"
"choose_location_hint": "Choose the location to edit:",
"confirm_large_qty": "You are setting the quantity to {qty} {unit}. This seems unusually high. Confirm?"
},
"screensaver": {
"recipe_btn": "Recipes",
@@ -1170,7 +1259,13 @@
"source": "Based on {n} products in your pantry · EverShelf",
"products_count": "products",
"today_title": "🥗 Your pantry today",
"products_n": "{n} products"
"products_n": "{n} products",
"macros_title": "Estimated Macronutrients",
"macros_proteins": "Proteins",
"macros_carbs": "Carbohydrates",
"macros_fat": "Fat",
"macros_fiber": "Fibre",
"macros_source": "Estimate based on {n} pantry products"
},
"facts": {
"greeting_morning": "Good morning",
@@ -1344,6 +1439,20 @@
"critical_error_intro": "The app cannot start due to the following issues:",
"error_network": "Cannot reach the server.",
"error_network_detail": "The browser cannot reach the PHP server.\n\nPossible causes:\n• Apache/PHP server is not running\n• Network or firewall issue\n• Incorrect app URL\n\nMake sure the server is started and try again.",
"retry": "Retry"
"retry": "Retry",
"syncing_local": "Syncing local data...",
"sync_done": "Local data synced"
},
"stats_monthly": {
"title": "Monthly Stats",
"consumed": "products used",
"trend_up": "+{pct}% vs {prev}",
"trend_down": "-{pct}% vs {prev}",
"trend_same": "same pace as last month",
"added": "added",
"wasted": "wasted",
"top_used": "top used",
"top_cats": "Top categories",
"source": "Transaction history · current month"
}
}
+112 -8
View File
@@ -211,7 +211,13 @@
"barcode_acquired": "🔖 Código de barras escaneado: {code}",
"scan_barcode": "🔖 Escanear código de barras",
"create_named": "Crear {name}",
"new_without_barcode": "Nuevo producto sin código de barras"
"new_without_barcode": "Nuevo producto sin código de barras",
"status_ready": "Apunta la cámara al código de barras",
"status_scanning": "Escaneando...",
"status_partial": "Detectado: {code} — verificando...",
"status_invalid": "Inválido: {code} — reintentando",
"status_confirmed": "Confirmado!",
"status_parallel": "Escaneo combinado activo..."
},
"action": {
"title": "¿Qué quieres hacer?",
@@ -357,6 +363,9 @@
"loading_msg": "Preparando tu receta...",
"start_cooking": "👨‍🍳 Modo cocina",
"regenerate": "🔄 Generar otra",
"regen_choice_title": "¿Qué quieres hacer con esta receta?",
"regen_replace": "🔄 Generar otra (descartar esta)",
"regen_save_new": "💾 Guardar en el archivo y generar una nueva",
"close_btn": "✅ Cerrar",
"ingredients_title": "🧾 Ingredientes",
"tools_title": "Equipo necesario",
@@ -376,7 +385,10 @@
"scale_wait_stable": "Espera 10s de peso estable para el relleno automático…",
"ingredient_scaled_toast": "📦 ¡Ingrediente deducido de la despensa!",
"finished_added_bring_toast": "🛒 Producto terminado → ¡añadido a Bring!",
"load_error": "Error de carga"
"load_error": "Error de carga",
"favorite": "Añadir a favoritos",
"unfavorite": "Quitar de favoritos",
"adjust_persons": "Personas"
},
"shopping": {
"title": "🛒 Lista de la compra",
@@ -691,7 +703,12 @@
"voices_hint": "Las voces disponibles dependen del SO y el navegador. Pulsa ↺ si la lista no carga.",
"url_missing": "⚠️ URL del endpoint faltante.",
"test_sending": "⏳ Enviando…",
"test_ok": "✅ Respuesta {code} — comprueba que el altavoz haya hablado."
"test_ok": "✅ Respuesta {code} — comprueba que el altavoz haya hablado.",
"heard_question": "¿Has escuchado la voz?",
"heard_yes": "Sí, la escuché",
"heard_no": "No, no escuché nada",
"test_ok_kiosk": "TTS funcionando.",
"test_fail_steps": "Comprueba: 1) el volumen del multimedia no es 0; 2) Google Text-to-Speech está instalado y actualizado; 3) el paquete de voz español está descargado en la configuración TTS de Android."
},
"language": {
"title": "🌐 Idioma",
@@ -821,6 +838,60 @@
"forecast_label": "Previsión de productos por agotar",
"auto_add_label": "Añadir automáticamente cuando",
"auto_add_suffix": "restante en stock (0 = solo cuando se agota)"
},
"ha": {
"tab": "Home Assistant",
"title": "Home Assistant",
"hint": "Conecta EverShelf a Home Assistant para automatizaciones, notificaciones push y sensores REST.",
"enabled": "Activar integración con Home Assistant",
"connection_title": "Conexión",
"url_label": "URL de Home Assistant",
"url_placeholder": "http://192.168.1.50:8123",
"url_hint": "URL base de tu instancia de Home Assistant.",
"token_label": "Token de acceso de larga duración",
"token_hint": "Genera desde Perfil HA → Seguridad → Tokens de acceso de larga duración.",
"token_placeholder": "eyJhbGci...",
"token_saved": "Token guardado (oculto por seguridad)",
"test_btn": "Probar conexión",
"test_ok": "Conectado a {version}",
"test_fail": "Conexión fallida: {error}",
"test_bad_token": "HA accesible pero el token no es válido",
"testing": "Probando…",
"error_no_url": "Por favor, introduce primero la URL de Home Assistant.",
"tts_title": "TTS en altavoz inteligente",
"tts_hint": "Lee los pasos de la receta en un reproductor de medios de Home Assistant.",
"tts_entity_label": "Entity ID del reproductor multimedia",
"tts_entity_placeholder": "media_player.salon",
"tts_entity_hint": "ID de entidad del reproductor multimedia HA. Encuéntralo en HA: Herramientas para desarrolladores → Estados.",
"tts_platform_label": "Plataforma TTS",
"tts_platform_speak": "tts.speak (recomendado)",
"tts_platform_notify": "notify.* (servicio de notificaciones)",
"tts_apply_btn": "Aplicar preset HA a la pestaña TTS",
"tts_apply_hint": "Pre-rellena la pestaña TTS con la URL y el token de Home Assistant.",
"tts_preset_applied": "Preset HA aplicado a la pestaña TTS.",
"webhook_title": "Automatizaciones Webhook",
"webhook_hint": "Envía datos a Home Assistant cuando ocurren eventos en la despensa.",
"webhook_id_label": "ID de Webhook",
"webhook_id_placeholder": "evershelf_webhook_abc123",
"webhook_id_hint": "ID del webhook creado en HA. Copia desde: HA → Ajustes → Automatizaciones → Crear → Disparador Webhook.",
"webhook_events_label": "Notificar en estos eventos",
"event_expiry": "Productos próximos a caducar (diario)",
"event_shopping": "Artículo añadido a la lista de compras",
"event_stock": "Nivel de stock actualizado",
"expiry_days_label": "Antelación de caducidad (días)",
"expiry_days_hint": "Enviar alerta de caducidad N días antes de la fecha.",
"webhook_help": "En HA: Ajustes → Automatizaciones → Crear automatización → Disparador: Webhook → copia el ID generado.",
"notify_title": "Notificaciones push",
"notify_hint": "Envía notificaciones push a tu teléfono mediante un servicio notify de Home Assistant.",
"notify_service_label": "Servicio notify",
"notify_service_placeholder": "notify.mobile_app_mi_telefono",
"notify_service_hint": "Nombre del servicio notify de HA. Déjalo vacío para desactivar.",
"sensor_title": "Sensores REST",
"sensor_hint": "Añade a configuration.yaml para crear sensores de EverShelf en Home Assistant.",
"sensor_copy_btn": "Copiar YAML",
"sensor_copied": "¡YAML copiado al portapapeles!",
"save_btn": "Guardar ajustes HA",
"ha_hint": "Si usas Home Assistant, utiliza la pestaña Home Assistant para configurar TTS, webhooks y sensores."
}
},
"expiry": {
@@ -954,13 +1025,25 @@
"server_retry": "Reintentar",
"unknown": "Error desconocido",
"prefix": "Error",
"no_inventory_entry": "No se encontró ninguna entrada de inventario"
"no_inventory_entry": "No se encontró ninguna entrada de inventario",
"offline_title": "Sin conexión",
"offline_subtitle": "La app no puede conectar con el servidor. Verifica tu conexión Wi-Fi.",
"offline_checking": "Verificando conexión…",
"offline_restored": "¡Conexión restaurada!",
"offline_continue": "Continuar en modo sin conexión",
"offline_reading_cache": "Leyendo desde caché local",
"offline_ops_pending": "{n} operaciones pendientes",
"offline_synced": "{n} operaciones sincronizadas",
"offline_ai_disabled": "No disponible sin conexión",
"offline_cache_ready": "Offline — {n} productos en caché"
},
"confirm_placeholder_search": null,
"confirm": {
"remove_item": "¿Realmente quieres eliminar este producto del inventario?",
"kiosk_exit": "¿Salir del modo kiosco?",
"cancel": "Cancelar",
"proceed": "Confirmar"
"proceed": "Confirmar",
"discard_one": "Tirar 1 unidad"
},
"location": {
"dispensa": "Despensa",
@@ -972,7 +1055,8 @@
"unknown_hint": "Introduce el nombre del producto y la información",
"label_name": "🏷️ Nombre del producto",
"choose_location_title": "¿Qué ubicación?",
"choose_location_hint": "Elige la ubicación a editar:"
"choose_location_hint": "Elige la ubicación a editar:",
"confirm_large_qty": "Estás configurando la cantidad a {qty} {unit}. Esto parece inusualmente alto. ¿Confirmar?"
},
"screensaver": {
"recipe_btn": "Recetas",
@@ -1123,7 +1207,13 @@
"source": "Basado en {n} productos en tu despensa · EverShelf",
"products_count": "productos",
"today_title": "🥗 Tu despensa hoy",
"products_n": "{n} productos"
"products_n": "{n} productos",
"macros_title": "Macronutrientes estimados",
"macros_proteins": "Proteínas",
"macros_carbs": "Carbohidratos",
"macros_fat": "Grasas",
"macros_fiber": "Fibra",
"macros_source": "Estimación basada en {n} productos en despensa"
},
"facts": {
"greeting_morning": "Buenos días",
@@ -1292,6 +1382,20 @@
"critical_error_short": "Error crítico",
"critical_error": "Error crítico: la aplicación no puede iniciarse. Revisa los registros del servidor.",
"error_network": "No se puede contactar con el servidor. Comprueba tu conexión de red.",
"retry": "Reintentar"
"retry": "Reintentar",
"syncing_local": "Sincronizando datos locales...",
"sync_done": "Datos locales sincronizados"
},
"stats_monthly": {
"title": "Estadísticas Mensuales",
"consumed": "productos usados",
"trend_up": "+{pct}% vs {prev}",
"trend_down": "-{pct}% vs {prev}",
"trend_same": "mismo ritmo que el mes pasado",
"added": "añadidos",
"wasted": "desperdiciados",
"top_used": "más usado",
"top_cats": "Categorías principales",
"source": "Historial de transacciones · mes actual"
}
}
+112 -8
View File
@@ -211,7 +211,13 @@
"barcode_acquired": "🔖 Code-barres scanné : {code}",
"scan_barcode": "🔖 Scanner le code-barres",
"create_named": "Créer {name}",
"new_without_barcode": "Nouveau produit sans code-barres"
"new_without_barcode": "Nouveau produit sans code-barres",
"status_ready": "Pointez la caméra sur le code-barres",
"status_scanning": "Scan en cours...",
"status_partial": "Lu : {code} — vérification...",
"status_invalid": "Invalide : {code} — nouvel essai",
"status_confirmed": "Confirmé !",
"status_parallel": "Scan combiné actif..."
},
"action": {
"title": "Que voulez-vous faire ?",
@@ -357,6 +363,9 @@
"loading_msg": "Préparation de votre recette...",
"start_cooking": "👨‍🍳 Mode cuisine",
"regenerate": "🔄 En générer une autre",
"regen_choice_title": "Que veux-tu faire de cette recette ?",
"regen_replace": "🔄 En générer une autre (ignorer celle-ci)",
"regen_save_new": "💾 Sauvegarder dans l'archive et en générer une nouvelle",
"close_btn": "✅ Fermer",
"ingredients_title": "🧾 Ingrédients",
"tools_title": "Matériel nécessaire",
@@ -376,7 +385,10 @@
"scale_wait_stable": "Attendez 10s de poids stable pour le remplissage automatique…",
"ingredient_scaled_toast": "📦 Ingrédient déduit du garde-manger !",
"finished_added_bring_toast": "🛒 Produit terminé → ajouté à Bring !",
"load_error": "Erreur de chargement"
"load_error": "Erreur de chargement",
"favorite": "Ajouter aux favoris",
"unfavorite": "Retirer des favoris",
"adjust_persons": "Personnes"
},
"shopping": {
"title": "🛒 Liste de courses",
@@ -691,7 +703,12 @@
"voices_hint": "Les voix disponibles dépendent du système d'exploitation et du navigateur. Appuyez sur ↺ si la liste ne se charge pas.",
"url_missing": "⚠️ URL de l'endpoint manquante.",
"test_sending": "⏳ Envoi…",
"test_ok": "✅ Réponse {code} — vérifiez que le haut-parleur a parlé."
"test_ok": "✅ Réponse {code} — vérifiez que le haut-parleur a parlé.",
"heard_question": "Avez-vous entendu la voix ?",
"heard_yes": "Oui, je l'ai entendu",
"heard_no": "Non, je n'ai rien entendu",
"test_ok_kiosk": "TTS fonctionne.",
"test_fail_steps": "Vérifiez : 1) le volume média n'est pas 0 ; 2) Google Text-to-Speech est installé et mis à jour ; 3) le pack vocal français est téléchargé dans les paramètres TTS Android."
},
"language": {
"title": "🌐 Langue",
@@ -821,6 +838,60 @@
"forecast_label": "Prévision des produits bientôt épuisés",
"auto_add_label": "Ajouter automatiquement quand",
"auto_add_suffix": "restant en stock (0 = seulement quand épuisé)"
},
"ha": {
"tab": "Home Assistant",
"title": "Home Assistant",
"hint": "Connectez EverShelf à Home Assistant pour les automations, les notifications push et les capteurs REST.",
"enabled": "Activer l'intégration Home Assistant",
"connection_title": "Connexion",
"url_label": "URL Home Assistant",
"url_placeholder": "http://192.168.1.50:8123",
"url_hint": "URL de base de votre instance Home Assistant.",
"token_label": "Jeton d'accès longue durée",
"token_hint": "Générez depuis Profil HA → Sécurité → Jetons d'accès longue durée.",
"token_placeholder": "eyJhbGci...",
"token_saved": "Jeton enregistré (masqué pour des raisons de sécurité)",
"test_btn": "Tester la connexion",
"test_ok": "Connecté à {version}",
"test_fail": "Connexion échouée : {error}",
"test_bad_token": "HA accessible mais le jeton est invalide",
"testing": "Test en cours…",
"error_no_url": "Veuillez d'abord saisir l'URL de Home Assistant.",
"tts_title": "TTS sur enceinte connectée",
"tts_hint": "Lisez les étapes de recette sur un media player Home Assistant.",
"tts_entity_label": "Entity ID du lecteur multimédia",
"tts_entity_placeholder": "media_player.salon",
"tts_entity_hint": "Entity ID du lecteur multimédia HA. Disponible dans HA : Outils développeur → États.",
"tts_platform_label": "Plateforme TTS",
"tts_platform_speak": "tts.speak (recommandé)",
"tts_platform_notify": "notify.* (service de notification)",
"tts_apply_btn": "Appliquer le preset HA à l'onglet TTS",
"tts_apply_hint": "Pré-remplit l'onglet TTS avec l'URL et le jeton de Home Assistant.",
"tts_preset_applied": "Preset HA appliqué à l'onglet TTS.",
"webhook_title": "Automations Webhook",
"webhook_hint": "Envoyez des données à Home Assistant lors d'événements dans le garde-manger.",
"webhook_id_label": "ID Webhook",
"webhook_id_placeholder": "evershelf_webhook_abc123",
"webhook_id_hint": "ID du webhook créé dans HA. Copiez depuis : HA → Paramètres → Automations → Créer → Déclencheur Webhook.",
"webhook_events_label": "Notifier pour ces événements",
"event_expiry": "Produits expirant bientôt (quotidien)",
"event_shopping": "Article ajouté à la liste de courses",
"event_stock": "Niveau de stock mis à jour",
"expiry_days_label": "Préavis d'expiration (jours)",
"expiry_days_hint": "Envoyer l'alerte d'expiration N jours avant la date d'expiration.",
"webhook_help": "Dans HA : Paramètres → Automations → Créer → Déclencheur : Webhook → copier l'ID généré.",
"notify_title": "Notifications push",
"notify_hint": "Envoyez des notifications push sur votre téléphone via un service notify de Home Assistant.",
"notify_service_label": "Service notify",
"notify_service_placeholder": "notify.mobile_app_mon_telephone",
"notify_service_hint": "Nom du service notify HA. Laissez vide pour désactiver.",
"sensor_title": "Capteurs REST",
"sensor_hint": "Ajoutez à configuration.yaml pour créer des capteurs EverShelf dans Home Assistant.",
"sensor_copy_btn": "Copier le YAML",
"sensor_copied": "YAML copié dans le presse-papiers !",
"save_btn": "Enregistrer les paramètres HA",
"ha_hint": "Si vous utilisez Home Assistant, utilisez l'onglet Home Assistant pour configurer TTS, webhooks et capteurs."
}
},
"expiry": {
@@ -954,13 +1025,25 @@
"server_retry": "Réessayer",
"unknown": "Erreur inconnue",
"prefix": "Erreur",
"no_inventory_entry": "Aucune entrée d'inventaire trouvée"
"no_inventory_entry": "Aucune entrée d'inventaire trouvée",
"offline_title": "Aucune connexion",
"offline_subtitle": "L'app ne peut pas atteindre le serveur. Vérifiez votre connexion Wi-Fi.",
"offline_checking": "Vérification de la connexion…",
"offline_restored": "Connexion rétablie !",
"offline_continue": "Continuer en mode hors ligne",
"offline_reading_cache": "Lecture depuis le cache local",
"offline_ops_pending": "{n} opérations en attente",
"offline_synced": "{n} opérations synchronisées",
"offline_ai_disabled": "Indisponible hors ligne",
"offline_cache_ready": "Offline — {n} produits en cache"
},
"confirm_placeholder_search": null,
"confirm": {
"remove_item": "Voulez-vous vraiment supprimer ce produit de l'inventaire ?",
"kiosk_exit": "Quitter le mode kiosque ?",
"cancel": "Annuler",
"proceed": "Confirmer"
"proceed": "Confirmer",
"discard_one": "Jeter 1 pièce"
},
"location": {
"dispensa": "Garde-manger",
@@ -972,7 +1055,8 @@
"unknown_hint": "Entrez le nom du produit et les informations",
"label_name": "🏷️ Nom du produit",
"choose_location_title": "Quel emplacement ?",
"choose_location_hint": "Choisissez l'emplacement à modifier :"
"choose_location_hint": "Choisissez l'emplacement à modifier :",
"confirm_large_qty": "Vous définissez la quantité à {qty} {unit}. Cela semble inhabituellement élevé. Confirmer ?"
},
"screensaver": {
"recipe_btn": "Recettes",
@@ -1123,7 +1207,13 @@
"source": "Basé sur {n} produits dans votre garde-manger · EverShelf",
"products_count": "produits",
"today_title": "🥗 Votre garde-manger aujourd'hui",
"products_n": "{n} produits"
"products_n": "{n} produits",
"macros_title": "Macronutriments estimés",
"macros_proteins": "Protéines",
"macros_carbs": "Glucides",
"macros_fat": "Lipides",
"macros_fiber": "Fibres",
"macros_source": "Estimation basée sur {n} produits en stock"
},
"facts": {
"greeting_morning": "Bonjour",
@@ -1292,6 +1382,20 @@
"critical_error_short": "Erreur critique",
"critical_error": "Erreur critique : l'application ne peut pas démarrer. Vérifiez les logs.",
"error_network": "Impossible de contacter le serveur. Vérifiez votre connexion réseau.",
"retry": "Réessayer"
"retry": "Réessayer",
"syncing_local": "Synchronisation des données locales...",
"sync_done": "Données locales synchronisées"
},
"stats_monthly": {
"title": "Statistiques Mensuelles",
"consumed": "produits utilisés",
"trend_up": "+{pct}% vs {prev}",
"trend_down": "-{pct}% vs {prev}",
"trend_same": "même rythme que le mois dernier",
"added": "ajoutés",
"wasted": "gaspillés",
"top_used": "le plus utilisé",
"top_cats": "Catégories principales",
"source": "Historique des transactions · mois en cours"
}
}
+118 -10
View File
@@ -213,7 +213,14 @@
"barcode_acquired": "🔖 Barcode acquisito: {code}",
"scan_barcode": "🔖 Scansiona Barcode",
"create_named": "Crea {name}",
"new_without_barcode": "Nuovo prodotto senza barcode"
"new_without_barcode": "Nuovo prodotto senza barcode",
"stock_in_pantry": "Hai gia in dispensa:",
"status_ready": "Inquadra il codice a barre",
"status_scanning": "Scansione in corso...",
"status_partial": "Letto: {code} — verifico...",
"status_invalid": "Non valido: {code} — riprovo",
"status_confirmed": "Confermato!",
"status_parallel": "Doppia scansione attiva..."
},
"action": {
"title": "Cosa vuoi fare?",
@@ -227,7 +234,8 @@
"throw_btn": "🗑️ BUTTA",
"throw_sub": "butta il prodotto",
"edit_sub": "scadenza, luogo…",
"create_recipe_btn": "Ricetta"
"create_recipe_btn": "Ricetta",
"related_stock_title": "Hai anche in casa"
},
"add": {
"title": "Aggiungi alla Dispensa",
@@ -359,12 +367,16 @@
"loading_msg": "Sto preparando la ricetta...",
"start_cooking": "👨‍🍳 Modalità Cucina",
"regenerate": "🔄 Generane un'altra",
"regen_choice_title": "Cosa vuoi fare con questa ricetta?",
"regen_replace": "🔄 Genera un'altra (scarta questa)",
"regen_save_new": "💾 Salva nell'archivio e genera una nuova",
"close_btn": "✅ Chiudi",
"ingredients_title": "🧾 Ingredienti",
"tools_title": "Strumenti necessari",
"steps_title": "👨‍🍳 Procedimento",
"no_steps": "Nessun procedimento disponibile",
"generate_error": "Errore nella generazione",
"stream_interrupted": "Generazione interrotta (risposta incompleta dal server). Controlla i log o riprova.",
"persons_short": "pers.",
"use_ingredient_title": "Usa ingrediente",
"recipe_qty_label": "Ricetta",
@@ -378,7 +390,10 @@
"scale_wait_stable": "Attendi 10s di stabilità per la compilazione automatica…",
"ingredient_scaled_toast": "📦 Ingrediente scalato dalla dispensa!",
"finished_added_bring_toast": "🛒 Prodotto finito → aggiunto a Bring!",
"load_error": "Errore nel caricamento"
"load_error": "Errore nel caricamento",
"favorite": "Aggiungi ai preferiti",
"unfavorite": "Rimuovi dai preferiti",
"adjust_persons": "Persone"
},
"shopping": {
"title": "🛒 Lista della Spesa",
@@ -473,7 +488,8 @@
"priority_medium": "Media",
"priority_low": "Bassa",
"smart_last_update": "Aggiornato {time}",
"names_already_updated": "Tutti i nomi sono già aggiornati"
"names_already_updated": "Tutti i nomi sono già aggiornati",
"pantry_hint": "Hai gia {qty} in dispensa"
},
"ai": {
"title": "🤖 Identificazione AI",
@@ -686,6 +702,7 @@
"extra_fields_label": " Campi extra (JSON)",
"extra_fields_placeholder": "{\"entity_id\": \"media_player.living_room\"}",
"extra_fields_hint": "Campi aggiuntivi da includere nel payload, in formato JSON. Lascia vuoto se non necessario.",
"test_sound_btn": "🔔 Esegui Test Suono",
"test_btn": "🔊 Invia Test Vocale",
"voices_loading": "Caricamento voci…",
"voice_not_supported": "Voce non supportata dal browser",
@@ -693,7 +710,12 @@
"voices_hint": "Le voci disponibili dipendono dal sistema operativo e dal browser. Su macOS/iOS è disponibile la voce Paola (italiano). Premi ↺ se la lista non si carica.",
"url_missing": "⚠️ URL endpoint mancante.",
"test_sending": "⏳ Invio in corso…",
"test_ok": "✅ Risposta {code} — controlla che l'altoparlante abbia parlato."
"test_ok": "✅ Risposta {code} — controlla che l'altoparlante abbia parlato.",
"heard_question": "Hai sentito la voce?",
"heard_yes": "Sì, ho sentito",
"heard_no": "No, non ho sentito",
"test_ok_kiosk": "TTS funzionante.",
"test_fail_steps": "Controlla: 1) volume media del dispositivo non sia 0; 2) Google Text-to-Speech installato e aggiornato; 3) pacchetto vocale italiano scaricato nelle impostazioni TTS Android."
},
"language": {
"title": "🌐 Lingua / Language",
@@ -864,6 +886,60 @@
"forecast_label": "Previsione prodotti in esaurimento",
"auto_add_label": "Aggiungi automaticamente quando",
"auto_add_suffix": "rimasto in magazzino (0 = solo quando esaurito)"
},
"ha": {
"tab": "Home Assistant",
"title": "Home Assistant",
"hint": "Collega EverShelf a Home Assistant per automazioni, notifiche push e sensori REST.",
"enabled": "Abilita integrazione Home Assistant",
"connection_title": "Connessione",
"url_label": "URL Home Assistant",
"url_placeholder": "http://192.168.1.50:8123",
"url_hint": "URL del tuo server Home Assistant (es. http://homeassistant.local:8123).",
"token_label": "Long-Lived Access Token",
"token_hint": "Genera da Profilo HA → Sicurezza → Token di accesso a lungo termine.",
"token_placeholder": "eyJhbGci...",
"token_saved": "Token salvato (non mostrato per sicurezza)",
"test_btn": "Testa connessione",
"test_ok": "Connesso a {version}",
"test_fail": "Connessione fallita: {error}",
"test_bad_token": "HA raggiungibile ma token non valido",
"testing": "Test in corso…",
"error_no_url": "Inserisci prima l'URL di Home Assistant.",
"tts_title": "TTS su Speaker Smart",
"tts_hint": "Leggi i passi delle ricette su un media player di Home Assistant.",
"tts_entity_label": "Entity ID media player",
"tts_entity_placeholder": "media_player.living_room",
"tts_entity_hint": "Entity ID del media player su cui vuoi la voce. Puoi trovarlo in HA: Strumenti per sviluppatori → Stati.",
"tts_platform_label": "Piattaforma TTS",
"tts_platform_speak": "tts.speak (raccomandato)",
"tts_platform_notify": "notify.* (servizio notifiche)",
"tts_apply_btn": "Applica preset HA al tab TTS",
"tts_apply_hint": "Pre-compila il tab TTS con l'URL e il token di Home Assistant.",
"tts_preset_applied": "Preset HA applicato al tab TTS.",
"webhook_title": "Automazioni Webhook",
"webhook_hint": "Invia dati a Home Assistant quando avvengono eventi nella dispensa. Crea un'automazione in HA con trigger Webhook e copia l'ID generato.",
"webhook_id_label": "Webhook ID",
"webhook_id_placeholder": "evershelf_webhook_abc123",
"webhook_id_hint": "ID del webhook creato in HA. Copia da: HA → Impostazioni → Automazioni → Crea → Trigger Webhook.",
"webhook_events_label": "Notifica per questi eventi",
"event_expiry": "Prodotti in scadenza (giornaliero)",
"event_shopping": "Aggiunta alla lista della spesa",
"event_stock": "Aggiornamento scorte",
"expiry_days_label": "Anticipo scadenze (giorni)",
"expiry_days_hint": "Invia la notifica di scadenza N giorni prima della data di scadenza.",
"webhook_help": "In HA: Impostazioni → Automazioni → Crea automazione → Trigger: Webhook → copia l'ID generato qui sopra.",
"notify_title": "Notifiche Push",
"notify_hint": "Invia notifiche push al tuo telefono tramite il servizio notify di Home Assistant.",
"notify_service_label": "Servizio notify",
"notify_service_placeholder": "notify.mobile_app_mio_telefono",
"notify_service_hint": "Nome del servizio notify HA (es. notify.mobile_app_phone). Lascia vuoto per disabilitare.",
"sensor_title": "Sensori REST",
"sensor_hint": "Aggiungi a configuration.yaml per creare sensori EverShelf in Home Assistant.",
"sensor_copy_btn": "Copia YAML",
"sensor_copied": "YAML copiato negli appunti!",
"save_btn": "Salva impostazioni HA",
"ha_hint": "Se usi Home Assistant, usa il tab Home Assistant per configurare TTS, webhook e sensori."
}
},
"expiry": {
@@ -998,13 +1074,24 @@
"server_retry": "Riprova",
"unknown": "Errore sconosciuto",
"prefix": "Errore",
"no_inventory_entry": "Nessuna voce di inventario trovata"
"no_inventory_entry": "Nessuna voce di inventario trovata",
"offline_title": "Nessuna connessione",
"offline_subtitle": "L'app non riesce a raggiungere il server. Verifica la connessione Wi-Fi.",
"offline_checking": "Verifica connessione…",
"offline_restored": "Connessione ripristinata!",
"offline_continue": "Continua in modalità offline",
"offline_reading_cache": "Lettura dalla cache locale",
"offline_ops_pending": "{n} operazioni in attesa",
"offline_synced": "{n} operazioni sincronizzate",
"offline_ai_disabled": "Non disponibile offline",
"offline_cache_ready": "Offline — {n} prodotti in cache"
},
"confirm": {
"remove_item": "Vuoi davvero rimuovere questo prodotto dall'inventario?",
"kiosk_exit": "Uscire dalla modalità kiosk?",
"cancel": "Annulla",
"proceed": "Conferma"
"proceed": "Conferma",
"discard_one": "Butta 1 pezzo"
},
"location": {
"dispensa": "Dispensa",
@@ -1016,7 +1103,8 @@
"unknown_hint": "Inserisci il nome e le informazioni del prodotto",
"label_name": "🏷️ Nome prodotto",
"choose_location_title": "Quale modifica?",
"choose_location_hint": "Scegli la posizione da modificare:"
"choose_location_hint": "Scegli la posizione da modificare:",
"confirm_large_qty": "Stai impostando la quantità a {qty} {unit}. Questo sembra un valore insolitamente alto. Confermare?"
},
"screensaver": {
"recipe_btn": "Ricette",
@@ -1181,7 +1269,13 @@
"source": "Basato su {n} prodotti in dispensa · EverShelf",
"products_count": "prodotti",
"today_title": "🥗 La tua dispensa oggi",
"products_n": "{n} prodotti"
"products_n": "{n} prodotti",
"macros_title": "Macronutrienti stimati",
"macros_proteins": "Proteine",
"macros_carbs": "Carboidrati",
"macros_fat": "Grassi",
"macros_fiber": "Fibre",
"macros_source": "Stima basata su {n} prodotti in dispensa"
},
"facts": {
"greeting_morning": "Buongiorno",
@@ -1344,6 +1438,20 @@
"critical_error_intro": "L'app non può avviarsi a causa dei seguenti problemi:",
"error_network": "Impossibile contattare il server.",
"error_network_detail": "Il browser non riesce a raggiungere il server PHP.\n\nPossibili cause:\n• Il server Apache/PHP non è in esecuzione\n• Problema di rete o firewall\n• URL dell'app non corretta\n\nControlla che il server sia avviato e riprova.",
"retry": "Riprova"
"retry": "Riprova",
"syncing_local": "Sincronizzazione dati locali...",
"sync_done": "Dati locali aggiornati"
},
"stats_monthly": {
"title": "Statistiche Mensili",
"consumed": "prodotti usati",
"trend_up": "+{pct}% rispetto a {prev}",
"trend_down": "-{pct}% rispetto a {prev}",
"trend_same": "stesso ritmo del mese scorso",
"added": "aggiunti",
"wasted": "sprecati",
"top_used": "più usato",
"top_cats": "Categorie principali",
"source": "Storico transazioni · mese corrente"
}
}