Compare commits
183 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bcecc2430d | |||
| 33d5c4c370 | |||
| ddef591108 | |||
| ca98acc1f2 | |||
| 616818998d | |||
| ef73630cad | |||
| a6369765b0 | |||
| 5a50403e52 | |||
| 865cb561be | |||
| 18169417d3 | |||
| 22dd77c879 | |||
| bcbdf669a1 | |||
| c513b0b4ef | |||
| 3bfa89e61d | |||
| 427bcd20a4 | |||
| 9b026408e2 | |||
| 5abf5f9adf | |||
| e58eb0501a | |||
| ecf7ec53fd | |||
| 78310f9fe9 | |||
| b73748346e | |||
| 833afb3cfd | |||
| 726d371d26 | |||
| a4267ee420 | |||
| fef70cb97c | |||
| ca285f7a9d | |||
| 8176237b93 | |||
| 5ce935de49 | |||
| 751b18ba3c | |||
| 55d562e0a3 | |||
| 7c3fb41b43 | |||
| a84824327d | |||
| 409ea5d2e5 | |||
| 0de44ae341 | |||
| 5a4e16c30d | |||
| cfcc3ce49f | |||
| 3965c6ef44 | |||
| 99b65900c4 | |||
| 1505550a16 | |||
| b23edc39b3 | |||
| 371dda46f0 | |||
| 3c68ce0dd1 | |||
| 94f5649183 | |||
| cff250055c | |||
| 477978aed9 | |||
| bf08556556 | |||
| 594c9fa115 | |||
| 8c471c1b7f | |||
| 9075740454 | |||
| 3f242c1836 | |||
| 9c97798a21 | |||
| 0fb887756f | |||
| 37bf403412 | |||
| 3676793194 | |||
| 3f410022a7 | |||
| 7631018929 | |||
| bb16e441f5 | |||
| bacb98b4eb | |||
| b739cb3a47 | |||
| 754daa9989 | |||
| 80a915ac35 | |||
| b28c6591a9 | |||
| 1e40da7235 | |||
| 046355d6b0 | |||
| 3a33dc7173 | |||
| c0c1a312c6 | |||
| 8ae455d82c | |||
| 0c8eee404e | |||
| e746c3d05b | |||
| 6f1966113d | |||
| f1e07f4151 | |||
| 7d4b881d7c | |||
| 63faf402c2 | |||
| 8543106fed | |||
| 4d1a4be0ea | |||
| 0065987050 | |||
| ad101cc58f | |||
| 3a08929353 | |||
| f935790ab2 | |||
| 45ea60e305 | |||
| d364588011 | |||
| 9fc1dd3614 | |||
| 68484b8323 | |||
| e534ea2e96 | |||
| 08bb293963 | |||
| ae3254c195 | |||
| 77b053c573 | |||
| ab79339e8c | |||
| 6cba7132d5 | |||
| 32da374e57 | |||
| 8fda93044d | |||
| a97907dbe1 | |||
| 94eeba280a | |||
| 978088ae23 | |||
| d4c5b5b97c | |||
| 0ec2620926 | |||
| f65f65d38f | |||
| f3c1fd2e7b | |||
| 69eb319aa1 | |||
| 02bfc82e2b | |||
| b8e43388b6 | |||
| 6e532ebdd1 | |||
| 56a10bd1c4 | |||
| 621fb4f96e | |||
| 57ec6af58c | |||
| 11886578ab | |||
| a02abce26e | |||
| 79d1ca06c7 | |||
| d1ba63d9a0 | |||
| 0ca4df0d27 | |||
| fc324d55f5 | |||
| 99c35e4c18 | |||
| b12cd76acc | |||
| 477139d47c | |||
| 8b44432244 | |||
| 4f1717c4b1 | |||
| c2002977cd | |||
| f7fb4e8f33 | |||
| ef9c26bed6 | |||
| aaf4de5e6b | |||
| 56bc6a709f | |||
| b60994d745 | |||
| 6e86c19262 | |||
| 619b7b4517 | |||
| ca2e39bc49 | |||
| 42fcbef95b | |||
| 0619d1487c | |||
| 79fff10b48 | |||
| a6f59cabfb | |||
| eaf9ebc52e | |||
| df8211c8ac | |||
| 7fae0e08bf | |||
| 450095376c | |||
| 257fa4797d | |||
| 9acf952c10 | |||
| e0deb3481b | |||
| d35f975ab9 | |||
| 0502a4d132 | |||
| 2ea69fd223 | |||
| 01f28a2b09 | |||
| 4bedfdd0f4 | |||
| 6c63bb17a3 | |||
| 665d3097e7 | |||
| 43047276a6 | |||
| 51ad25926a | |||
| 33e552373c | |||
| fc124c87bf | |||
| b166a305f2 | |||
| 6377c45eaa | |||
| 8d18362d83 | |||
| cabb8b28d4 | |||
| 27257caa3e | |||
| 3d744f256a | |||
| 8e504cad28 | |||
| e6b3328fe9 | |||
| db7fc0df18 | |||
| 3bb6dc7155 | |||
| d75a6e76c6 | |||
| b28aca5e55 | |||
| 2fb782f8e1 | |||
| c08798d462 | |||
| f6fe0a55bd | |||
| fecbbafd38 | |||
| c0ad69cf11 | |||
| a2a1a5ba77 | |||
| 98553c7600 | |||
| 7d89d61b95 | |||
| 6b1d5f4c45 | |||
| 288e0c05f6 | |||
| e52aa6d699 | |||
| 9512e3a8df | |||
| 4a729d2d10 | |||
| 6ecb881d9f | |||
| 662c27d7b4 | |||
| df9624ad75 | |||
| df0a47e336 | |||
| b97845553e | |||
| 495d7a22eb | |||
| a1511c608a | |||
| a3a3b54a85 | |||
| 4ee2e5638b | |||
| a9f0527769 | |||
| bf27f7f462 |
@@ -15,9 +15,9 @@ RewriteEngine On
|
||||
</FilesMatch>
|
||||
|
||||
# Force HTTPS (skip when terminated TLS is forwarded — Traefik, Caddy, NPM, …)
|
||||
RewriteCond %{HTTPS} !=on
|
||||
RewriteCond %{HTTP:X-Forwarded-Proto} !^https$ [NC]
|
||||
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
#RewriteCond %{HTTPS} !=on
|
||||
#RewriteCond %{HTTP:X-Forwarded-Proto} !^https$ [NC]
|
||||
#RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
|
||||
# API routing
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
|
||||
+4
-1
@@ -6,10 +6,12 @@ RUN apt-get update && apt-get upgrade -y && apt-get install -y \
|
||||
libcurl4-openssl-dev \
|
||||
libonig-dev \
|
||||
libgd-dev \
|
||||
libzip-dev \
|
||||
libicu-dev \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-ita \
|
||||
tesseract-ocr-eng \
|
||||
&& docker-php-ext-install pdo_sqlite curl mbstring gd \
|
||||
&& docker-php-ext-install pdo_sqlite curl mbstring gd zip intl \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Enable Apache mod_rewrite and mod_headers
|
||||
@@ -28,6 +30,7 @@ RUN mkdir -p /var/www/html/data/backups \
|
||||
|
||||
# Create .env from example if it doesn't exist (will be overridden by volume mount)
|
||||
RUN [ ! -f /var/www/html/.env ] && cp /var/www/html/.env.example /var/www/html/.env || true
|
||||
RUN chown www-data:www-data /var/www/html/.env && chmod 664 /var/www/html/.env
|
||||
|
||||
# Apache configuration: serve from app root
|
||||
RUN echo '<Directory /var/www/html>\n\
|
||||
|
||||
@@ -1,562 +1,223 @@
|
||||
# 🏠 EverShelf
|
||||
# 🏠 EverShelf for Ricardo
|
||||
|
||||
> **Self-hosted pantry management system** — Track your food inventory, scan barcodes, get AI-powered recipe suggestions, and reduce waste.
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
### 🚀 Try the live demo — no installation required!
|
||||
|
||||
**[▶ Open Live Demo](https://evershelfproject.dadaloop.it/demo)**
|
||||
·
|
||||
[🌐 Project Website](https://evershelfproject.dadaloop.it/)
|
||||
·
|
||||
[📖 Wiki](https://github.com/dadaloop82/EverShelf/wiki)
|
||||
|
||||
*The demo runs with mock pantry data. AI features are fully enabled. All write operations are safely sandboxed.*
|
||||
|
||||
</div>
|
||||
> Fork personnalisé d'EverShelf, adapté pour servir de backend stock/recettes à **Ricardo**, l'application bartender. Garde toutes les fonctionnalités d'EverShelf, avec des ajustements pour la gestion de bar (catégorie boissons, sous-catégories alcools, intégration directe avec Ricardo).
|
||||
|
||||
---
|
||||
|
||||
[](LICENSE)
|
||||
[](https://www.php.net/)
|
||||
[](https://www.sqlite.org/)
|
||||
[](Dockerfile)
|
||||
[](translations/)
|
||||
[](CHANGELOG.md)
|
||||
[](https://github.com/dadaloop82/EverShelf/stargazers)
|
||||
[](https://github.com/dadaloop82/EverShelf/commits/main)
|
||||
[](https://github.com/dadaloop82/EverShelf/graphs/contributors)
|
||||
[](https://github.com/dadaloop82/EverShelf/discussions)
|
||||
[](https://github.com/dadaloop82/EverShelf/actions/workflows/ci.yml)
|
||||
|
||||
[](https://ko-fi.com/J3J01ZNETZ)
|
||||
[](Dockerfile)
|
||||
|
||||
---
|
||||
|
||||
> **⚠️ 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.
|
||||
## ✨ Fonctionnalités principales
|
||||
|
||||
### 📦 Gestion des stocks
|
||||
|
||||
- Inventaire alimentaire complet
|
||||
- Gestion des emplacements :
|
||||
- 🏠 Placard
|
||||
- ❄️ Réfrigérateur
|
||||
- 🧊 Congélateur
|
||||
- 📍 Emplacements personnalisés, entièrement gérés depuis une page **🔧 Configuration** dédiée (ajout, modification, suppression sans toucher au code)
|
||||
- Sous-catégorie dédiée aux boissons (vin, bière, spiritueux, soda, jus, eau...) pour filtrer et trier l'inventaire plus précisément
|
||||
- Scan de codes-barres avec la caméra du téléphone
|
||||
- Ajout rapide de produits
|
||||
- Suivi des dates de péremption
|
||||
- Gestion des produits ouverts
|
||||
- Support des produits sous vide
|
||||
- Détection des incohérences de stock
|
||||
|
||||
---
|
||||
|
||||
### 🆕 Release 1.7.42 (2026-06-11)
|
||||
## 🤖 Intelligence artificielle (Google Gemini)
|
||||
|
||||
- **Stable shopping total** — Each list item is priced as one typical purchase (no more inflated totals from 14-day restock quantities or €/kg × piece count).
|
||||
- **Waste reason picker** — When discarding food, choose why (expired, wrong storage, bought too much, …); EverShelf learns and adjusts restock suggestions and storage hints.
|
||||
- **Fewer SQLite lock errors** — `inventory_use` and `shopping_add` retry on `SQLITE_BUSY`; smart shopping gets a longer PHP time limit on large pantries.
|
||||
- **Android kiosk** — Locale string escaping fix; setup wizard JSON save fix (CI build).
|
||||
EverShelf peut utiliser l'IA pour :
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md) for full details.
|
||||
- 📸 Identifier un produit depuis une photo
|
||||
- 📅 Lire automatiquement une date limite de consommation
|
||||
- 🧊 Proposer un stockage adapté
|
||||
- 🍳 Générer des recettes selon votre inventaire
|
||||
- 💬 Répondre aux questions sur vos produits
|
||||
- 🛒 Améliorer les suggestions de courses
|
||||
|
||||
> L'IA est optionnelle. EverShelf fonctionne sans clé Gemini.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
## 🛒 Liste de courses intelligente
|
||||
|
||||
### 🏠 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)
|
||||
- Création automatique depuis les ruptures de stock
|
||||
- Prévisions de besoins
|
||||
- Synchronisation avec Bring!
|
||||
- Nettoyage automatique des doublons
|
||||
- Suggestions d'achat personnalisées
|
||||
|
||||
---
|
||||
|
||||
### 📦 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
|
||||
- **Barcode scanning** — Scan products with your phone camera using QuaggaJS; last 20 scanned products saved as tappable chips so you can re-select them without rescanning
|
||||
- **AI identification** — Take a photo and let Google Gemini identify the product, with suggestions from your existing inventory; gracefully shows a friendly message when AI quota is exhausted instead of a raw API error
|
||||
- **Smart locations** — Track items across Pantry, Fridge, Freezer, and custom locations
|
||||
- **Expiry tracking** — Automatic shelf-life estimation based on product type and storage
|
||||
- **Opened product tracking** — Reduced shelf-life calculation when packages are opened; opened-product expiry is now also checked when building banner alerts (not just the dashboard section)
|
||||
- **Vacuum-sealed support** — Extended expiry dates for vacuum-sealed items; products sealed under vacuum are only flagged as expired after a configurable grace period past the printed date (`VACUUM_EXPIRY_EXTENSION_DAYS`, default 30 days, configurable in `.env`)
|
||||
- **Anomaly detection** — Banner alerts for suspicious quantities and consumption predictions with inline correction; dismiss button now shows the current inventory quantity so the action is unambiguous ("Quantity is correct (2 pcs)")
|
||||
## 🍳 Mode cuisine
|
||||
|
||||
### 🤖 AI-Powered (Google Gemini)
|
||||
- **Expiry date reading** — Photograph a label and extract the expiry date automatically
|
||||
- **Product identification** — Point your camera at any product for instant recognition
|
||||
- **Existing product matching** — AI scan shows matching products already in your pantry before suggesting new ones
|
||||
- **Storage & shelf-life hint** — When adding a new product, Gemini suggests the optimal storage location and shelf-life in the background; shown as an inline AI badge next to the expiry estimate
|
||||
- **Recipe generation** — Get personalized recipes based on what's in your pantry; streams live via Server-Sent Events so results appear as they are generated
|
||||
- **Recipe stock hints** — Each pantry ingredient shows how much you have and what remains after use; when the leftover would be less than 5% of the full sealed package (10% for an already-opened partial pack), the recipe automatically uses everything on hand to avoid waste
|
||||
- **Smart chat assistant** — Ask questions about your inventory, get cooking tips
|
||||
- **Shopping suggestions with tips** — AI-powered purchase recommendations, each enriched with a short practical buying/storing tip
|
||||
- **Anomaly explanation** — "Explain" button on anomaly banners explains in plain language why a discrepancy likely occurred and what to do
|
||||
- **Model fallback** — All AI endpoints try `gemini-2.5-flash` first and fall back to `gemini-2.0-flash` automatically
|
||||
- **Graceful no-key state** — When no Gemini key is configured, AI entry points show a friendly message; the header button is visually greyed with an amber dot
|
||||
|
||||
### 🛒 Shopping List
|
||||
- **Bring! integration** — Sync with the [Bring!](https://www.getbring.com/) shopping list app
|
||||
- **Generic shopping names** — Products are grouped by type (e.g. "Milk", "Cold cuts", "Cooking cream") rather than brand, keeping the Bring! list clean and consolidated
|
||||
- **Smart predictions** — Know what you'll need before you run out
|
||||
- **Auto-add on depletion** — When a product reaches zero the app adds it to Bring! automatically, no confirmation needed
|
||||
- **Auto-remove on scan** — Products are removed from the shopping list when scanned in - **Auto-migration** — Items already on the Bring! list are silently renamed to their generic name in the background (throttled, runs on list load)
|
||||
- **Catalog coverage** — All product types resolve to a German Bring! catalog key for icon and category display in the Bring! app
|
||||
|
||||
### 🍳 Cooking Mode
|
||||
- **♻️ Zero-waste tips** — For each cooking step that generates reusable scraps (peels, cooking water, egg whites, cheese rinds, bread crusts, vegetable tops, etc.), a dismissible ♻️ tip card appears with a practical reuse idea; tips are generated by Gemini as part of the recipe at no extra API cost; opt-in toggle in Settings (default OFF)
|
||||
- **Step-by-step guidance** — Follow recipes with a hands-free cooking interface
|
||||
- **Text-to-Speech** — Voice readout of recipe steps; supports browser Web Speech API, native Android TTS (kiosk), or a custom REST endpoint (Home Assistant, etc.); retries voice loading for up to 10 seconds with a fallback refresh button; TTS activates automatically without requiring the global TTS setting to be enabled
|
||||
- **Auto-read on navigate** — Each step is read aloud automatically when you tap Next or Previous; the first step is read when entering cooking mode
|
||||
- **Timer voice alerts** — 10-second countdown warning spoken aloud before each timer expires; expiry announced vocally when time is up
|
||||
- **Recipe completion** — "Bon appétit!" announced via TTS when the last step is confirmed
|
||||
- **Built-in timer** — Automatic timer suggestions based on recipe instructions
|
||||
- **Ingredient tracking** — Mark ingredients as used during cooking; leftover quantities prompt a "move to another location" flow
|
||||
|
||||
### 📊 Dashboard
|
||||
- **Waste tracking** — Monitor consumed vs. wasted products over 30 days
|
||||
- **Anti-waste report** — Personalised waste rate vs. national average with annual kg estimate; shown above the expiring-items list
|
||||
- **Expiry alerts** — Visual warnings for expired and soon-to-expire items
|
||||
- **Opened products panel** — Tracks partially-used items; expiry is recalculated from the opening date using AI (Gemini) + per-category rule fallback; whole sealed packages always keep their original manufacturer expiry; conf items with mixed whole + fractional units are shown as two separate entries
|
||||
- **Freezer shelf-life** — Granular per-product estimates (USDA/EFSA): fish 120 d, poultry 270 d, whole red-meat cuts 365 d, mince 120 d, vegetables/fruit 270 d, generic 180 d; AI + cache still take priority over rules
|
||||
- **Safety ratings** — Smart assessment of expired product safety (by category and location); expired unsafe items shown with a red danger banner and a discard action as the primary action
|
||||
- **Expired product banner** — Products that have passed their effective shelf-life (including opened-product reduced expiry) appear in the top notification banner; icon, colour and title adapt to the actual safety level (✅ green for safe, 👀 amber to check, 🚫 red for danger); high-risk items get a prominent discard action
|
||||
- **Quick recipe bar** — One-tap recipe suggestion using expiring products
|
||||
- **Anomaly banner** — Scrollable banner with suspicious quantities and consumption prediction mismatches, with one-tap correction or inline edit
|
||||
- **Expired/expiring alerts** — Priority-sorted banner notifications for expired and soon-to-expire products with use, throw, edit, and dismiss actions
|
||||
- **Swipe navigation** — Touch swipe or tap arrows/dots to browse banner notifications
|
||||
- **Quick-access buttons** — Recently used and most popular products shown on the inventory page for fast access
|
||||
|
||||
### 🌙 Appearance
|
||||
- **Dark mode** — Three modes: Light, Dark, and Auto (time-based: dark from 20:00 to 07:00, light otherwise); applies immediately without page reload; auto mode re-evaluates every 5 minutes, so night/day transitions happen automatically even on always-on kiosk displays; theme is applied before the first render to prevent a white flash
|
||||
- **Global settings tab** — A dedicated **⚙️ General** tab groups all system-wide settings (language, currency, theme, screensaver, zero-waste tips, export) at the top of the Settings panel
|
||||
|
||||
### �️ Database Maintenance
|
||||
- **Automatic cleanup** — Recipes older than `RECIPE_RETENTION_DAYS` (default 7) and transactions older than `TRANSACTION_RETENTION_DAYS` (default 7) are deleted automatically on every cron cycle; SQLite `VACUUM` runs after each cleanup to keep the file compact
|
||||
- **Manual cleanup** — Trigger immediately via `GET /api/?action=db_cleanup`
|
||||
- **Compact by default** — Fresh installs stay small; large accumulated databases shrink back to a few hundred KB within one cron cycle
|
||||
|
||||
### �📱 Progressive Web App
|
||||
- **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
|
||||
- **Auto-discovery** — Server scans LAN to find the gateway automatically
|
||||
- **Auto weight reading** — When adding/using a product with unit g/ml, weight fills automatically
|
||||
- **10g threshold** — Ignores readings that haven't changed enough between products - **Duplicate-reading prevention** — Server-side 12-second dedup window rejects a second scale-triggered deduction of the same product, guarding against BLE multi-fire- **ml conversion hint** — Shows "weight in grams → will be converted to ml" when product unit is ml
|
||||
- **Stability + auto-confirm** — 10s stable wait + 5s countdown before confirming
|
||||
- **Real-time status** — Scale connection indicator always visible in the header
|
||||
- **Multi-protocol** — Supports Bluetooth SIG Weight Scale, Body Composition, Xiaomi Mi Scale 2 and 100+ models
|
||||
- **Built into kiosk (v1.6.0+)** — BLE gateway runs as an integrated foreground service inside the [EverShelf Kiosk](evershelf-kiosk/) app; no separate APK needed.
|
||||
|
||||
### 📺 Android Kiosk Mode (Add-on)
|
||||
- **Dedicated tablet app** — Full-screen WebView wrapper for wall-mounted kitchen tablets
|
||||
- **True kiosk lock** — Screen pinning blocks home/recent buttons
|
||||
- **Setup wizard** — 6-step guided configuration (language, welcome, permissions, server URL, BLE scale scan, screensaver, summary)
|
||||
- **Smart auto-discovery** — Scans the LAN in parallel (60 threads, TCP pre-check, ports 80/443/8080/8443) with real-time UI feedback; correctly identifies the device's Wi-Fi/Ethernet subnet (VPN and cellular interfaces are filtered out)
|
||||
- **Built-in BLE scale gateway** — `GatewayService` foreground service; BLE scanning + WebSocket server `:8765` run directly inside the kiosk app. Select your scale in step 5 of the wizard — no external app required
|
||||
- **Scale auto-configuration** — After selecting the BLE device, the wizard writes `scale_enabled` and `scale_gateway_url=ws://127.0.0.1:8765` to the server automatically
|
||||
- **Camera & mic permissions** — Full hardware access for barcode scanning and voice; grant button transforms to a green confirmation after granting
|
||||
- **Native TTS bridge** — Cooking mode voice readout uses the Android TextToSpeech engine directly, bypassing Web Speech API voice limitations; no offline voice packs required
|
||||
- **Hard refresh** — ↻ button clears WebView cache to pick up web app updates
|
||||
- **Update notifications** — Checks GitHub releases every 6h, shows banner when updates available
|
||||
- **SSL support** — Accepts self-signed certificates
|
||||
- **Android kiosk app** — [`evershelf-kiosk/`](evershelf-kiosk/) — downloadable APK
|
||||
- Recettes étape par étape
|
||||
- Mode mains libres
|
||||
- Synthèse vocale (TTS)
|
||||
- Minuteurs automatiques
|
||||
- Suivi des ingrédients utilisés
|
||||
- Conseils anti-gaspillage
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
## ♻️ Réduction du gaspillage
|
||||
|
||||
### Prerequisites
|
||||
- **Web server** with PHP 8.0+ (Apache or Nginx)
|
||||
- **PHP extensions**: `pdo_sqlite`, `curl`, `mbstring`, `json`
|
||||
- **HTTPS** recommended (required for camera access on mobile)
|
||||
- Suivi des aliments consommés ou jetés
|
||||
- Analyse des pertes
|
||||
- Alertes de péremption
|
||||
- Suggestions pour utiliser les produits bientôt périmés
|
||||
|
||||
### Installation
|
||||
---
|
||||
|
||||
#### Option A: Docker (recommended)
|
||||
## 🏡 Intégrations
|
||||
|
||||
### Home Assistant
|
||||
|
||||
Intégration native disponible :
|
||||
|
||||
- Capteurs de stock
|
||||
- Dates de péremption
|
||||
- Liste de courses
|
||||
- Calendrier des produits
|
||||
- Actions personnalisées
|
||||
- Suggestions de recettes IA
|
||||
|
||||
Compatible avec une installation 100% locale.
|
||||
|
||||
---
|
||||
|
||||
## 📱 Application mobile / PWA
|
||||
|
||||
- Interface adaptée smartphone
|
||||
- Installation comme une application
|
||||
- Synchronisation multi-appareils
|
||||
- Mode hors-ligne :
|
||||
- consultation du stock
|
||||
- actions mises en attente
|
||||
- synchronisation automatique au retour réseau
|
||||
|
||||
---
|
||||
|
||||
## 📺 Mode tablette (Kiosque Android)
|
||||
|
||||
- Affichage plein écran
|
||||
- Verrouillage kiosque
|
||||
- Scan caméra
|
||||
- Support TTS natif Android
|
||||
- Découverte automatique du serveur
|
||||
- Support des balances Bluetooth
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Installation rapide
|
||||
|
||||
### Prérequis
|
||||
|
||||
- PHP 8.0+
|
||||
- SQLite 3
|
||||
- Extensions PHP :
|
||||
- `pdo_sqlite`
|
||||
- `curl`
|
||||
- `mbstring`
|
||||
- `json`
|
||||
|
||||
Docker est recommandé.
|
||||
|
||||
---
|
||||
|
||||
### 🐳 Installation Docker (CLI)
|
||||
|
||||
```bash
|
||||
# 1. Clone the repository
|
||||
git clone https://github.com/dadaloop82/EverShelf.git
|
||||
git clone https://git.mashome.fr/morgane/EverShelf.git
|
||||
cd EverShelf
|
||||
|
||||
# 2. Create configuration file
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
|
||||
# 3. Start with Docker Compose
|
||||
docker compose up -d
|
||||
|
||||
# → Open http://localhost:8080
|
||||
```
|
||||
|
||||
#### Option B: Manual
|
||||
Puis ouvrez :
|
||||
http://localhost:8080
|
||||
|
||||
```bash
|
||||
# 1. Clone the repository
|
||||
git clone https://github.com/dadaloop82/EverShelf.git
|
||||
cd EverShelf
|
||||
---
|
||||
### 🐳 Déploiement via Portainer
|
||||
|
||||
# 2. Create configuration file
|
||||
cp .env.example .env
|
||||
1. Dans Portainer, va dans **Stacks** → **Add stack**
|
||||
2. Donne un nom à la stack (ex : `evershelf`)
|
||||
3. Colle le contenu de ton `docker-compose.yml` dans l'éditeur web (ou utilise l'option **Repository** en pointant vers `https://git.mashome.fr/morgane/EverShelf.git` et le chemin du fichier compose)
|
||||
4. Renseigne tes variables d'environnement dans la section **Environment variables** (ou via un fichier `.env` à la racine du repo)
|
||||
5. Clique sur **Deploy the stack**
|
||||
|
||||
# 3. Set permissions
|
||||
chmod 755 data/
|
||||
chmod 664 data/.gitkeep
|
||||
chown -R www-data:www-data data/
|
||||
Pour mettre à jour après une modification de code :
|
||||
- Va dans **Stacks** → ta stack → **Update the stack**
|
||||
- Coche bien **« Re-pull image »** / **« Re-build image »** avant de valider — sinon Portainer redémarre le conteneur avec l'image déjà construite en cache, sans prendre en compte tes changements.
|
||||
|
||||
# 4. Edit your configuration
|
||||
nano .env
|
||||
```
|
||||
---
|
||||
|
||||
### Configuration (.env)
|
||||
## ⚙️ Configuration
|
||||
|
||||
```ini
|
||||
# Required for AI features (get a key at https://aistudio.google.com/app/apikey)
|
||||
GEMINI_API_KEY=your_api_key_here
|
||||
Exemple de fichier `.env` :
|
||||
|
||||
# Optional: Bring! shopping list integration
|
||||
BRING_EMAIL=your_email@example.com
|
||||
BRING_PASSWORD=your_password
|
||||
```env
|
||||
# IA Google Gemini (optionnel)
|
||||
GEMINI_API_KEY=votre_cle
|
||||
|
||||
# Optional: Text-to-Speech for cooking mode
|
||||
TTS_URL=http://your-home-assistant:8123/api/events/tts_speak
|
||||
TTS_TOKEN=your_long_lived_token
|
||||
TTS_ENABLED=true
|
||||
# Bring! (optionnel)
|
||||
BRING_EMAIL=email@example.com
|
||||
BRING_PASSWORD=motdepasse
|
||||
|
||||
# Optional: DB retention and cleanup (applied automatically each cron cycle)
|
||||
RECIPE_RETENTION_DAYS=7 # delete recipe plans older than N days
|
||||
TRANSACTION_RETENTION_DAYS=90 # delete stock transactions older than N days (min 30 enforced)
|
||||
|
||||
# Optional: Vacuum-sealed expiry grace period
|
||||
VACUUM_EXPIRY_EXTENSION_DAYS=30 # extra days before vacuum-sealed items are flagged expired
|
||||
|
||||
# Optional: Gemini cost rates (USD per million tokens, for the Info tab cost estimate)
|
||||
GEMINI_COST_25F_IN=0.15
|
||||
GEMINI_COST_25F_OUT=0.60
|
||||
GEMINI_COST_20F_IN=0.10
|
||||
GEMINI_COST_20F_OUT=0.40
|
||||
|
||||
# Optional: Security — protect all API endpoints
|
||||
# Set a strong random string; clients send it as X-API-Token header (or ?api_token= for HA)
|
||||
# Sécurité API
|
||||
API_TOKEN=
|
||||
|
||||
# Optional: Legacy alias for API_TOKEN (settings save only)
|
||||
SETTINGS_TOKEN=
|
||||
|
||||
# Optional: Demo mode — block all write operations at the router level
|
||||
DEMO_MODE=false
|
||||
|
||||
# Optional: Logging
|
||||
# LOG_LEVEL sets the minimum severity written to disk (DEBUG / INFO / WARN / ERROR)
|
||||
# DEBUG also logs every SQL query executed against the database
|
||||
LOG_LEVEL=INFO
|
||||
LOG_ROTATE_HOURS=24 # hours before opening a new log file (default: 24)
|
||||
LOG_MAX_FILES=14 # maximum number of rotated files to keep (default: 14)
|
||||
# Nettoyage automatique
|
||||
RECIPE_RETENTION_DAYS=7
|
||||
TRANSACTION_RETENTION_DAYS=90
|
||||
```
|
||||
|
||||
### Web Server Configuration
|
||||
|
||||
<details>
|
||||
<summary><strong>Apache (.htaccess)</strong></summary>
|
||||
|
||||
The app works out of the box with Apache if placed in the web root or a subdirectory. Make sure `mod_rewrite` is enabled and `AllowOverride All` is set.
|
||||
|
||||
```apache
|
||||
<Directory /var/www/html/evershelf>
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Nginx</strong></summary>
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-server.local;
|
||||
root /var/www/html/evershelf;
|
||||
index index.html;
|
||||
|
||||
location /api/ {
|
||||
try_files $uri $uri/ =404;
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
||||
|
||||
# Deny access to sensitive files
|
||||
location ~ /\.env { deny all; }
|
||||
location ~ /data/ { deny all; }
|
||||
location ~ /backup\.sh { deny all; }
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### HTTPS Setup (Recommended)
|
||||
|
||||
Camera access requires HTTPS on most mobile browsers. Options:
|
||||
- **Let's Encrypt** with Certbot (for public-facing servers)
|
||||
- **Self-signed certificate** (for local network only)
|
||||
- **Reverse proxy** (e.g., Caddy, Traefik) with automatic TLS
|
||||
|
||||
### Cron Job (Optional)
|
||||
|
||||
Set up a cron job for smart shopping predictions:
|
||||
|
||||
```bash
|
||||
# Run every 5 minutes
|
||||
*/5 * * * * php /path/to/evershelf/api/cron_smart_shopping.php >> /path/to/evershelf/data/cron.log 2>&1
|
||||
```
|
||||
|
||||
### Backup (Optional)
|
||||
|
||||
The included `backup.sh` creates local daily backups of your database:
|
||||
|
||||
```bash
|
||||
# Run daily at 3 AM
|
||||
0 3 * * * /path/to/evershelf/backup.sh
|
||||
```
|
||||
|
||||
### Google Drive Backup (Optional)
|
||||
|
||||
EverShelf supports automatic daily backups to Google Drive via OAuth 2.0. This works on any server, including private IP / local network setups (no public domain required).
|
||||
|
||||
**Setup:**
|
||||
|
||||
1. Go to [console.cloud.google.com](https://console.cloud.google.com) and select or create a project.
|
||||
2. Enable the **Google Drive API** (`APIs & Services → Enable APIs → Google Drive API`).
|
||||
3. Go to `APIs & Services → Credentials → Create Credentials → OAuth client ID`.
|
||||
4. Application type: **Web application**.
|
||||
5. Add **`http://localhost`** as an Authorized Redirect URI (this is the key — it works even without a real domain).
|
||||
6. Copy **Client ID** and **Client Secret** into EverShelf Settings → Backup.
|
||||
7. Enter your **Google Drive Folder ID** (the last part of the folder URL).
|
||||
8. Click **Authorize with Google** and sign in.
|
||||
9. The browser will redirect to `http://localhost` and may show a connection error — **this is expected**. Copy the full URL from the address bar (e.g. `http://localhost/?code=4%2F0A...`) and paste it into the field that appears in EverShelf, then click **Submit**.
|
||||
|
||||
> **Note:** While the OAuth app is in *Testing* status in Google Cloud Console, you must add your Google account as a test user under `APIs & Services → OAuth consent screen → Test users`.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture
|
||||
## 🔒 Vie privée
|
||||
|
||||
```
|
||||
evershelf/
|
||||
├── index.html # Single-page application (SPA)
|
||||
├── manifest.json # PWA manifest
|
||||
├── .env.example # Configuration template
|
||||
├── backup.sh # Local database backup script
|
||||
├── LICENSE # MIT License
|
||||
│
|
||||
├── api/
|
||||
│ ├── index.php # Main API router (all endpoints)
|
||||
│ ├── database.php # SQLite schema, migrations, helpers
|
||||
│ └── cron_smart_shopping.php # Background job for predictions
|
||||
│
|
||||
├── assets/
|
||||
│ ├── css/style.css # All application styles
|
||||
│ ├── js/app.js # All application logic
|
||||
│ └── img/ # Static images
|
||||
│
|
||||
└── data/ # Runtime data (gitignored)
|
||||
├── evershelf.db # SQLite database (auto-created)
|
||||
├── backups/ # Local DB backups
|
||||
└── *.json # Token/cache files
|
||||
EverShelf est conçu pour fonctionner en auto-hébergement :
|
||||
|
||||
evershelf-scale-gateway/ # ⚖️ Android BLE gateway [DEPRECATED — integrated into kiosk v1.6.0+]
|
||||
├── README.md # Deprecation notice + legacy docs
|
||||
└── app/src/ # Kotlin Android source (WebSocket + BLE)
|
||||
|
||||
evershelf-kiosk/ # 📺 Android kiosk app (add-on)
|
||||
├── README.md # Setup & feature docs
|
||||
└── app/src/ # Kotlin Android source (WebView wrapper)
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
|
||||
| Category | Action | Method | Description |
|
||||
|----------|--------|--------|-------------|
|
||||
| **Products** | `search_barcode` | GET | Find product by barcode |
|
||||
| | `lookup_barcode` | GET | Look up barcode on Open Food Facts |
|
||||
| | `product_save` | POST | Create or update a product |
|
||||
| | `products_list` | GET | List all products |
|
||||
| **Inventory** | `inventory_list` | GET | List inventory items |
|
||||
| | `inventory_add` | POST | Add product to inventory |
|
||||
| | `inventory_use` | POST | Use/consume from inventory |
|
||||
| | `inventory_summary` | GET | Count by location |
|
||||
| **AI** | `gemini_identify` | POST | Identify product from photo |
|
||||
| | `gemini_expiry` | POST | Read expiry date from photo |
|
||||
| | `gemini_chat` | POST | Chat with AI assistant |
|
||||
| | `generate_recipe` | POST | Generate recipe from inventory |
|
||||
| | `gemini_product_hint` | POST | Storage location + shelf-life hint |
|
||||
| | `gemini_shopping_enrich` | POST | Enrich shopping suggestions with tips |
|
||||
| | `gemini_anomaly_explain` | POST | Plain-language anomaly explanation |
|
||||
| **Shopping** | `bring_list` | GET | Get Bring! shopping list |
|
||||
| | `bring_add` | POST | Add items to Bring! |
|
||||
| | `smart_shopping` | GET | Smart shopping predictions |
|
||||
| **Settings** | `get_settings` | GET | Get server configuration |
|
||||
| | `save_settings` | POST | Update server configuration |
|
||||
- Pas de compte obligatoire
|
||||
- Pas de cloud imposé
|
||||
- Données stockées localement
|
||||
- SQLite comme base de données
|
||||
- Les fonctions IA utilisent uniquement les services configurés par l'utilisateur
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Notes
|
||||
## 🛠️ Développement
|
||||
|
||||
- **Credentials** are stored in `.env` (server-side, never committed to Git)
|
||||
- **Database** stays local — never pushed to remote repositories
|
||||
- **Apache/Nginx hardening** — `.env`, `data/`, and `logs/` are blocked from direct HTTP access
|
||||
- **API token** — set `API_TOKEN` in `.env` to require `X-API-Token` on all API calls (Home Assistant: `?api_token=`)
|
||||
- **API keys are never exposed to the browser** — `get_settings` returns only boolean flags (`gemini_key_set`, `ha_token_set`, …)
|
||||
- **GitHub Issues token** — stored encrypted as `GH_ISSUE_TOKEN_ENC` + `GH_ISSUE_TOKEN_KEY` (see `scripts/encrypt-gh-token.php`)
|
||||
- **Settings write protection** — `save_settings` requires the same API token when configured; validated with `hash_equals`
|
||||
- **Demo / public mode** — set `DEMO_MODE=true` to block all write operations at the PHP router level before any business logic runs
|
||||
- The API uses **parameterized SQL queries** (PDO prepared statements) against injection
|
||||
- **Input validation** on all inventory operations (quantity bounds, location whitelist)
|
||||
- Consider adding **reverse-proxy authentication** (e.g. Authelia, Nginx `auth_basic`) if the server is accessible from the internet
|
||||
Technologies principales :
|
||||
|
||||
- PHP
|
||||
- SQLite
|
||||
- JavaScript
|
||||
- HTML/CSS
|
||||
- Docker
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Development
|
||||
## 📜 Licence
|
||||
|
||||
```bash
|
||||
# Run PHP's built-in server for local development
|
||||
php -S localhost:8080 -t /path/to/evershelf
|
||||
|
||||
# Check PHP syntax
|
||||
php -l api/index.php
|
||||
php -l api/database.php
|
||||
```
|
||||
|
||||
The application uses no build tools — edit files directly and refresh.
|
||||
Projet sous licence MIT.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Roadmap
|
||||
## 🙏 Crédits
|
||||
|
||||
Feature requests, bug reports and planned work are tracked in the [**EverShelf Roadmap**](https://github.com/users/dadaloop82/projects/2) GitHub Project.
|
||||
Ce fork, **EverShelf for Ricardo**, est maintenu par Morgane pour servir de système de gestion de stock/recettes à l'application **Ricardo**.
|
||||
|
||||
---
|
||||
Projet original :
|
||||
|
||||
## 🌐 Translations
|
||||
https://github.com/dadaloop82/EverShelf
|
||||
|
||||
The app supports multiple languages via JSON translation files in the `translations/` folder.
|
||||
|
||||
| Language | Status |
|
||||
|----------|--------|
|
||||
| 🇮🇹 Italian (it) | ✅ Complete (base) |
|
||||
| 🇬🇧 English (en) | ✅ Complete |
|
||||
| 🇩🇪 German (de) | ✅ Complete |
|
||||
| 🇫🇷 French (fr) | ✅ Complete |
|
||||
| 🇪🇸 Spanish (es) | ✅ Complete |
|
||||
|
||||
**Want to add your language?** See the [Translation Guide](CONTRIBUTING.md#-adding-translations) — just copy `translations/it.json`, translate the values, and submit a PR!
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/my-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add my feature'`)
|
||||
4. Push to the branch (`git push origin feature/my-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
### Easiest way to start — translate EverShelf into your language
|
||||
|
||||
Translations are just JSON files. No coding, no setup — fork → edit → PR.
|
||||
|
||||
```
|
||||
translations/
|
||||
├── it.json ✅ Italian (base)
|
||||
├── en.json ✅ English
|
||||
├── de.json ✅ German
|
||||
├── fr.json ✅ French
|
||||
├── es.json ✅ Spanish
|
||||
├── pt.json ❌ Portuguese — wanted!
|
||||
├── nl.json ❌ Dutch — wanted!
|
||||
└── ... ❌ Your language here!
|
||||
```
|
||||
|
||||
👉 See [issue #93](https://github.com/dadaloop82/EverShelf/issues/93) to claim a language.
|
||||
|
||||
### Other ways to contribute
|
||||
|
||||
| What | Skill needed |
|
||||
|---|---|
|
||||
| 🐛 Report a bug | None |
|
||||
| 📖 Improve the wiki | Markdown |
|
||||
| 🌍 Add a translation | JSON editing |
|
||||
| 🎨 Fix a CSS/UI issue | CSS / HTML |
|
||||
| ⚙️ Implement a feature | PHP / JS |
|
||||
| ⭐ Star the repo | Clicking |
|
||||
|
||||
👉 Browse [`help wanted`](https://github.com/dadaloop82/EverShelf/labels/help%20wanted) issues for good starting points.
|
||||
|
||||
Read [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide (branch naming, code style, how to run locally).
|
||||
|
||||
---
|
||||
|
||||
## 💬 Community
|
||||
|
||||
Join the conversation in [GitHub Discussions](https://github.com/dadaloop82/EverShelf/discussions):
|
||||
- **Vote on upcoming features** — tell us what to build next
|
||||
- **Show your setup** — share your kitchen kiosk
|
||||
- **Ask questions** — get help from the community
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the **MIT License** — see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
---
|
||||
|
||||
## 👨💻 Author
|
||||
|
||||
**Stimpfl Daniel** — [evershelfproject@gmail.com](mailto:evershelfproject@gmail.com)
|
||||
|
||||
- Website: [evershelfproject.dadaloop.it](https://evershelfproject.dadaloop.it/)
|
||||
- GitHub: [@dadaloop82](https://github.com/dadaloop82)
|
||||
|
||||
---
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
For a live walkthrough with real data and full AI enabled, visit the **[live demo](https://evershelfproject.dadaloop.it/demo)** — no installation required.
|
||||
|
||||
> Want to contribute additional screenshots? See [CONTRIBUTING.md](CONTRIBUTING.md) — PRs welcome!
|
||||
Ce dépôt contient des améliorations et adaptations personnelles, incluant un système d'export/import avec fusion intelligente des données.
|
||||
@@ -186,6 +186,10 @@ function migrateDB(PDO $db): void {
|
||||
try { $db->exec("ALTER TABLE products ADD COLUMN shopping_name TEXT DEFAULT ''"); }
|
||||
catch (PDOException $e) { if (strpos($e->getMessage(), 'duplicate column') === false) throw $e; }
|
||||
}
|
||||
if (!in_array('subcategory', $colNames)) {
|
||||
try { $db->exec("ALTER TABLE products ADD COLUMN subcategory TEXT DEFAULT NULL"); }
|
||||
catch (PDOException $e) { if (strpos($e->getMessage(), 'duplicate column') === false) throw $e; }
|
||||
}
|
||||
|
||||
// Empty barcode strings break UNIQUE (only one '' allowed); normalize to NULL.
|
||||
$db->exec("UPDATE products SET barcode = NULL WHERE barcode IS NOT NULL AND TRIM(barcode) = ''");
|
||||
@@ -319,6 +323,227 @@ function migrateDB(PDO $db): void {
|
||||
$db->exec("CREATE INDEX IF NOT EXISTS idx_transactions_type_date ON transactions(type, created_at)");
|
||||
$db->exec("CREATE INDEX IF NOT EXISTS idx_transactions_pid_type_undone ON transactions(product_id, type, undone)");
|
||||
|
||||
// Custom locations table (v1.9.0) — dynamic inventory locations managed from Settings
|
||||
$locTables = $db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='locations'")->fetchAll();
|
||||
if (empty($locTables)) {
|
||||
$db->exec("
|
||||
CREATE TABLE locations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key TEXT UNIQUE NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
icon TEXT DEFAULT '📦',
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
is_builtin INTEGER DEFAULT 0
|
||||
);
|
||||
");
|
||||
|
||||
$db->exec("INSERT INTO locations (key, label, icon, sort_order, is_builtin) VALUES
|
||||
('dispensa', 'Dispensa', '🗄️', 1, 1),
|
||||
('frigo', 'Frigo', '🧊', 2, 1),
|
||||
('freezer', 'Freezer', '❄️', 3, 1),
|
||||
('altro', 'Altro', '📦', 4, 1)
|
||||
");
|
||||
}
|
||||
|
||||
// Custom subcategories table (v2.0) — sous-catégories par catégorie, gérables depuis Config
|
||||
$subcatTables = $db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='subcategories'")->fetchAll();
|
||||
if (empty($subcatTables)) {
|
||||
$db->exec("
|
||||
CREATE TABLE subcategories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
category TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
UNIQUE(category, key)
|
||||
);
|
||||
");
|
||||
$db->exec("INSERT INTO subcategories (category, key, label, sort_order) VALUES
|
||||
('latticini', 'lait', '🥛 Lait', 1),
|
||||
('latticini', 'yaourt', '🥣 Yaourt', 2),
|
||||
('latticini', 'fromage', '🧀 Fromage', 3),
|
||||
('latticini', 'beurre', '🧈 Beurre', 4),
|
||||
('latticini', 'creme', '🍦 Crème', 5),
|
||||
('latticini', 'oeufs', '🥚 Œufs', 6),
|
||||
('latticini', 'autre', '📦 Autre', 7),
|
||||
('carne', 'poulet', '🍗 Poulet', 1),
|
||||
('carne', 'boeuf', '🐄 Bœuf', 2),
|
||||
('carne', 'porc', '🐖 Porc', 3),
|
||||
('carne', 'agneau', '🐑 Agneau', 4),
|
||||
('carne', 'charcuterie', '🥓 Charcuterie', 5),
|
||||
('carne', 'autre', '📦 Autre', 6),
|
||||
('pesce', 'poisson_frais', '🐟 Poisson frais', 1),
|
||||
('pesce', 'poisson_surgele', '🧊 Poisson surgelé', 2),
|
||||
('pesce', 'fruits_mer', '🦐 Fruits de mer', 3),
|
||||
('pesce', 'conserve_poisson', '🥫 Conserve', 4),
|
||||
('pesce', 'autre', '📦 Autre', 5),
|
||||
('frutta', 'agrumes', '🍊 Agrumes', 1),
|
||||
('frutta', 'baies', '🫐 Baies', 2),
|
||||
('frutta', 'fruits_noyau', '🍑 Fruits à noyau', 3),
|
||||
('frutta', 'fruits_tropicaux', '🍍 Fruits tropicaux', 4),
|
||||
('frutta', 'autre', '📦 Autre', 5),
|
||||
('verdura', 'legumes_feuilles', '🥬 Légumes feuilles', 1),
|
||||
('verdura', 'legumes_racines', '🥕 Légumes racines', 2),
|
||||
('verdura', 'legumineuses_fraiches', '🌱 Légumineuses fraîches', 3),
|
||||
('verdura', 'autre', '📦 Autre', 4),
|
||||
('pasta', 'pates', '🍝 Pâtes', 1),
|
||||
('pasta', 'riz', '🍚 Riz', 2),
|
||||
('pasta', 'semoule', '🌾 Semoule', 3),
|
||||
('pasta', 'autre', '📦 Autre', 4),
|
||||
('pane', 'pain_frais', '🍞 Pain frais', 1),
|
||||
('pane', 'biscottes', '🥖 Biscottes', 2),
|
||||
('pane', 'viennoiserie', '🥐 Viennoiserie', 3),
|
||||
('pane', 'autre', '📦 Autre', 4),
|
||||
('surgelati', 'plats_prepares', '🍱 Plats préparés', 1),
|
||||
('surgelati', 'legumes_surgeles', '🧊 Légumes surgelés', 2),
|
||||
('surgelati', 'glaces', '🍨 Glaces', 3),
|
||||
('surgelati', 'viande_poisson_surgele', '🧊 Viande/poisson surgelé', 4),
|
||||
('surgelati', 'autre', '📦 Autre', 5),
|
||||
('bevande', 'vin', '🍷 Vin', 1),
|
||||
('bevande', 'biere', '🍺 Bière', 2),
|
||||
('bevande', 'spiritueux', '🥃 Spiritueux', 3),
|
||||
('bevande', 'soda', '🥤 Soda', 4),
|
||||
('bevande', 'jus', '🧃 Jus', 5),
|
||||
('bevande', 'eau', '💧 Eau', 6),
|
||||
('bevande', 'autre', '📦 Autre', 7),
|
||||
('condimenti', 'huile', '🫒 Huile', 1),
|
||||
('condimenti', 'vinaigre', '🍶 Vinaigre', 2),
|
||||
('condimenti', 'sauce', '🥫 Sauce', 3),
|
||||
('condimenti', 'epice', '🌿 Épice', 4),
|
||||
('condimenti', 'autre', '📦 Autre', 5),
|
||||
('snack', 'chocolat', '🍫 Chocolat', 1),
|
||||
('snack', 'biscuit', '🍪 Biscuit', 2),
|
||||
('snack', 'chips', '🥔 Chips', 3),
|
||||
('snack', 'bonbon', '🍬 Bonbon', 4),
|
||||
('snack', 'autre', '📦 Autre', 5),
|
||||
('conserve', 'legumes_conserve', '🥫 Légumes', 1),
|
||||
('conserve', 'fruits_conserve', '🥫 Fruits', 2),
|
||||
('conserve', 'poisson_conserve', '🥫 Poisson', 3),
|
||||
('conserve', 'confiture', '🍯 Confiture', 4),
|
||||
('conserve', 'autre', '📦 Autre', 5),
|
||||
('cereali', 'cereales_petitdej', '🥣 Céréales petit-déj', 1),
|
||||
('cereali', 'legumineuses_seches', '🫘 Légumineuses sèches', 2),
|
||||
('cereali', 'farine', '🌾 Farine', 3),
|
||||
('cereali', 'autre', '📦 Autre', 4)
|
||||
");
|
||||
}
|
||||
|
||||
// Custom categories table (v2.1) — catégories produit, gérables depuis Config
|
||||
$catTables = $db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='categories'")->fetchAll();
|
||||
if (empty($catTables)) {
|
||||
$db->exec("
|
||||
CREATE TABLE categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key TEXT UNIQUE NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
icon TEXT DEFAULT '📦',
|
||||
keywords TEXT DEFAULT '',
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
is_builtin INTEGER DEFAULT 0
|
||||
);
|
||||
");
|
||||
$db->exec("INSERT INTO categories (key, label, icon, sort_order, is_builtin) VALUES
|
||||
('latticini', 'Latticini', '🥛', 1, 1),
|
||||
('carne', 'Carne', '🥩', 2, 1),
|
||||
('pesce', 'Pesce', '🐟', 3, 1),
|
||||
('frutta', 'Frutta', '🍎', 4, 1),
|
||||
('verdura', 'Verdura', '🥬', 5, 1),
|
||||
('pasta', 'Pasta', '🍝', 6, 1),
|
||||
('pane', 'Pane', '🍞', 7, 1),
|
||||
('surgelati', 'Surgelati', '🧊', 8, 1),
|
||||
('bevande', 'Bevande', '🥤', 9, 1),
|
||||
('condimenti', 'Condimenti', '🧂', 10, 1),
|
||||
('snack', 'Snack', '🍪', 11, 1),
|
||||
('conserve', 'Conserve', '🥫', 12, 1),
|
||||
('cereali', 'Cereali', '🌾', 13, 1),
|
||||
('igiene', 'Igiene', '🧴', 14, 1),
|
||||
('pulizia', 'Pulizia', '🧹', 15, 1),
|
||||
('altro', 'Altro', '📦', 16, 1)
|
||||
");
|
||||
}
|
||||
|
||||
// Recipe library (v2.2) — recettes ajoutées manuellement (cocktails, boissons...), distinctes du planning repas
|
||||
$recipeLibTables = $db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='recipe_library'")->fetchAll();
|
||||
if (empty($recipeLibTables)) {
|
||||
$db->exec("
|
||||
CREATE TABLE recipe_library (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
recipe_json TEXT NOT NULL,
|
||||
is_favorite INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
");
|
||||
}
|
||||
// Recipe tags (v2.3) — tags pour trier/filtrer "Mes recettes", gérables depuis Config
|
||||
$recipeTagsTables = $db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='recipe_tags'")->fetchAll();
|
||||
if (empty($recipeTagsTables)) {
|
||||
$db->exec("
|
||||
CREATE TABLE recipe_tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key TEXT UNIQUE NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
icon TEXT DEFAULT '🏷️',
|
||||
sort_order INTEGER DEFAULT 0
|
||||
);
|
||||
");
|
||||
$db->exec("INSERT INTO recipe_tags (key, label, icon, sort_order) VALUES
|
||||
('cocktail', 'Cocktail', '🍹', 1),
|
||||
('sans_alcool', 'Sans alcool', '🚫', 2),
|
||||
('shot', 'Shot', '🥃', 3),
|
||||
('long_drink', 'Long drink', '🍺', 4),
|
||||
('aperitif', 'Apéritif', '🍾', 5),
|
||||
('digestif', 'Digestif', '❄️', 6),
|
||||
('rhum', 'Rhum', '🥃', 7),
|
||||
('vodka', 'Vodka', '🍸', 8),
|
||||
('gin', 'Gin', '🌿', 9),
|
||||
('whisky', 'Whisky', '🥃', 10),
|
||||
('tequila', 'Tequila', '🌵', 11),
|
||||
('vin', 'Vin', '🍷', 12),
|
||||
('champagne', 'Champagne / Mousseux', '🍾', 13),
|
||||
('acidule', 'Acidulé', '🍋', 14),
|
||||
('sucre', 'Sucré', '🍬', 15),
|
||||
('amer', 'Amer', '☕', 16),
|
||||
('epice', 'Épicé', '🌶️', 17),
|
||||
('fruite', 'Fruité', '🍊', 18),
|
||||
('herbace', 'Herbacé', '🌿', 19),
|
||||
('ete', 'Été', '☀️', 20),
|
||||
('hiver', 'Hiver', '❄️', 21),
|
||||
('soiree', 'Soirée', '🎉', 22),
|
||||
('brunch', 'Brunch', '🥂', 23)
|
||||
");
|
||||
}
|
||||
// Migration: add keywords column to recipe_tags if missing
|
||||
$rtCols = array_column($db->query("PRAGMA table_info(recipe_tags)")->fetchAll(), 'name');
|
||||
if (!in_array('keywords', $rtCols)) {
|
||||
try { $db->exec("ALTER TABLE recipe_tags ADD COLUMN keywords TEXT DEFAULT ''"); }
|
||||
catch (PDOException $e) { if (strpos($e->getMessage(), 'duplicate column') === false) throw $e; }
|
||||
}
|
||||
|
||||
// Custom quantity units (v2.4) — unités personnalisées (ex: kg, L) avec facteur de conversion vers pz/g/ml, gérables depuis Config
|
||||
$customUnitsTables = $db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='custom_units'")->fetchAll();
|
||||
if (empty($customUnitsTables)) {
|
||||
$db->exec("
|
||||
CREATE TABLE custom_units (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key TEXT UNIQUE NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
icon TEXT DEFAULT '📏',
|
||||
base_unit TEXT NOT NULL DEFAULT 'g',
|
||||
factor REAL NOT NULL DEFAULT 1,
|
||||
sort_order INTEGER DEFAULT 0
|
||||
);
|
||||
");
|
||||
}
|
||||
// Add display_unit_key column to products if missing — unité personnalisée a afficher pour ce produit
|
||||
$prodColsUnits = array_column($db->query("PRAGMA table_info(products)")->fetchAll(), 'name');
|
||||
if (!in_array('display_unit_key', $prodColsUnits)) {
|
||||
try { $db->exec("ALTER TABLE products ADD COLUMN display_unit_key TEXT DEFAULT NULL"); }
|
||||
catch (PDOException $e) { if (strpos($e->getMessage(), 'duplicate column') === false) throw $e; }
|
||||
}
|
||||
// Internal shopping list table (v1.8.0) — used when SHOPPING_MODE=internal
|
||||
|
||||
// Internal shopping list table (v1.8.0) — used when SHOPPING_MODE=internal
|
||||
$shopTables = $db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='shopping_list'")->fetchAll();
|
||||
if (empty($shopTables)) {
|
||||
@@ -348,6 +573,12 @@ function migrateDB(PDO $db): void {
|
||||
try { $db->exec("ALTER TABLE products ADD COLUMN nutriments_json TEXT DEFAULT NULL"); }
|
||||
catch (PDOException $e) { if (strpos($e->getMessage(), 'duplicate column') === false) throw $e; }
|
||||
}
|
||||
|
||||
// Add tags column to products if missing (bugfix: insert referenced a column never created)
|
||||
if (!in_array('tags', $prodCols2)) {
|
||||
try { $db->exec("ALTER TABLE products ADD COLUMN tags TEXT DEFAULT NULL"); }
|
||||
catch (PDOException $e) { if (strpos($e->getMessage(), 'duplicate column') === false) throw $e; }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+952
-30
File diff suppressed because it is too large
Load Diff
+9
-4
@@ -2,7 +2,6 @@
|
||||
/**
|
||||
* EverShelf — environment variable loader (.env).
|
||||
*/
|
||||
|
||||
function loadEnv(): array {
|
||||
static $cache = null;
|
||||
if ($cache !== null) {
|
||||
@@ -22,12 +21,18 @@ function loadEnv(): array {
|
||||
}
|
||||
return $cache;
|
||||
}
|
||||
|
||||
function env(string $key, string $default = ''): string {
|
||||
$vars = loadEnv();
|
||||
return $vars[$key] ?? $default;
|
||||
if (isset($vars[$key]) && $vars[$key] !== '') {
|
||||
return $vars[$key];
|
||||
}
|
||||
// Fallback to system/Docker environment variables (e.g. set via Portainer)
|
||||
$sysVal = getenv($key);
|
||||
if ($sysVal !== false && $sysVal !== '') {
|
||||
return $sysVal;
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
/** Push a single key into the in-memory env cache (after .env write). */
|
||||
function envCacheSet(string $key, string $value): void {
|
||||
loadEnv();
|
||||
|
||||
+1473
-135
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
# Retention follows BACKUP_RETENTION_DAYS from .env (default 3)
|
||||
|
||||
set -euo pipefail
|
||||
INSTALL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
INSTALL_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
BACKUP_DIR="${INSTALL_DIR}/data/backups"
|
||||
ENV_FILE="${INSTALL_DIR}/.env"
|
||||
|
||||
|
||||
+9
-2
@@ -5,14 +5,21 @@ services:
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
# Persist database and runtime data
|
||||
- evershelf_data:/var/www/html/data
|
||||
# Mount your local .env configuration
|
||||
- ./.env:/var/www/html/.env:ro
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Europe/Rome
|
||||
networks:
|
||||
- backend
|
||||
- frontend
|
||||
|
||||
volumes:
|
||||
evershelf_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
external: true
|
||||
backend:
|
||||
external: true
|
||||
+150
-48
@@ -126,6 +126,7 @@
|
||||
<button class="header-btn header-gemini-btn" onclick="showPage('chat')" title="Chat con Gemini" data-i18n-title="chat.title">
|
||||
<svg class="gemini-icon" viewBox="0 0 24 24" width="24" height="24" fill="white"><path d="M12 0C12 6.627 6.627 12 0 12c6.627 0 12 5.373 12 12 0-6.627 5.373-12 12-12-6.627 0-12-5.373-12-12z"/></svg>
|
||||
</button>
|
||||
<button class="header-btn" onclick="startManualEntry()" title="Ajouter manuellement">✏️</button>
|
||||
<button class="header-btn header-scan-btn" id="btn-header-scan"
|
||||
title="Scansiona prodotto (tieni premuto per modalità spesa)" data-i18n-title="scan.hint">
|
||||
📷
|
||||
@@ -147,21 +148,6 @@
|
||||
<!-- ===== DASHBOARD ===== -->
|
||||
<section class="page active" id="page-dashboard">
|
||||
<div class="dashboard-stats" id="dashboard-stats">
|
||||
<div class="stat-card" onclick="showPage('inventory', 'dispensa')">
|
||||
<span class="stat-icon">🗄️</span>
|
||||
<span class="stat-value" id="stat-dispensa">-</span>
|
||||
<span class="stat-label" data-i18n="locations.dispensa">Dispensa</span>
|
||||
</div>
|
||||
<div class="stat-card" onclick="showPage('inventory', 'frigo')">
|
||||
<span class="stat-icon">🧊</span>
|
||||
<span class="stat-value" id="stat-frigo">-</span>
|
||||
<span class="stat-label" data-i18n="locations.frigo">Frigo</span>
|
||||
</div>
|
||||
<div class="stat-card" onclick="showPage('inventory', 'freezer')">
|
||||
<span class="stat-icon">❄️</span>
|
||||
<span class="stat-value" id="stat-freezer">-</span>
|
||||
<span class="stat-label" data-i18n="locations.freezer">Freezer</span>
|
||||
</div>
|
||||
<div class="stat-card" onclick="showPage('shopping')">
|
||||
<span class="stat-icon">🛒</span>
|
||||
<span class="stat-value" id="stat-spesa">-</span>
|
||||
@@ -404,7 +390,7 @@
|
||||
<form class="form" onsubmit="submitAdd(event)">
|
||||
<div class="form-group">
|
||||
<label data-i18n="add.location_label">📍 Dove lo metti?</label>
|
||||
<div class="location-selector">
|
||||
<div class="location-selector" id="location-selector-add">
|
||||
<button type="button" class="loc-btn active" onclick="selectLocation(this, 'dispensa')">🗄️ <span data-i18n="locations.dispensa">Dispensa</span></button>
|
||||
<button type="button" class="loc-btn" onclick="selectLocation(this, 'frigo')">🧊 <span data-i18n="locations.frigo">Frigo</span></button>
|
||||
<button type="button" class="loc-btn" onclick="selectLocation(this, 'freezer')">❄️ <span data-i18n="locations.freezer">Freezer</span></button>
|
||||
@@ -452,8 +438,7 @@
|
||||
<p class="form-hint" id="add-vacuum-hint" style="display:none" data-i18n="add.vacuum_hint">La scadenza verrà estesa automaticamente</p>
|
||||
</div>
|
||||
<div class="form-group" id="add-expiry-section">
|
||||
<!-- Populated dynamically by showAddForm() -->
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-large btn-success full-width" data-i18n="add.submit">✅ Aggiungi</button>
|
||||
</form>
|
||||
</section>
|
||||
@@ -660,6 +645,12 @@
|
||||
<option value="altro">📦 Altro</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="pf-subcategory-group" style="display:none">
|
||||
<label>📂 Sous-catégorie <span class="subcategory-required-mark" style="display:none;color:#e74c3c">*</span></label>
|
||||
<select id="pf-subcategory" class="form-input">
|
||||
<option value="">-- Aucune --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group flex-1">
|
||||
<label data-i18n="product.unit_label">📏 Unità di misura</label>
|
||||
@@ -728,6 +719,15 @@
|
||||
✨ Genera nuova ricetta
|
||||
</button>
|
||||
<div id="recipe-archive" class="recipe-archive"></div>
|
||||
|
||||
<div class="settings-section" style="margin-top:24px">
|
||||
<h3 class="settings-section-title">📖 Mes recettes</h3>
|
||||
<p class="settings-hint">Tes propres recettes (cocktails, boissons...), ajoutées à la main.</p>
|
||||
<button class="btn btn-large btn-accent full-width" style="margin-top:10px" onclick="openRecipeLibraryForm()">➕ Ajouter une recette</button>
|
||||
<button class="btn btn-large btn-secondary full-width" style="margin-top:8px" onclick="openRecipeLibraryImportForm()">📋 Importer texte brut</button>
|
||||
<div id="recipe-library-tag-filter" style="display:flex;flex-wrap:wrap;gap:6px;margin-top:14px"></div>
|
||||
<div id="recipe-library-list" style="margin-top:14px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1590,6 +1590,17 @@
|
||||
</div>
|
||||
<button class="btn btn-large btn-accent full-width" onclick="_backupNow()" id="btn-backup-now" data-i18n="settings.backup.backup_now">💾 Backup Ora</button>
|
||||
<div id="backup-status" style="display:none;margin-top:8px" class="settings-status"></div>
|
||||
|
||||
<div style="margin-top:14px;padding-top:14px;border-top:1px solid var(--border,#e2e8f0)">
|
||||
<h4 style="margin-bottom:6px">📦 Export / Import complet</h4>
|
||||
<p class="settings-hint">Exporte la DB + config en un .zip, ou importe un export pour fusionner sans doublons.</p>
|
||||
<a href="api/index.php?action=export_full" class="btn btn-large btn-accent full-width" style="text-decoration:none;display:block;text-align:center;margin-top:8px">📤 Exporter tout</a>
|
||||
<div style="margin-top:8px">
|
||||
<input type="file" id="import-merge-file" accept=".zip" style="display:none" onchange="_importMergeFile(this)">
|
||||
<button class="btn btn-large btn-secondary full-width" onclick="document.getElementById('import-merge-file').click()">📥 Importer (fusion)</button>
|
||||
</div>
|
||||
<div id="import-merge-status" style="display:none;margin-top:8px" class="settings-status"></div>
|
||||
</div>
|
||||
<!-- List of backups -->
|
||||
<div id="backup-list-container" style="margin-top:14px">
|
||||
<p class="settings-hint" data-i18n="settings.info.loading">Caricamento…</p>
|
||||
@@ -1696,18 +1707,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kiosk app download banner (hidden inside kiosk WebView) -->
|
||||
<div id="kiosk-download-banner" style="background:linear-gradient(135deg,rgba(16,185,129,0.08),rgba(5,150,105,0.12));border:1.5px solid rgba(16,185,129,0.25);border-radius:12px;padding:16px;margin-top:16px">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px">
|
||||
<span style="font-size:1.6rem">📺</span>
|
||||
<div>
|
||||
<p style="margin:0;font-weight:700;font-size:0.95rem;color:#065f46">EverShelf Kiosk</p>
|
||||
<p class="settings-hint" style="margin:2px 0 0" data-i18n="settings.kiosk.hint">Trasforma un tablet Android in un pannello EverShelf sempre acceso, con bilancia BLE integrata.</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://github.com/dadaloop82/EverShelf/releases/download/kiosk-latest/evershelf-kiosk.apk" target="_blank" rel="noopener noreferrer" class="btn btn-large btn-accent full-width" style="text-decoration:none;display:block;text-align:center;background:linear-gradient(135deg,#059669,#10b981);color:#fff" data-i18n="settings.kiosk.download_btn">📥 Scarica EverShelf Kiosk (APK)</a>
|
||||
<p class="settings-hint" style="margin-top:8px" data-i18n="settings.kiosk.download_sub">Modalità kiosk full-screen + gateway bilancia integrato. Sorgente: <code>evershelf-kiosk/</code></p>
|
||||
</div>
|
||||
|
||||
<!-- Kiosk native settings panel (visible only inside kiosk WebView) -->
|
||||
<div id="kiosk-native-settings-panel" style="display:none;background:rgba(99,102,241,0.06);border:1.5px solid rgba(99,102,241,0.2);border-radius:12px;padding:16px;margin-top:16px">
|
||||
@@ -1740,25 +1739,124 @@
|
||||
<button class="btn btn-large btn-success full-width mt-2" onclick="saveSettings()" data-i18n="btn.save_config">💾 Salva Configurazione</button>
|
||||
<div id="settings-status" class="settings-status" style="display:none"></div>
|
||||
|
||||
<!-- About & Support -->
|
||||
<div class="settings-section" style="margin-top:24px">
|
||||
<h3 class="settings-section-title" data-i18n="about.title">About</h3>
|
||||
<div class="settings-row" style="justify-content:space-between;align-items:center">
|
||||
<span class="settings-label" data-i18n="about.version">Version</span>
|
||||
<span id="about-version-label" class="settings-hint" style="font-family:monospace">—</span>
|
||||
</section>
|
||||
|
||||
<!-- ===== CONFIGURATION PAGE ===== -->
|
||||
<section class="page" id="page-config">
|
||||
<div class="page-header">
|
||||
<h2>🔧 Configuration</h2>
|
||||
</div>
|
||||
<div class="config-tabs" style="display:flex;gap:6px;margin-bottom:14px;flex-wrap:wrap">
|
||||
<button class="btn btn-small btn-primary config-tab-btn" data-tab="locations" onclick="showConfigTab('locations')">📍 Emplacements</button>
|
||||
<button class="btn btn-small btn-secondary config-tab-btn" data-tab="categories" onclick="showConfigTab('categories')">📁 Catégories</button>
|
||||
<button class="btn btn-small btn-secondary config-tab-btn" data-tab="subcategories" onclick="showConfigTab('subcategories')">📂 Sous-catégories</button>
|
||||
<button class="btn btn-small btn-secondary config-tab-btn" data-tab="recipetags" onclick="showConfigTab('recipetags')">🏷️ Tags recettes</button>
|
||||
<button class="btn btn-small btn-secondary config-tab-btn" data-tab="units" onclick="showConfigTab('units')">📏 Unités</button>
|
||||
</div>
|
||||
|
||||
<div class="config-tab-content" id="config-tab-locations">
|
||||
<div class="settings-card">
|
||||
<h4>📍 Emplacements de stockage</h4>
|
||||
<p class="settings-hint">Gère les emplacements disponibles pour ranger tes produits (Frigo, Cave, Bar...).</p>
|
||||
<div id="locations-list-container" style="margin-top:10px">
|
||||
<p class="settings-hint">Chargement…</p>
|
||||
</div>
|
||||
<div class="form-group mt-2">
|
||||
<label>➕ Nouvel emplacement</label>
|
||||
<div class="barcode-input-row">
|
||||
<input type="text" id="new-location-icon" class="form-input" style="max-width:70px;text-align:center" placeholder="📦" maxlength="4">
|
||||
<input type="text" id="new-location-label" class="form-input" placeholder="Es: Cave, Bar, Garage..." onkeydown="if(event.key==='Enter'){event.preventDefault();addLocation()}">
|
||||
<button class="btn btn-accent" onclick="addLocation()">➕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:10px;display:flex;flex-direction:column;gap:8px">
|
||||
<button class="btn btn-outline full-width" onclick="reportBugManual()" id="btn-report-bug">
|
||||
🐛 <span data-i18n="about.report_bug">Segnala un problema</span>
|
||||
</button>
|
||||
<p class="settings-hint" style="text-align:center;margin:0" data-i18n="about.report_bug_hint">Qualcosa non funziona? Inviaci una segnalazione direttamente dall'app.</p>
|
||||
<div style="display:flex;gap:8px">
|
||||
<a class="btn btn-outline full-width" style="text-decoration:none;text-align:center"
|
||||
href="https://github.com/dadaloop82/EverShelf/blob/main/CHANGELOG.md"
|
||||
target="_blank" rel="noopener" data-i18n="about.changelog">Changelog</a>
|
||||
<a class="btn btn-outline full-width" style="text-decoration:none;text-align:center"
|
||||
href="https://github.com/dadaloop82/EverShelf"
|
||||
target="_blank" rel="noopener" data-i18n="about.github">GitHub</a>
|
||||
</div>
|
||||
|
||||
<div class="config-tab-content" id="config-tab-categories" style="display:none">
|
||||
<div class="settings-card">
|
||||
<h4>📁 Catégories</h4>
|
||||
<p class="settings-hint">Gère les catégories de produits : icône, libellé, et mots-clés de détection automatique à partir du nom du produit.</p>
|
||||
<div id="categories-list-container" style="margin-top:10px">
|
||||
<p class="settings-hint">Chargement…</p>
|
||||
</div>
|
||||
<div class="form-group mt-2">
|
||||
<label>➕ Nouvelle catégorie</label>
|
||||
<div class="barcode-input-row">
|
||||
<input type="text" id="new-category-icon" class="form-input" style="max-width:70px;text-align:center" placeholder="📦" maxlength="4">
|
||||
<input type="text" id="new-category-label" class="form-input" placeholder="Es: Apéritifs, Bébé...">
|
||||
<button class="btn btn-accent" onclick="addCategory()">➕</button>
|
||||
</div>
|
||||
<input type="text" id="new-category-keywords" class="form-input mt-1" placeholder="Mots-clés séparés par des virgules (ex: chips, apéro, biscuit salé)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-tab-content" id="config-tab-subcategories" style="display:none">
|
||||
<div class="settings-card">
|
||||
<h4>📂 Sous-catégories</h4>
|
||||
<p class="settings-hint">Gère les sous-catégories disponibles pour chaque catégorie de produit.</p>
|
||||
<div class="form-group">
|
||||
<label>Catégorie</label>
|
||||
<select id="subcat-config-category" class="form-input" onchange="onSubcatConfigCategoryChange()"></select>
|
||||
</div>
|
||||
<div class="form-group" style="display:flex;align-items:center;gap:8px">
|
||||
<input type="checkbox" id="subcat-config-required" onchange="toggleSubcategoryRequired()" style="width:auto">
|
||||
<label for="subcat-config-required" style="margin:0">Sous-catégorie obligatoire pour cette catégorie</label>
|
||||
</div>
|
||||
<div id="subcat-list-container" style="margin-top:10px">
|
||||
<p class="settings-hint">Chargement…</p>
|
||||
</div>
|
||||
<div class="form-group mt-2">
|
||||
<label>➕ Nouvelle sous-catégorie (pour la catégorie sélectionnée)</label>
|
||||
<div class="barcode-input-row">
|
||||
<input type="text" id="new-subcat-label" class="form-input" placeholder="Es: 🍷 Vin, 🍗 Poulet..." onkeydown="if(event.key==='Enter'){event.preventDefault();addSubcategoryRow()}">
|
||||
<button class="btn btn-accent" onclick="addSubcategoryRow()">➕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-tab-content" id="config-tab-recipetags" style="display:none">
|
||||
<div class="settings-card">
|
||||
<h4>🏷️ Tags recettes</h4>
|
||||
<p class="settings-hint">Gère les tags utilisés pour trier/filtrer "Mes recettes" (cocktails, boissons...).</p>
|
||||
<div id="recipe-tags-list-container" style="margin-top:10px">
|
||||
<p class="settings-hint">Chargement…</p>
|
||||
</div>
|
||||
<div class="form-group mt-2">
|
||||
<label>➕ Nouveau tag</label>
|
||||
<div class="barcode-input-row">
|
||||
<input type="text" id="new-recipe-tag-icon" class="form-input" style="max-width:70px;text-align:center" placeholder="🏷️" maxlength="4">
|
||||
<input type="text" id="new-recipe-tag-label" class="form-input" placeholder="Es: Tiki, Sans alcool...">
|
||||
<input type="text" id="new-recipe-tag-keywords" class="form-input" placeholder="Mots-clés (ex: citron, lime, vinaigre)" onkeydown="if(event.key==='Enter'){event.preventDefault();addRecipeTagConfig()}">
|
||||
<button class="btn btn-accent" onclick="addRecipeTagConfig()">➕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-tab-content" id="config-tab-units" style="display:none">
|
||||
<div class="settings-card">
|
||||
<h4>📏 Unités</h4>
|
||||
<p class="settings-hint">Ajoute des unités personnalisées (ex: kg, L) qui se convertissent automatiquement en g/ml/pz pour le stockage, mais s'affichent dans leur propre unité partout dans l'app.</p>
|
||||
<div id="custom-units-list-container" style="margin-top:10px">
|
||||
<p class="settings-hint">Chargement…</p>
|
||||
</div>
|
||||
<div class="form-group mt-2">
|
||||
<label>➕ Nouvelle unité</label>
|
||||
<div class="barcode-input-row">
|
||||
<input type="text" id="new-unit-icon" class="form-input" style="max-width:70px;text-align:center" placeholder="📏" maxlength="4">
|
||||
<input type="text" id="new-unit-key" class="form-input" style="max-width:80px" placeholder="Es: kg" maxlength="10">
|
||||
<input type="text" id="new-unit-label" class="form-input" placeholder="Es: kg (Kilogrammes)">
|
||||
</div>
|
||||
<div class="barcode-input-row" style="margin-top:8px">
|
||||
<select id="new-unit-base" class="form-input">
|
||||
<option value="g">g (poids)</option>
|
||||
<option value="ml">ml (volume)</option>
|
||||
<option value="pz">pz (pièces)</option>
|
||||
</select>
|
||||
<input type="number" id="new-unit-factor" class="form-input" placeholder="Facteur (ex: 1000)" min="0.001" step="any">
|
||||
<button class="btn btn-accent" onclick="addCustomUnitConfig()">➕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1820,6 +1918,10 @@
|
||||
<span class="nav-icon">📋</span>
|
||||
<span class="nav-label" data-i18n="nav.log">Storico</span>
|
||||
</button>
|
||||
<button class="nav-btn" onclick="showPage('config')" data-page="config">
|
||||
<span class="nav-icon">🔧</span>
|
||||
<span class="nav-label">Config.</span>
|
||||
</button>
|
||||
<button class="nav-btn" onclick="showPage('settings')" data-page="settings">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
<span class="nav-label" data-i18n="nav.settings">Config</span>
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"short_name": "EverShelf",
|
||||
"description": "Gestione completa della dispensa di casa con scansione barcode",
|
||||
"version": "1.7.42",
|
||||
"start_url": "/evershelf/",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#f0f4e8",
|
||||
"theme_color": "#2d5016",
|
||||
|
||||
@@ -471,7 +471,13 @@
|
||||
"storage_immediately": "À consommer immédiatement",
|
||||
"stream_interrupted": "Génération interrompue (réponse serveur incomplète). Vérifiez les logs ou réessayez.",
|
||||
"ing_stock_line": "Vous avez {have} · il reste {remain} après usage",
|
||||
"ing_use_all_note": "tout utiliser (<5% du conditionnement entier)"
|
||||
"ing_use_all_note": "tout utiliser (<5% du conditionnement entier)",
|
||||
"shopping_suggestions_intro": "Pour une variante, il faudrait (pas dans le garde-manger — optionnel) :",
|
||||
"shopping_suggestions_add": "Ajouter à la liste de courses",
|
||||
"shopping_suggestions_added": "Ajouté à la liste de courses",
|
||||
"frozen_badge": "surgelé — du congélateur",
|
||||
"unit_for_input": "Unité de mesure",
|
||||
"enter_in": "Saisie en"
|
||||
},
|
||||
"shopping": {
|
||||
"title": "🛒 Liste de courses",
|
||||
|
||||
Reference in New Issue
Block a user