Compare commits

...

63 Commits

Author SHA1 Message Date
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
17 changed files with 9745 additions and 6783 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
+32 -1
View File
@@ -102,7 +102,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 +113,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 +129,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 ───────────────────────────────────
+15
View File
@@ -11,6 +11,21 @@ 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.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)
 
[![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
+84
View File
@@ -133,3 +133,87 @@ 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.name, i.quantity, i.unit, i.expiry_date, i.location
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.name, i.quantity, i.unit, i.expiry_date, i.location
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);
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
}
}
+948 -101
View File
File diff suppressed because it is too large Load Diff
+257 -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;
@@ -2567,6 +2591,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 +3062,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 +3147,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 {
@@ -5583,6 +5693,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;
@@ -7510,6 +7640,14 @@ 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; }
@@ -7541,3 +7679,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);
}
+1070 -101
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>
+219
View File
@@ -0,0 +1,219 @@
# 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 (≤3 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` |
### 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
- 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"
```
Restart Home Assistant after editing `configuration.yaml`.
---
## 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": "3 products expiring within 3 days",
"items": [
{ "name": "Milk", "expiry_date": "2025-06-14", "quantity": 1, "unit": "l" }
]
}
}
```
### 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.summary }}
{% for item in trigger.json.data.items %}
— {{ item.name }} (expires {{ item.expiry_date }})
{% endfor %}
```
---
## 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
+133 -4
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>
@@ -331,8 +331,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 +672,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 +841,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>
@@ -1314,8 +1316,124 @@
<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.tts.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">
@@ -1757,6 +1875,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",
+75 -5
View File
@@ -213,7 +213,8 @@
"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:"
},
"action": {
"title": "Was möchtest du tun?",
@@ -227,7 +228,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",
@@ -365,6 +367,7 @@
"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",
@@ -473,7 +476,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",
@@ -864,6 +868,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,7 +1056,17 @@
"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": {
"remove_item": "Möchtest du dieses Produkt wirklich aus dem Bestand entfernen?",
@@ -1344,6 +1412,8 @@
"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"
}
}
+75 -5
View File
@@ -213,7 +213,8 @@
"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:"
},
"action": {
"title": "What do you want to do?",
@@ -227,7 +228,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",
@@ -365,6 +367,7 @@
"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",
@@ -473,7 +476,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",
@@ -864,6 +868,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,7 +1056,17 @@
"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": {
"remove_item": "Do you really want to remove this product from inventory?",
@@ -1344,6 +1412,8 @@
"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"
}
}
+68 -2
View File
@@ -821,6 +821,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,7 +1008,17 @@
"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": {
"remove_item": "¿Realmente quieres eliminar este producto del inventario?",
@@ -1292,6 +1356,8 @@
"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"
}
}
+68 -2
View File
@@ -821,6 +821,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,7 +1008,17 @@
"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": {
"remove_item": "Voulez-vous vraiment supprimer ce produit de l'inventaire ?",
@@ -1292,6 +1356,8 @@
"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"
}
}
+75 -5
View File
@@ -213,7 +213,8 @@
"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:"
},
"action": {
"title": "Cosa vuoi fare?",
@@ -227,7 +228,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",
@@ -365,6 +367,7 @@
"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",
@@ -473,7 +476,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",
@@ -864,6 +868,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,7 +1056,17 @@
"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?",
@@ -1344,6 +1412,8 @@
"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"
}
}