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:
@@ -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)**
|
||||||
|
·
|
||||||
|
[🌐 Project Website](https://evershelfproject.dadaloop.it/)
|
||||||
|
·
|
||||||
|
[📖 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)
|
[](LICENSE)
|
||||||
[](https://www.php.net/)
|
[](https://www.php.net/)
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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 |
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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).
|
||||||
@@ -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)
|
||||||
@@ -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 2–3× 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
|
||||||
@@ -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)
|
||||||
@@ -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.
|
||||||
@@ -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" }
|
||||||
|
```
|
||||||
@@ -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
|
||||||
|
[](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**.
|
||||||
Reference in New Issue
Block a user