Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d27433eb3 | |||
| eddb622c85 | |||
| 95c20adbbd | |||
| 6fa2e4d830 | |||
| 6ff1dfe0cc | |||
| 43e0ac9da3 | |||
| 1ce32cb5f0 | |||
| d75cde7eb6 | |||
| 43fe1c7bb5 | |||
| b2c87ae343 | |||
| fbdae35516 | |||
| d9ebc51e71 | |||
| 56ca58bc18 | |||
| b2e0f6d683 | |||
| ddb9bd9f75 | |||
| 965a672abe | |||
| 7249daa8eb | |||
| ec53f7529c | |||
| 1074dff87d | |||
| 3989d11094 | |||
| b010ced1a6 | |||
| cc0fa09219 | |||
| c0a076749e | |||
| 6a41b53174 | |||
| 1d04236bc0 | |||
| 561c6e9809 | |||
| 6857c20893 | |||
| 964de98203 | |||
| e28a6e4e39 | |||
| fd9e2471e0 | |||
| 3c8a9693b2 | |||
| b38bdc45f5 | |||
| 83a0df272a | |||
| 6320b575e0 | |||
| 8ccd218c5a | |||
| 5c1afaaaf5 | |||
| 6245b15420 | |||
| 02f673a164 | |||
| 61bb1b5552 | |||
| cbf4bd54da | |||
| 1cdbdb3b25 | |||
| 837d62c335 | |||
| fa36ba83bf | |||
| 1efeaf9236 | |||
| 573bcd1102 | |||
| d3eb82eee2 | |||
| 264b1f648e | |||
| 5e34bc90b3 | |||
| 2ecb3cbac6 | |||
| fba0947945 | |||
| 37fb522e8b | |||
| 47197d0d66 | |||
| b5a6daa557 | |||
| 9e80915a61 | |||
| 7019160704 | |||
| 34df755ba3 | |||
| ef15f3536c | |||
| 5ad24ed73b | |||
| dd0625b253 | |||
| a85414d790 | |||
| 8f6934485a | |||
| d8aff8ac04 | |||
| ff25307662 |
@@ -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
|
||||
|
||||
@@ -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 ───────────────────────────────────
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
[](https://www.sqlite.org/)
|
||||
[](Dockerfile)
|
||||
[](translations/)
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](https://github.com/dadaloop82/EverShelf/stargazers)
|
||||
[](https://github.com/dadaloop82/EverShelf/commits/main)
|
||||
[](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:00–07: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.
|
||||
|
||||
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=dadaloop82&repository=ha-evershelf&category=integration)
|
||||
|
||||
[](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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+983
-136
File diff suppressed because it is too large
Load Diff
+257
-5
@@ -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);
|
||||
}
|
||||
|
||||
+1071
-102
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
+1395
-1325
File diff suppressed because it is too large
Load Diff
+1395
-1325
File diff suppressed because it is too large
Load Diff
+1342
-1276
File diff suppressed because it is too large
Load Diff
+1342
-1276
File diff suppressed because it is too large
Load Diff
+1395
-1325
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user