docs: add live demo banner to README + complete wiki (9 pages)

README:
- Added prominent live demo banner at the top with links to
  https://evershelfproject.dadaloop.it/demo and project website

Wiki (docs/wiki/):
- Home.md         — overview, table of contents, what's new
- Installation.md — Docker, Apache, Nginx, HTTPS, cron, backup
- Configuration.md — full .env reference, settings UI, rate limits
- Features.md     — complete feature documentation
- API-Reference.md — all REST endpoints with params/responses
- Android-Kiosk.md — setup wizard, permissions, troubleshooting
- Scale-Gateway.md — BLE protocol, setup, troubleshooting
- Translations.md  — how to add/edit language files
- Contributing.md  — dev workflow, branch strategy, CI, code style
- FAQ.md           — common issues and solutions
This commit is contained in:
dadaloop82
2026-05-04 20:01:45 +00:00
parent d02e48543f
commit 36d2328eb4
11 changed files with 1818 additions and 1 deletions
+17 -1
View File
@@ -2,7 +2,23 @@
> **Self-hosted pantry management system** — Track your food inventory, scan barcodes, get AI-powered recipe suggestions, and reduce waste. > **Self-hosted pantry management system** — Track your food inventory, scan barcodes, get AI-powered recipe suggestions, and reduce waste.
🌐 **Website:** [evershelfproject.dadaloop.it](https://evershelfproject.dadaloop.it/) ---
<div align="center">
### 🚀 Try the live demo — no installation required!
**[▶ Open Live Demo](https://evershelfproject.dadaloop.it/demo)**
&nbsp;·&nbsp;
[🌐 Project Website](https://evershelfproject.dadaloop.it/)
&nbsp;·&nbsp;
[📖 Wiki](docs/wiki/Home.md)
*The demo runs with mock pantry data. AI features are fully enabled. All write operations are safely sandboxed.*
</div>
---
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![PHP](https://img.shields.io/badge/PHP-8.0+-blue.svg)](https://www.php.net/) [![PHP](https://img.shields.io/badge/PHP-8.0+-blue.svg)](https://www.php.net/)
+332
View File
@@ -0,0 +1,332 @@
# 🔌 API Reference
EverShelf exposes a single PHP endpoint: **`api/index.php`**. All actions are selected via the `action` query parameter.
> **Full OpenAPI 3.1 spec:** [`docs/openapi.yaml`](https://github.com/dadaloop82/EverShelf/blob/main/docs/openapi.yaml)
---
## Base URL
```
https://your-server/api/index.php?action=ACTION_NAME
```
GET requests pass parameters as query params; POST requests send JSON in the body.
---
## Rate Limits
| Tier | Limit | Applies to |
|------|-------|-----------|
| Standard | 120 req/min | All general endpoints |
| AI | 15 req/min | `gemini_*`, `generate_recipe*` |
| Strict | 5 req/min | `report_error` |
Exceeded limits return HTTP 429 with `{"error": "rate_limit_exceeded"}`.
---
## Products
### `search_barcode` — GET
Search for a product in the local database by barcode.
| Param | Type | Description |
|-------|------|-------------|
| `barcode` | string | EAN/UPC barcode |
### `lookup_barcode` — GET
Look up a barcode on Open Food Facts (external call).
| Param | Type | Description |
|-------|------|-------------|
| `barcode` | string | EAN/UPC barcode |
### `product_save` — POST
Create or update a product. Pass `id` to update.
```json
{
"id": 42,
"name": "Pasta Barilla",
"brand": "Barilla",
"category": "pasta",
"unit": "g",
"default_quantity": 500,
"barcode": "8076800105988"
}
```
### `product_get` — GET
Get product details by `id`.
### `product_delete` — POST
Delete a product by `id`.
### `products_list` — GET
List all products.
### `products_search` — GET
Search products by name (`?q=pasta`).
---
## Inventory
### `inventory_list` — GET
List all inventory items with product details, grouped.
**Response:**
```json
{
"inventory": [
{
"id": 1,
"product_id": 42,
"name": "Pasta Barilla",
"quantity": 2,
"unit": "pz",
"location": "dispensa",
"expiry_date": "2027-03-01",
"opened_at": null,
"vacuum_sealed": 0
}
]
}
```
### `inventory_add` — POST
Add a product to inventory.
```json
{
"product_id": 42,
"quantity": 3,
"location": "dispensa",
"expiry_date": "2027-03-01",
"vacuum_sealed": false
}
```
**Locations:** `dispensa`, `frigo`, `freezer`, `altro`
### `inventory_use` — POST
Consume inventory. Set `use_all: true` to consume all stock at a location.
```json
{
"product_id": 42,
"quantity": 1,
"location": "dispensa"
}
```
```json
{
"product_id": 42,
"use_all": true,
"location": "__all__",
"notes": "Buttato"
}
```
### `inventory_update` — POST
Update an inventory entry by `id`.
### `inventory_delete` — POST
Remove an inventory entry by `id`.
### `inventory_summary` — GET
Returns item counts per location.
```json
{
"dispensa": 12,
"frigo": 5,
"freezer": 8
}
```
---
## Transactions (Log)
### `transactions_list` — GET
Returns the operation log.
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `limit` | int | 50 | Results per page |
| `offset` | int | 0 | Pagination offset |
### `transaction_undo` — POST
Undo a transaction within 24 hours.
```json
{ "id": 873 }
```
**Response on success:**
```json
{ "success": true, "name": "Tonno all'olio d'oliva" }
```
**Error cases:**
```json
{ "error": "...", "already_undone": true }
{ "error": "...", "too_old": true }
```
### `stats` — GET
Returns waste and consumption statistics for the last 30 days.
---
## AI / Gemini
All AI endpoints require `GEMINI_API_KEY` to be configured. Rate limit: 15 req/min.
### `gemini_expiry` — POST
Read an expiry date from a product photo.
```json
{ "image": "data:image/jpeg;base64,..." }
```
### `gemini_identify` — POST
Identify a product from a photo.
```json
{ "image": "data:image/jpeg;base64,..." }
```
### `gemini_chat` — POST
Chat with the AI kitchen assistant.
```json
{ "message": "Cosa posso fare con la pasta?", "history": [] }
```
### `generate_recipe` — POST
Generate a recipe based on current inventory.
```json
{ "persons": 2, "meal": "dinner", "preferences": {} }
```
### `generate_recipe_stream` — POST
Same as `generate_recipe` but streams output via Server-Sent Events.
### `gemini_product_hint` — POST
Get AI storage location + shelf-life hint for a new product.
### `gemini_shopping_enrich` — POST
Enrich shopping suggestions with practical tips.
### `gemini_anomaly_explain` — POST
Get a plain-language explanation for a specific inventory anomaly.
---
## Shopping List (Bring!)
Requires `BRING_EMAIL` and `BRING_PASSWORD` in `.env`.
### `bring_list` — GET
Get the current Bring! shopping list.
### `bring_add` — POST
Add items to the Bring! list.
```json
{ "items": ["Latte", "Pane"] }
```
### `bring_remove` — POST
Remove an item from the Bring! list.
```json
{ "name": "Latte" }
```
### `smart_shopping` — GET
Get smart shopping predictions based on consumption history.
---
## Settings
### `get_settings` — GET
Returns current settings as **boolean flags only** (no raw key values):
```json
{
"gemini_key_set": true,
"bring_configured": false,
"tts_enabled": false,
"scale_enabled": true,
"demo_mode": false,
"settings_token_set": true
}
```
### `save_settings` — POST
Update server configuration. If `SETTINGS_TOKEN` is set, requires header:
```
X-Settings-Token: your_token
```
```json
{
"gemini_api_key": "...",
"bring_email": "...",
"scale_enabled": true,
"scale_gateway_url": "ws://127.0.0.1:8765"
}
```
---
## Error Reporting
### `report_error` — POST
Submit an automatic error report (creates a GitHub Issue).
```json
{
"type": "uncaught-error",
"message": "...",
"stack": "...",
"context": {}
}
```
Only creates an issue if:
- The client is running the latest released version
- The fingerprint hasn't been seen in the last 24 hours
---
## Anomaly Detection
### `inventory_anomalies` — GET
Returns inventory rows where stored quantity significantly differs from transaction history.
### `dismiss_anomaly` — POST
Dismiss an anomaly banner without changing inventory.
---
## Scale Integration
### `scale_relay` (SSE) — GET
Relays BLE scale readings from the gateway to the browser via Server-Sent Events (avoids HTTPS→WS mixed-content issues).
### `scale_ping` — GET
Check if the Scale Gateway is reachable.
### `scale_discover` — GET
Scan the local LAN for a running Scale Gateway instance.
+141
View File
@@ -0,0 +1,141 @@
# 📺 Android Kiosk App
The EverShelf Kiosk app turns any Android tablet into a dedicated, locked-down kitchen display running EverShelf full-screen.
---
## Download
**[⬇ Download latest APK](https://github.com/dadaloop82/EverShelf/releases/latest/download/evershelf-kiosk.apk)**
> Current version: **v1.5.0** — requires Android 7.0+
---
## What it does
- Displays the EverShelf web app in a **full-screen WebView** (no browser chrome)
- **Locks the screen** with Android's `startLockTask` — home, recents, and back buttons are blocked
- Runs the **Scale Gateway** app in the background automatically on startup
- Provides a **native TTS bridge** so Cooking Mode reads steps aloud via Android TextToSpeech
- Auto-detects your EverShelf server on the LAN with a **smart discovery scanner**
- Reports errors and install failures back to the developer automatically
---
## Setup Wizard (6 steps)
The wizard runs automatically on first launch.
### Step 1 — Language
Select the app and web interface language (Italian, English, German).
### Step 2 — Welcome
Overview of what the wizard will configure.
### Step 3 — Permissions
Grant camera, microphone, and storage permissions needed by the web app.
The button transforms from "Concedi permessi" to **"✅ Permessi concessi — Continua →"** (green) once all permissions are granted.
### Step 4 — Server URL
Enter your EverShelf server URL (e.g. `https://192.168.1.100/dispensa`).
**Or tap "Rileva automaticamente"** to let the wizard scan your LAN:
- 60 parallel threads, TCP pre-check, ports 80/443/8080/8443
- Only scans your actual Wi-Fi/Ethernet subnet (VPN and cellular interfaces ignored)
- Real-time feedback as hosts are tested
### Step 5 — Scale Gateway
If you have a BLE smart scale, install and configure the Scale Gateway:
1. Tap **"Installa Gateway"** — the APK is downloaded from GitHub and installed via `PackageInstaller`
2. If installation fails, a diagnostic dialog shows: status code, error message, APK size, Android version, and device model — plus a "Riprova" button
3. On success, the wizard automatically writes `scale_enabled=true` and `scale_gateway_url=ws://127.0.0.1:8765` to your EverShelf server
### Step 6 — Screensaver
Choose whether the screen should go dark after inactivity.
### Summary
All done — the web app loads in full-screen kiosk mode.
---
## Exiting Kiosk Mode
Tap the **✕** button in the header (top-left). A confirmation dialog appears.
---
## Hard Refresh
Tap the **↻** button in the header to clear the WebView cache and reload the latest version of the web app.
---
## Update Notifications
Every 6 hours the app checks GitHub releases. If a newer version is available, a banner appears with a one-tap download and install flow.
---
## Native TTS Bridge
When Cooking Mode reads recipe steps, the kiosk app:
1. Intercepts the TTS call from the web app via a JavaScript bridge
2. Uses the Android `TextToSpeech` engine directly
3. Falls back to the browser Web Speech API if the bridge is unavailable
No internet connection required for TTS. No extra voice packs to install.
---
## SSL / Self-signed Certificates
The WebView accepts self-signed certificates automatically. No configuration needed for local HTTPS servers.
---
## Troubleshooting
### "Impossibile installare il gateway"
- Make sure "Install from unknown sources" is enabled for the kiosk app in Android Settings → Apps → Special app access
- Check that there is enough free storage (the APK is ~15 MB)
- The diagnostic dialog shows the exact failure code — include it when opening an issue
### "Server non trovato" during auto-discovery
- Make sure your tablet and server are on the same Wi-Fi network
- Ensure the server is not on a VPN-only interface
- Try entering the URL manually
### Screen pinning / back button not working
- Screen pinning requires the app to be set as Device Owner or the user to confirm the pin prompt
- Some Android skins (Samsung, Xiaomi) may require additional accessibility permissions
### App crashes on startup
- Force-stop the app, clear its data (Settings → Apps → EverShelf Kiosk → Clear data), and relaunch
---
## Building from Source
```bash
cd evershelf-kiosk
./gradlew assembleRelease
# APK: app/build/outputs/apk/release/app-release.apk
```
Requires Android Studio or JDK 17+ with the Android SDK.
---
## Permissions
| Permission | Purpose |
|-----------|---------|
| `INTERNET` | Load the EverShelf web app |
| `CAMERA` | Barcode scanning and AI photo identification |
| `RECORD_AUDIO` | Voice input in AI chat |
| `WAKE_LOCK` | Keep the screen on |
| `REQUEST_INSTALL_PACKAGES` | Install the Scale Gateway APK |
| `ACCESS_WIFI_STATE` | LAN auto-discovery |
| `REORDER_TASKS` | Bring app to foreground after gateway launch |
+157
View File
@@ -0,0 +1,157 @@
# ⚙️ Configuration
EverShelf is configured via a `.env` file in the project root. Copy `.env.example` to `.env` and edit it — the app reads this file on every API call.
**Never commit `.env` to Git.** It is already in `.gitignore`.
---
## Full `.env` Reference
```ini
# ─────────────────────────────────────────────
# AI — Google Gemini
# ─────────────────────────────────────────────
# Your Gemini API key (required for all AI features)
# Get one free at: https://aistudio.google.com/app/apikey
GEMINI_API_KEY=
# ─────────────────────────────────────────────
# Shopping List — Bring! Integration
# ─────────────────────────────────────────────
# Your Bring! account credentials
# Leave blank to disable Bring! integration
BRING_EMAIL=
BRING_PASSWORD=
# ─────────────────────────────────────────────
# Text-to-Speech (for Cooking Mode)
# ─────────────────────────────────────────────
# URL to a TTS endpoint (e.g. Home Assistant event endpoint)
TTS_URL=
# Bearer token for the TTS endpoint
TTS_TOKEN=
# Set to true to enable server-side TTS (the browser Web Speech API is always used as fallback)
TTS_ENABLED=false
# ─────────────────────────────────────────────
# Security
# ─────────────────────────────────────────────
# Protect the save_settings endpoint with a token
# If set, the Settings UI will prompt for this value before saving
# Validated with hash_equals() to prevent timing attacks
SETTINGS_TOKEN=
# ─────────────────────────────────────────────
# Demo / Public Mode
# ─────────────────────────────────────────────
# Set to true to block ALL write operations at the PHP router level
# Useful for public demos or read-only kiosk deployments
# Also activatable per-request via ?demo=1 URL parameter
DEMO_MODE=false
# ─────────────────────────────────────────────
# Scale Gateway
# ─────────────────────────────────────────────
# Enable the BLE scale integration
SCALE_ENABLED=false
# WebSocket URL of the Scale Gateway app running on the same device
# Default for Android kiosk: ws://127.0.0.1:8765
SCALE_GATEWAY_URL=ws://127.0.0.1:8765
```
---
## Settings UI
Most settings can also be configured from the browser via **Settings → ⚙️**:
| Setting | `.env` key | Notes |
|---------|-----------|-------|
| Gemini API key | `GEMINI_API_KEY` | Stored server-side, never exposed to browser |
| Bring! email | `BRING_EMAIL` | — |
| Bring! password | `BRING_PASSWORD` | — |
| TTS URL | `TTS_URL` | — |
| TTS token | `TTS_TOKEN` | — |
| TTS enabled | `TTS_ENABLED` | — |
| Scale enabled | `SCALE_ENABLED` | — |
| Scale gateway URL | `SCALE_GATEWAY_URL` | — |
| Settings token | `SETTINGS_TOKEN` | Write-only; current value never shown |
> **Security note:** `get_settings` returns only **boolean flags** (`gemini_key_set: true/false`), never raw key values. Raw values are only accessible server-side.
---
## Protecting Settings with a Token
If your EverShelf instance is accessible from untrusted networks, set `SETTINGS_TOKEN` to a strong random string:
```bash
# Generate a strong token
openssl rand -hex 32
```
```ini
SETTINGS_TOKEN=a3f9b2c1d4e5...
```
Users will be prompted for this token before any Settings save. If the token doesn't match, the request is rejected with HTTP 403.
---
## Demo Mode
Two ways to enable demo mode:
1. **Permanent:** Set `DEMO_MODE=true` in `.env`
2. **Per-session:** Append `?demo=1` to any URL (e.g. `https://evershelfproject.dadaloop.it/demo`)
In demo mode:
- All POST/write API calls return success without touching the database
- A "DEMO" badge appears in the header
- Gemini AI is treated as available (mock responses)
- Bring! write operations are silently no-op'd
- A mock pantry with sample data is loaded
---
## API Rate Limiting
EverShelf applies file-based rate limiting to protect AI endpoints:
| Tier | Limit | Endpoints |
|------|-------|-----------|
| Standard | 120 req/min | All general endpoints |
| AI | 15 req/min | `gemini_*`, `generate_recipe` |
| Strict | 5 req/min | `report_error` |
Rate limit state is stored in `data/rate_limits/`. To reset, delete the files in that directory.
---
## Database
EverShelf uses **SQLite** stored at `data/evershelf.db`. The file is created automatically on first run.
Schema migrations run automatically whenever `database.php` is loaded — no manual migration steps needed.
To back up the database:
```bash
cp data/evershelf.db data/backups/evershelf-$(date +%Y%m%d).db
```
Or use the included `backup.sh`:
```bash
./backup.sh
```
+164
View File
@@ -0,0 +1,164 @@
# 🤝 Contributing
Contributions of all kinds are welcome — bug fixes, new features, translations, documentation improvements.
---
## Getting Started
### 1. Fork and clone
```bash
git clone https://github.com/YOUR_USERNAME/EverShelf.git
cd EverShelf
```
### 2. Create a branch
```bash
git checkout -b feature/my-feature
# or
git checkout -b fix/my-bug-fix
```
### 3. Set up a local server
```bash
# Option A: PHP built-in server
php -S localhost:8080
# Option B: Docker
docker compose up -d
```
Open `http://localhost:8080` in your browser.
### 4. Make your changes
The app has **no build step**. Edit files directly and refresh the browser.
Key files:
- `assets/js/app.js` — all frontend logic
- `assets/css/style.css` — all styles
- `api/index.php` — all API endpoints
- `api/database.php` — SQLite schema and migrations
- `translations/*.json` — i18n strings
### 5. Test
```bash
# Check PHP syntax
php -l api/index.php
php -l api/database.php
# Check JS syntax
node --check assets/js/app.js
```
There are no automated JS tests yet — manual testing in the browser is the current approach. If you add a feature, test the full flow: add, use, undo.
### 6. Commit
Use [Conventional Commits](https://www.conventionalcommits.org/):
```bash
git commit -m "feat(inventory): add bulk delete"
git commit -m "fix(scale): handle BLE disconnect during countdown"
git commit -m "docs: update kiosk setup guide"
git commit -m "chore: bump version to 1.8.0"
```
Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
Scopes: `inventory`, `ai`, `shopping`, `cooking`, `scale`, `kiosk`, `gateway`, `webapp`, `api`, `db`
### 7. Push and open a PR
```bash
git push origin feature/my-feature
```
Open a Pull Request against the `develop` branch (not `main`).
---
## Branch Strategy
| Branch | Purpose |
|--------|---------|
| `main` | Production — auto-deployed, never commit directly |
| `develop` | Integration branch — all PRs target here |
| `feature/*` | New features |
| `fix/*` | Bug fixes |
CI auto-merges `develop → main` on every push to `develop`.
---
## CI / CD Pipeline
GitHub Actions runs on every push to `develop` and `main`:
1. **PHP lint**`php -l` on all PHP files
2. **JS syntax check**`node --check assets/js/app.js`
3. **Translation validation** — checks that all language files have the same keys
4. **Docker build** — verifies the Docker image builds successfully
5. **Android build** — (on tagged commits) builds Kiosk and Scale Gateway APKs
---
## Adding Translations
See the full guide in [Translations](Translations).
Short version:
1. Copy `translations/it.json``translations/xx.json`
2. Translate all values
3. Add `'xx'` to `SUPPORTED_LANGUAGES` in `app.js`
4. Open a PR
---
## Reporting Bugs
Open an issue on GitHub. Include:
- Steps to reproduce
- Expected vs. actual behaviour
- Browser/OS version
- Any console errors (F12 → Console)
---
## Code Style
- **PHP:** PSR-12, 4-space indent, type hints where practical
- **JavaScript:** ES2020+, `async/await`, no frameworks, 4-space indent
- **CSS:** BEM-ish class names, CSS custom properties for theming
- **SQL:** parameterized queries (PDO), no raw string interpolation
---
## Adding a New API Endpoint
1. Add a `case 'my_action':` to the router in `api/index.php`
2. Implement `function myAction(PDO $db): void`
3. Add the endpoint to `docs/openapi.yaml`
4. Add translations for any new UI strings to all 3 language files
---
## Security
If you find a security vulnerability, **do not open a public issue**. Email [evershelfproject@gmail.com](mailto:evershelfproject@gmail.com) directly.
Relevant resources:
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- All SQL must use PDO prepared statements
- Never expose API keys in API responses (boolean flags only)
- Use `hash_equals()` for token comparison
---
## License
By contributing you agree that your code will be licensed under the [MIT License](https://github.com/dadaloop82/EverShelf/blob/main/LICENSE).
+164
View File
@@ -0,0 +1,164 @@
# ❓ FAQ & Troubleshooting
---
## Installation
### The app shows a blank page after setup
- Open the browser console (F12 → Console) and check for errors
- Make sure PHP is running and `api/index.php` is reachable: visit `https://your-server/dispensa/api/index.php?action=get_settings` — it should return JSON
- Check your web server error log: `tail -f /var/log/apache2/error.log`
### Camera doesn't work / barcode scanner won't open
Camera access requires **HTTPS**. On plain HTTP, browsers block `getUserMedia()`.
- Set up HTTPS with Let's Encrypt, Caddy, or a self-signed certificate
- On Android, you can also add a security exception in Chrome: `chrome://flags/#allow-insecure-localhost`
### "Permission denied" error for the data directory
```bash
chmod 755 data/
chown -R www-data:www-data data/
```
### Docker container exits immediately
```bash
docker compose logs evershelf
```
Usually a permission issue on the mounted `data/` volume. Try:
```bash
docker compose down
rm -rf data/
mkdir data/
docker compose up -d
```
---
## AI Features
### AI features don't work / "AI non disponibile"
1. Check that `GEMINI_API_KEY` is set in `.env`
2. Verify the key is valid at [aistudio.google.com](https://aistudio.google.com)
3. Check that you haven't exceeded the free tier quota (15 req/min, 1500 req/day)
4. Look for errors in the PHP error log
### Recipe generation stops midway
This is usually a Gemini API timeout. The app streams results via SSE — if the server PHP timeout is too low, the stream is cut short. Increase `max_execution_time` in `php.ini`:
```ini
max_execution_time = 120
```
---
## Shopping List (Bring!)
### "Bring! non configurato" message in the shopping tab
Add your Bring! credentials to `.env`:
```ini
BRING_EMAIL=your@email.com
BRING_PASSWORD=yourpassword
```
### Items aren't syncing to Bring!
- Verify your credentials are correct by logging into [getbring.com](https://web.getbring.com/)
- Check for rate-limit errors in the PHP error log — Bring! has API limits
---
## Scale Integration
### Scale readings don't appear in EverShelf
1. Confirm the gateway app is running and shows the WebSocket URL
2. Check the Gateway URL in EverShelf Settings matches exactly
3. Make sure both the Android device and the EverShelf server are on the same network
4. Look at the scale status indicator (⚖️) in the header — "disconnected" means no WebSocket connection
### Scale shows weight but form doesn't auto-fill
- The auto-fill only triggers for products with unit `g` or `ml`
- Make sure you tapped "⚖️ Leggi bilancia" first to activate the scale modal
- The weight must stabilize (stay within 10g) for the countdown to start
### Bluetooth scale not appearing in the gateway app
- Wake up the scale (step on it or press its button)
- Make sure Bluetooth and Location permissions are granted to the gateway app (Location is required by Android for BLE scanning)
- Restart the gateway app
---
## Kiosk App
### Setup wizard can't find my server
- Make sure the tablet is on the same Wi-Fi network as the server
- Try entering the URL manually instead of using auto-discovery
- Check that the server responds on the expected port (80/443/8080/8443)
### Gateway install fails with an error dialog
The dialog shows the exact failure code. Common causes:
| Code | Cause | Fix |
|------|-------|-----|
| `STATUS_FAILURE` (1) | Generic install failure — often OEM restriction | Enable "Install from unknown sources" for the kiosk app in Android Settings |
| `STATUS_FAILURE_CONFLICT` (3) | Signature mismatch with existing install | Uninstall the old gateway app, then retry |
| `STATUS_FAILURE_STORAGE` (6) | Not enough storage | Free up space on the device |
### Exit button (✕) is not visible
The ✕ button is injected into the header by the kiosk app. If the web app's header is covered or the page failed to load, try the hard refresh (↻) button. If neither is visible, triple-tap the page title area to access the developer settings.
### App is stuck in kiosk mode after a crash
Restart the device. Screen pinning is released on reboot.
---
## General
### The version shown in the app is outdated
The version is cached by the browser. Do a hard refresh:
- Desktop: `Ctrl+Shift+R` / `Cmd+Shift+R`
- Android: tap the ↻ button (kiosk) or clear site data in Chrome settings
### Transactions are missing from the log
The log shows the last 50 entries by default. Tap "Carica altri" to load more. Entries older than the database creation date won't appear.
### "Can only undo transactions within 24 hours"
The undo window is 24 hours. For older operations, manually correct the inventory via the Edit function on the affected product.
### Error reports keep creating duplicate GitHub issues
EverShelf uses a fingerprint to deduplicate — the same error from the same device won't create a new issue within 24 hours. If you're seeing duplicates, check the `data/rate_limits/` folder and clear old files.
---
## Getting Help
- **Open an issue:** [github.com/dadaloop82/EverShelf/issues](https://github.com/dadaloop82/EverShelf/issues)
- **Email:** [evershelfproject@gmail.com](mailto:evershelfproject@gmail.com)
- **Try the demo:** [evershelfproject.dadaloop.it/demo](https://evershelfproject.dadaloop.it/demo)
When reporting a bug, include:
1. EverShelf version (shown in the header as `v1.x.x`)
2. Browser and OS
3. Steps to reproduce
4. Any error messages from the browser console (F12)
+219
View File
@@ -0,0 +1,219 @@
# ✨ Features
A complete walkthrough of EverShelf's features.
---
## 📦 Inventory Management
### Adding Products
- Tap **** to open the add form
- Search by name or scan a barcode
- Select storage location: Pantry, Fridge, Freezer, or a custom location
- Enter quantity and expiry date (or let AI estimate it)
- Mark as vacuum-sealed or opened for adjusted shelf-life calculation
### Barcode Scanning
Tap the barcode icon to open the camera scanner (QuaggaJS). The app:
1. Checks your local database first
2. Falls back to [Open Food Facts](https://world.openfoodfacts.org/) for unknown barcodes
3. Pre-fills the product form with name, brand, category
### AI Product Identification
Point the camera at any product — Gemini identifies it and:
- Shows matching products **already in your pantry** first
- Suggests a new product entry with pre-filled fields
- Provides a storage location hint and estimated shelf-life
### Storage Locations
| Location | Icon | Notes |
|----------|------|-------|
| Pantry | 🏠 | Room temperature |
| Fridge | ❄️ | Refrigerated |
| Freezer | 🧊 | Frozen |
| Custom | 📦 | Any name you choose |
### Opened Product Tracking
When you partially use a product and mark it as "opened":
- Shelf-life is recalculated from the opening date
- Uses AI (Gemini) + per-category rules (e.g. fish: 2 days, milk: 3 days)
- Whole sealed packages always keep their original manufacturer expiry
- Products with mixed whole + fractional units show as two separate entries
### Vacuum-Sealed Support
Mark any product as vacuum-sealed to extend its estimated expiry date (typically 23× the normal shelf-life).
---
## 🤖 AI Features (Google Gemini)
All AI features require a `GEMINI_API_KEY` in `.env`. They degrade gracefully when the key is missing or quota is exceeded.
### Expiry Date Reading
Photograph the label on a product — Gemini extracts the expiry date and fills the field automatically.
### Product Identification
Camera-based identification with pantry matching. See [Adding Products](#adding-products) above.
### Storage & Shelf-life Hint
When adding a new product, a background Gemini call suggests:
- Optimal storage location
- Estimated shelf-life in days
Shown as an inline AI badge next to the expiry estimate. Does not block the form.
### Recipe Generation
Tap **🍳 Ricette** → **Genera ricetta** to get a recipe using:
- Ingredients about to expire (prioritised)
- What's currently in your pantry
- Your language preference
Recipes stream live via Server-Sent Events so results appear as they are generated.
### AI Chat Assistant
Open **💬 Chat** to ask questions like:
- "Cosa posso fare con le uova e la pasta?"
- "Quanti giorni dura il prosciutto cotto aperto in frigo?"
- "Suggeriscimi uno spuntino veloce"
The assistant knows your current inventory.
### Shopping Suggestions with Tips
Smart shopping predictions include a short AI-generated practical tip per item (e.g. "Buy the 2 kg bag — it freezes well").
### Anomaly Explanation
When the dashboard shows a suspicious quantity banner, tap **🤖 Spiega** to get a plain-language explanation of why the discrepancy likely occurred and what to do about it.
### Model Fallback
All AI endpoints try `gemini-2.5-flash` first and automatically fall back to `gemini-2.0-flash` if unavailable.
---
## 🛒 Shopping List (Bring! Integration)
Configure `BRING_EMAIL` and `BRING_PASSWORD` in `.env` to enable.
### Features
- **View and manage** your Bring! list inside EverShelf
- **Auto-add on depletion** — when stock hits zero, the product is added to Bring! automatically
- **Auto-remove on scan** — scanning a product in removes it from the shopping list
- **Generic names** — products are grouped by type ("Latte", "Panna da cucina") not brand, keeping the list clean
- **Auto-migration** — items already on Bring! are silently renamed to their generic name on list load
- **Catalog coverage** — 100+ product types mapped to Bring! catalog keys for icons and categories in the Bring! app
- **AI fallback** — unknown product types use Gemini to determine the best generic name
---
## 🍳 Cooking Mode
Start cooking mode from any recipe by tapping **▶ Avvia cottura**.
### Features
- **Step-by-step guidance** — fullscreen, distraction-free interface
- **Text-to-Speech** — each step is read aloud automatically when you navigate; supports:
- Browser Web Speech API (default)
- Native Android TTS (kiosk app)
- Custom REST endpoint (e.g. Home Assistant)
- **Built-in timers** — automatic timer suggestions based on recipe text; 10-second vocal countdown warning before expiry
- **Ingredient tracking** — mark ingredients as used; leftover quantities prompt a "move to another location" flow
- **Recipe completion** — "Buon appetito!" spoken on the last step
---
## 📊 Dashboard
### Inventory Overview
Three stat cards at the top show item counts for Pantry, Fridge, and Freezer with animated skeleton loading while data fetches.
### Expiry Alerts Banner
Priority-sorted notifications for:
- Expired products (with safety assessment — green ✅ safe, amber 👀 check, red 🚫 danger)
- Products expiring within 3 days
Actions per item: Use, Throw away, Edit, Dismiss. Swipe or tap arrows to navigate.
### Anomaly Banner
Highlights suspicious quantities (e.g. "You have 0 eggs but used 12 this month"). Actions:
- One-tap correction to the suggested quantity
- Inline edit with free-form quantity
- "🤖 Spiega" for AI explanation
- Dismiss (with current quantity shown: "La quantità è giusta (2 pz)")
### Anti-Waste Report
Shows your waste rate vs. the national average with an estimated annual kg of food wasted.
### Quick Recipe Bar
One-tap recipe suggestion using the ingredients closest to expiry.
---
## 📱 Progressive Web App (PWA)
EverShelf is installable as a PWA on any device:
1. Open in Chrome/Safari/Edge
2. Tap **"Add to Home Screen"** (browser menu)
3. Launch from the home screen like a native app
Features:
- Offline-capable shell (assets cached)
- Full-screen mode on mobile
- Multi-device: all data syncs via the shared server
---
## 🔔 Update Notifications
When a new EverShelf release is published on GitHub, a small pill appears in the header. Click it to see the changelog. Checked on load and every 30 minutes.
---
## 🌍 Multi-language
The app auto-detects your browser language. Supported: 🇮🇹 Italian, 🇬🇧 English, 🇩🇪 German.
Change the language in **Settings → Language**.
See [Translations](Translations) to add a new language.
---
## ↩ Transaction History & Undo
**Settings → Storico** shows all inventory operations (adds, uses, throws).
- Any operation within the **last 24 hours** shows a red ↩ undo button
- Tapping ↩ shows a 5-second countdown confirmation before reversing the transaction
- The original stock is restored and a counter-transaction is logged
---
## 🔒 Security Features
- API keys never exposed to the browser (`get_settings` returns boolean flags only)
- `save_settings` protected by optional `SETTINGS_TOKEN` (validated with `hash_equals`)
- `DEMO_MODE=true` blocks all write operations at the PHP router level
- Parameterized SQL queries (PDO prepared statements) throughout
- Input validation on all inventory operations (quantity bounds, location whitelist)
- See [Configuration](Configuration) for details
+93
View File
@@ -0,0 +1,93 @@
# 🏠 EverShelf Wiki
Welcome to the **EverShelf** project wiki — your complete reference for installation, configuration, features, and development.
---
## 🚀 Try it now
> **[▶ Live Demo](https://evershelfproject.dadaloop.it/demo)** — no installation, no login, full AI enabled
> **[🌐 Project Website](https://evershelfproject.dadaloop.it/)**
---
## 📚 Wiki Contents
| Page | Description |
|------|-------------|
| [Installation](Installation) | Docker, manual setup, HTTPS, web server config |
| [Configuration](Configuration) | `.env` reference — all options explained |
| [Features](Features) | Complete feature documentation |
| [API Reference](API-Reference) | All REST endpoints, parameters, and responses |
| [Android Kiosk](Android-Kiosk) | Tablet kiosk app setup and usage |
| [Scale Gateway](Scale-Gateway) | BLE smart scale integration |
| [Translations](Translations) | Adding and editing language files |
| [Contributing](Contributing) | Development workflow and PR process |
| [FAQ & Troubleshooting](FAQ) | Common issues and solutions |
---
## ✨ What is EverShelf?
EverShelf is a **self-hosted pantry management system** that runs entirely on your own server. It:
- Tracks food inventory across multiple storage locations (pantry, fridge, freezer, custom)
- Scans barcodes and uses **Google Gemini AI** to identify products from photos
- Suggests recipes based on what's in your pantry — especially items about to expire
- Predicts what you'll need to buy before you run out
- Integrates with the **Bring!** shopping list app
- Supports a **BLE smart scale** for weight-based tracking
- Runs as a **Progressive Web App** installable on any device
- Optionally pairs with a dedicated **Android kiosk tablet app**
All data stays on your server. No cloud, no subscriptions.
---
## 🆕 What's New
### v1.7.1 (2026-05-04)
- Destructive actions ("Butta tutto", "Finisci tutto") now require a **5-second countdown confirmation** before executing
- History undo button ↩ is now clearly visible (red tint, larger)
- Undo confirmation uses the in-app modal instead of the native browser `confirm()`
### v1.7.0 (2026-05-04)
- Smart auto-discovery rewrite (kiosk)
- Gateway auto-pre-configuration after install
- ErrorReporter init at setup start
- Graceful Bring! no-key state
- Use-quantity guard with shake animation
- Demo mode (`?demo=1`)
→ See the full [CHANGELOG](https://github.com/dadaloop82/EverShelf/blob/main/CHANGELOG.md)
---
## 📦 Repository Structure
```
EverShelf/
├── index.html # Single-page application entry point
├── manifest.json # PWA manifest
├── .env.example # Configuration template
├── api/
│ ├── index.php # Main API router
│ ├── database.php # SQLite schema + migrations
│ └── cron_smart_shopping.php # Background predictions job
├── assets/
│ ├── css/style.css
│ ├── js/app.js
│ └── img/
├── translations/ # i18n JSON files (it, en, de)
├── docs/openapi.yaml # OpenAPI 3.0 spec
├── evershelf-kiosk/ # Android kiosk app (Kotlin)
└── evershelf-scale-gateway/ # Android BLE gateway app (Kotlin)
```
---
## 📄 License
MIT — free to use, modify, and distribute. See [LICENSE](https://github.com/dadaloop82/EverShelf/blob/main/LICENSE).
**Author:** Stimpfl Daniel — [evershelfproject@gmail.com](mailto:evershelfproject@gmail.com)
+233
View File
@@ -0,0 +1,233 @@
# 📦 Installation
EverShelf runs on any server with PHP 8.0+ and SQLite. Docker is the recommended approach for the fastest setup.
---
## Prerequisites
| Requirement | Minimum | Notes |
|-------------|---------|-------|
| PHP | 8.0+ | Extensions: `pdo_sqlite`, `curl`, `mbstring`, `json` |
| Web server | Apache 2.4+ or Nginx | Apache `.htaccess` included |
| SQLite | 3.x | Bundled with PHP on most distros |
| HTTPS | Recommended | Required for camera access on mobile browsers |
| RAM | 256 MB | 512 MB+ recommended if using AI features |
---
## Option A: Docker (recommended)
The fastest way to get started.
```bash
# 1. Clone the repository
git clone https://github.com/dadaloop82/EverShelf.git
cd EverShelf
# 2. Create your configuration
cp .env.example .env
nano .env # set GEMINI_API_KEY and other options
# 3. Start
docker compose up -d
# 4. Open in browser
# → http://localhost:8080
```
The Docker image:
- Uses PHP-Apache on Debian Bookworm slim
- Auto-creates the `data/` directory with correct permissions
- Exposes port `8080` by default (configurable in `docker-compose.yml`)
- Persists data in a named Docker volume
### Changing the port
Edit `docker-compose.yml`:
```yaml
ports:
- "8080:80" # change 8080 to your desired host port
```
### Using HTTPS with Docker
Add a reverse proxy (e.g. Traefik, Caddy, or Nginx Proxy Manager) in front of the container for automatic TLS.
---
## Option B: Manual (Apache)
```bash
# 1. Clone into your web root
git clone https://github.com/dadaloop82/EverShelf.git /var/www/html/dispensa
cd /var/www/html/dispensa
# 2. Create configuration
cp .env.example .env
nano .env
# 3. Set permissions on the data directory
chmod 755 data/
chown -R www-data:www-data data/
```
Make sure `mod_rewrite` is enabled:
```bash
sudo a2enmod rewrite
sudo systemctl restart apache2
```
Apache virtual host (or add to `.htaccess` which is already included):
```apache
<VirtualHost *:443>
ServerName evershelf.local
DocumentRoot /var/www/html/dispensa
<Directory /var/www/html/dispensa>
AllowOverride All
Require all granted
</Directory>
# Hide sensitive paths
<LocationMatch "^/(data|\.env|backup\.sh)">
Require all denied
</LocationMatch>
SSLEngine on
SSLCertificateFile /etc/ssl/certs/evershelf.crt
SSLCertificateKeyFile /etc/ssl/private/evershelf.key
</VirtualHost>
```
---
## Option C: Manual (Nginx)
```nginx
server {
listen 443 ssl;
server_name evershelf.local;
root /var/www/html/dispensa;
index index.html;
ssl_certificate /etc/ssl/certs/evershelf.crt;
ssl_certificate_key /etc/ssl/private/evershelf.key;
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;
}
}
# Block sensitive files
location ~ /\.env { deny all; }
location ~ /data/ { deny all; }
location ~ /backup\.sh { deny all; }
location / {
try_files $uri $uri/ /index.html;
}
}
```
---
## HTTPS Setup
Camera and microphone access (barcode scanning, voice) **require HTTPS** on all modern mobile browsers.
### Self-signed certificate (local network)
```bash
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/ssl/private/evershelf.key \
-out /etc/ssl/certs/evershelf.crt \
-subj "/CN=evershelf.local" \
-addext "subjectAltName=IP:192.168.1.100,DNS:evershelf.local"
```
Android will show a certificate warning — tap "Advanced → Proceed" once. The kiosk app accepts self-signed certificates automatically.
### Let's Encrypt (public server)
```bash
sudo apt install certbot python3-certbot-apache
sudo certbot --apache -d evershelf.yourdomain.com
```
### Caddy (automatic TLS)
```
evershelf.yourdomain.com {
root * /var/www/html/dispensa
php_fastcgi unix//run/php/php8.2-fpm.sock
file_server
respond /data/* 403
respond /.env 403
}
```
---
## Cron Job (optional)
For smart shopping predictions to stay up to date:
```bash
# Edit crontab
crontab -e
# Add (runs every 5 minutes)
*/5 * * * * php /var/www/html/dispensa/api/cron_smart_shopping.php >> /var/www/html/dispensa/data/cron.log 2>&1
```
---
## Backup (optional)
```bash
# Edit crontab
crontab -e
# Daily backup at 3 AM
0 3 * * * /var/www/html/dispensa/backup.sh
```
The `backup.sh` script copies `data/evershelf.db` to `data/backups/` with a timestamp.
---
## Updating
```bash
cd /var/www/html/dispensa
git pull origin main
# Database migrations run automatically on next page load
```
With Docker:
```bash
docker compose pull
docker compose up -d
```
---
## Post-installation
Once the app is running, open it in your browser and:
1. Go to **Settings** (⚙️ icon in the header)
2. Enter your **Gemini API key** (get one free at [aistudio.google.com](https://aistudio.google.com/app/apikey))
3. Optionally configure Bring!, TTS, and scale settings
4. Add your first product via the button or barcode scan
See [Configuration](Configuration) for the full list of settings.
+155
View File
@@ -0,0 +1,155 @@
# ⚖️ Scale Gateway
The EverShelf Scale Gateway is an Android app that bridges a Bluetooth LE smart scale to EverShelf, enabling automatic weight-based inventory tracking.
---
## How it works
```
Smart Scale
│ (Bluetooth LE)
Android device (Scale Gateway app)
│ (WebSocket — ws://127.0.0.1:8765)
EverShelf Server (scale_relay.php — SSE relay)
│ (Server-Sent Events)
EverShelf Web App (auto-fills weight in add/use forms)
```
The Gateway runs a local WebSocket server on port **8765**. The EverShelf server proxies scale readings to the browser via SSE, avoiding HTTPS→WS mixed-content issues.
---
## Download
**[⬇ Download latest APK](https://github.com/dadaloop82/EverShelf/releases/latest/download/evershelf-scale-gateway.apk)**
> Current version: **v2.1.0** — requires Android 7.0+
---
## Supported Scales
| Protocol | BLE Service | Notes |
|----------|------------|-------|
| Bluetooth SIG Weight Scale | `0x181D` / char `0x2A9D` | Most compatible |
| Bluetooth SIG Body Composition | `0x181B` / char `0x2A9C` | Weight + body fat |
| Generic fallback | Any notifiable characteristic | Auto-heuristic for 100+ models |
**Verified compatible models:**
- Xiaomi Mi Body Composition Scale 2
- Renpho Smart Body Fat Scale
- Any scale supported by [openScale](https://github.com/oliexdev/openScale/wiki/Supported-scales)
---
## Setup
### 1. Install
Download and install the APK. You may need to enable "Install from unknown sources" in Android Settings.
> **Kiosk users:** the Setup Wizard installs the gateway automatically in Step 5.
### 2. Launch the app
The gateway server starts immediately. Note the **Gateway URL** shown (e.g. `ws://192.168.1.100:8765`).
### 3. Configure EverShelf
In EverShelf **Settings → Scale**:
- Enable scale integration
- Enter the Gateway URL (or let auto-discovery find it)
> **Kiosk users:** this is done automatically during setup.
### 4. Connect your scale
Tap **"Cerca Bilance Bluetooth"** (Find Bluetooth Scales). Make sure your scale is powered on. Tap it in the list to pair and connect.
---
## Using the Scale in EverShelf
When scale integration is enabled:
1. Open the **Add** or **Use** form for any product with unit `g` or `ml`
2. A **"⚖️ Leggi bilancia"** button appears
3. Tap it — a live weight display appears with a stability indicator
4. Step on or place the product on the scale
5. When the reading stabilizes, a **5-second countdown** starts
6. The weight auto-fills the quantity field and the form submits
### Thresholds and de-duplication
- **10g threshold** — readings that haven't changed enough between products are ignored to prevent stale readings
- **12-second server-side dedup** — a second scale-triggered deduction of the same product within 12 seconds is rejected (guards against BLE multi-fire)
- **ml conversion** — when the product unit is `ml`, the weight in grams is accepted and a hint is shown: "weight in grams → will be converted to ml"
---
## Scale Status Indicator
The header of the EverShelf web app shows a real-time scale status icon (⚖️):
| State | Meaning |
|-------|---------|
| ⚖️ green | Connected and ready |
| ⚖️ amber | Searching / reconnecting |
| ⚖️ grey | Disconnected |
| ⚖️ red | Error |
---
## Update Notifications
Every 6 hours the gateway app checks GitHub releases. If a newer version is available, a banner appears with a one-tap download and install.
---
## Troubleshooting
### Scale not appearing in the Bluetooth list
- Make sure BLE is enabled on the Android device
- Step on/shake the scale to wake it up (most scales enter sleep mode quickly)
- Some scales only advertise while someone stands on them
### Weight not appearing in EverShelf
- Confirm the Gateway URL in EverShelf Settings matches the URL shown in the gateway app
- Check that the Android device and the EverShelf server are on the same network
- Tap "Disconnetti / Riconnetti" in the gateway app to refresh the WebSocket connection
### "Mixed content" error in browser
- Make sure you are accessing EverShelf over HTTPS (not plain HTTP)
- The SSE relay (`scale_relay.php`) handles the HTTP→WS bridging — ensure the relay script is reachable
---
## Building from Source
```bash
cd evershelf-scale-gateway
./gradlew assembleRelease
# APK: app/build/outputs/apk/release/app-release.apk
```
Requires Android Studio or JDK 17+ with the Android SDK.
---
## BLE Protocol Details
The gateway uses the following GATT profile order:
1. **Weight Scale** (`0x181D`) — standard weight only
2. **Body Composition** (`0x181B`) — weight + additional metrics
3. **Generic fallback** — subscribes to all notifiable characteristics and applies a heuristic parser that handles byte-order variations used by the majority of consumer smart scales
Weight values are extracted in kg, converted to grams, and broadcast over WebSocket as:
```json
{ "weight_g": 1234, "stable": true, "unit": "g" }
```
+143
View File
@@ -0,0 +1,143 @@
# 🌍 Translations
EverShelf uses JSON translation files in the `translations/` folder. The app auto-detects the browser language on load and falls back to English.
---
## Currently Supported Languages
| Language | File | Status |
|----------|------|--------|
| 🇮🇹 Italian | `translations/it.json` | ✅ Complete (base language) |
| 🇬🇧 English | `translations/en.json` | ✅ Complete |
| 🇩🇪 German | `translations/de.json` | ✅ Complete |
---
## Adding a New Language
### 1. Copy the base file
```bash
cp translations/it.json translations/fr.json
```
### 2. Translate all values
Open `fr.json` in your editor and translate every **value** (leave the **keys** unchanged).
```json
{
"app": {
"name": "EverShelf",
"loading": "Chargement..." translate this
},
"nav": {
"title": "🏠 EverShelf", keep emoji, translate text
"home": "Accueil"
}
}
```
**Rules:**
- Never change the key names (left side of `:`)
- Keep `{placeholder}` tokens unchanged — they are replaced at runtime
- Example: `"toast.added": "Added {name} to {location}"` — keep `{name}` and `{location}`
- Keep HTML tags if present (rare): `<strong>`, `<br>`
- Keep emojis (they are part of the UX design)
- Plurals: some keys have `_one` / `_many` variants — translate both
### 3. Register the language in the app
Open `assets/js/app.js` and find the `SUPPORTED_LANGUAGES` constant (near the top):
```js
const SUPPORTED_LANGUAGES = ['it', 'en', 'de'];
```
Add your language code:
```js
const SUPPORTED_LANGUAGES = ['it', 'en', 'de', 'fr'];
```
### 4. Add the language to `translations/` badge list
Update the `README.md` badge:
```markdown
[![i18n](https://img.shields.io/badge/i18n-IT%20%7C%20EN%20%7C%20DE%20%7C%20FR-orange.svg)](translations/)
```
### 5. Test
Open the app with `?lang=fr` in the URL to force your language:
```
http://localhost:8080/?lang=fr
```
Check for missing keys — they will show the raw key name in the UI (e.g. `nav.title`).
### 6. Submit a PR
Open a pull request with your new `translations/fr.json` and the updated `app.js` line. See [Contributing](Contributing).
---
## Translation Key Structure
The file is a nested JSON object. Here are the main sections:
| Section | Description |
|---------|-------------|
| `app` | General app strings |
| `nav` | Navigation labels |
| `btn` | Button labels |
| `locations` | Storage location names |
| `categories` | Product category names |
| `dashboard` | Dashboard section titles |
| `inventory` | Inventory page strings |
| `use` | Use/consume form strings |
| `add` | Add product form strings |
| `scan` | Barcode scanner strings |
| `recipes` | Recipe page strings |
| `cooking` | Cooking mode strings |
| `shopping` | Shopping list strings |
| `log` | Transaction log strings |
| `settings` | Settings page strings |
| `scale` | Scale integration strings |
| `toast` | Toast notification messages |
| `error` | Error messages |
| `confirm` | Confirmation dialog strings |
---
## Updating Existing Translations
If a new feature adds keys to `it.json` (the base), you need to add the same keys to `en.json` and `de.json`.
The CI pipeline validates that all language files contain the same keys — a missing key will fail the build.
To check locally:
```bash
node -e "
const it = require('./translations/it.json');
const en = require('./translations/en.json');
// flatten and compare keys...
"
```
Or just open a PR — CI will flag any missing keys automatically.
---
## Language Detection Order
1. `?lang=xx` URL parameter (forces a specific language)
2. `localStorage.getItem('lang')` (last manually selected language)
3. `navigator.language` / `navigator.languages` (browser preference)
4. Fallback: `en`
Users can change the language in **Settings → Language**.