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:
@@ -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