Prepare for public distribution v1.0.0
- Remove all personal data from source code (HA IP, JWT tokens) - Move secrets to .env configuration (gitignored) - Create .env.example template for new installations - Add centralized env() helper, eliminate code duplication (~120 lines removed) - Add input validation on inventory operations (quantity bounds, location whitelist) - Remove sensitive credential exposure in API responses - Remove database and runtime files from Git tracking - Disable database push-to-GitHub backup (local-only backup now) - Update .gitignore for distribution - Add comprehensive README with installation guide - Add CHANGELOG.md for version tracking - Add MIT LICENSE - Add author/license headers to all source files - TTS defaults now empty (configured per-installation via .env)
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
# Dispensa Manager - Configuration
|
||||||
|
# Copy this file to .env and fill in your values
|
||||||
|
# cp .env.example .env
|
||||||
|
|
||||||
|
# Google Gemini AI API Key (required for AI features)
|
||||||
|
# Get one at: https://aistudio.google.com/app/apikey
|
||||||
|
GEMINI_API_KEY=
|
||||||
|
|
||||||
|
# Bring! Shopping List credentials (optional)
|
||||||
|
# Sign up at: https://www.getbring.com/
|
||||||
|
BRING_EMAIL=
|
||||||
|
BRING_PASSWORD=
|
||||||
|
|
||||||
|
# TTS (Text-to-Speech) for cooking mode voice guidance (optional)
|
||||||
|
# Works with Home Assistant, or any HTTP endpoint that accepts text
|
||||||
|
TTS_URL=
|
||||||
|
TTS_TOKEN=
|
||||||
|
TTS_METHOD=POST
|
||||||
|
TTS_AUTH_TYPE=bearer
|
||||||
|
TTS_CONTENT_TYPE=application/json
|
||||||
|
TTS_PAYLOAD_KEY=message
|
||||||
|
TTS_ENABLED=false
|
||||||
+14
-11
@@ -1,28 +1,31 @@
|
|||||||
# Environment variables (secrets)
|
# Environment variables (secrets — copy .env.example to .env)
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# SQLite WAL/SHM temp files
|
# Data directory (user-specific runtime data)
|
||||||
|
data/dispensa.db
|
||||||
data/*.db-wal
|
data/*.db-wal
|
||||||
data/*.db-shm
|
data/*.db-shm
|
||||||
|
data/backups/
|
||||||
# Bring! auth token cache
|
data/cron.log
|
||||||
|
data/smart_shopping_cache.json
|
||||||
data/bring_token.json
|
data/bring_token.json
|
||||||
data/bring_catalog.json
|
data/bring_catalog.json
|
||||||
|
|
||||||
# DupliClick token cache
|
|
||||||
data/dupliclick_token.json
|
data/dupliclick_token.json
|
||||||
|
|
||||||
# Client debug log (runtime only)
|
|
||||||
data/client_debug.log
|
data/client_debug.log
|
||||||
|
|
||||||
# SSL CA cert (local only)
|
# SSL certificates (local only)
|
||||||
ca.crt
|
data/*.crt
|
||||||
|
data/*.pem
|
||||||
|
*.crt
|
||||||
|
*.pem
|
||||||
|
|
||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Editor
|
# Editor / IDE
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to Dispensa Manager will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-04-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Complete pantry inventory management (Pantry, Fridge, Freezer, Other)
|
||||||
|
- Barcode scanning with QuaggaJS
|
||||||
|
- Open Food Facts barcode lookup
|
||||||
|
- Google Gemini AI integration (product identification, expiry reading, recipes, chat)
|
||||||
|
- Bring! shopping list integration
|
||||||
|
- Smart shopping predictions with cron-based caching
|
||||||
|
- Cooking mode with step-by-step guidance and TTS support
|
||||||
|
- Opened product tracking with reduced shelf-life calculation
|
||||||
|
- Vacuum-sealed product support with extended expiry
|
||||||
|
- Waste vs. consumption tracking (30-day chart)
|
||||||
|
- Expired product safety assessment by category
|
||||||
|
- Weekly meal plan configuration
|
||||||
|
- DupliClick online grocery ordering integration
|
||||||
|
- PWA support (installable, mobile-first)
|
||||||
|
- Local database backup script
|
||||||
|
- Multi-device settings sync via SQLite
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Centralized `.env` configuration (secrets never in code)
|
||||||
|
- Removed all hardcoded credentials and personal data
|
||||||
|
- Input validation on inventory operations
|
||||||
|
- Parameterized SQL queries throughout
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Stimpfl Daniel
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
# 🏠 Dispensa Manager
|
||||||
|
|
||||||
|
> **Self-hosted pantry management system** — Track your food inventory, scan barcodes, get AI-powered recipe suggestions, and reduce waste.
|
||||||
|
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://www.php.net/)
|
||||||
|
[](https://www.sqlite.org/)
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="assets/img/screenshot-dashboard.png" alt="Dashboard Screenshot" width="320" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
### 📦 Inventory Management
|
||||||
|
- **Barcode scanning** — Scan products with your phone camera using QuaggaJS
|
||||||
|
- **AI identification** — Take a photo and let Google Gemini identify the product
|
||||||
|
- **Smart locations** — Track items across Pantry, Fridge, Freezer, and custom locations
|
||||||
|
- **Expiry tracking** — Automatic shelf-life estimation based on product type and storage
|
||||||
|
- **Opened product tracking** — Reduced shelf-life calculation when packages are opened
|
||||||
|
- **Vacuum-sealed support** — Extended expiry dates for vacuum-sealed items
|
||||||
|
|
||||||
|
### 🤖 AI-Powered (Google Gemini)
|
||||||
|
- **Expiry date reading** — Photograph a label and extract the expiry date automatically
|
||||||
|
- **Product identification** — Point your camera at any product for instant recognition
|
||||||
|
- **Recipe generation** — Get personalized recipes based on what's in your pantry
|
||||||
|
- **Smart chat assistant** — Ask questions about your inventory, get cooking tips
|
||||||
|
- **Shopping suggestions** — AI-powered purchase recommendations
|
||||||
|
|
||||||
|
### 🛒 Shopping List
|
||||||
|
- **Bring! integration** — Sync with the [Bring!](https://www.getbring.com/) shopping list app
|
||||||
|
- **Smart predictions** — Know what you'll need before you run out
|
||||||
|
- **Auto-remove on scan** — Products are removed from the shopping list when scanned in
|
||||||
|
- **DupliClick integration** — Online grocery ordering (Gruppo Poli)
|
||||||
|
|
||||||
|
### 🍳 Cooking Mode
|
||||||
|
- **Step-by-step guidance** — Follow recipes with a hands-free cooking interface
|
||||||
|
- **Text-to-Speech** — Voice readout of recipe steps (configurable TTS endpoint)
|
||||||
|
- **Built-in timer** — Automatic timer suggestions based on recipe instructions
|
||||||
|
- **Ingredient tracking** — Mark ingredients as used during cooking
|
||||||
|
|
||||||
|
### 📊 Dashboard
|
||||||
|
- **Waste tracking** — Monitor consumed vs. wasted products over 30 days
|
||||||
|
- **Expiry alerts** — Visual warnings for expired and soon-to-expire items
|
||||||
|
- **Safety ratings** — Smart assessment of expired product safety (by category)
|
||||||
|
- **Quick recipe bar** — One-tap recipe suggestion using expiring products
|
||||||
|
|
||||||
|
### 📱 Progressive Web App
|
||||||
|
- **Mobile-first design** — Optimized for phones, works on tablets and desktop
|
||||||
|
- **Installable** — Add to home screen for a native app experience
|
||||||
|
- **Multi-device** — Settings and data sync across devices on the same server
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- **Web server** with PHP 8.0+ (Apache or Nginx)
|
||||||
|
- **PHP extensions**: `pdo_sqlite`, `curl`, `mbstring`, `json`
|
||||||
|
- **HTTPS** recommended (required for camera access on mobile)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone the repository
|
||||||
|
git clone https://github.com/dadaloop82/dispensa.git
|
||||||
|
cd dispensa
|
||||||
|
|
||||||
|
# 2. Create configuration file
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# 3. Set permissions
|
||||||
|
chmod 755 data/
|
||||||
|
chmod 664 data/.gitkeep
|
||||||
|
chown -R www-data:www-data data/
|
||||||
|
|
||||||
|
# 4. Edit your configuration
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration (.env)
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# Required for AI features (get a key at https://aistudio.google.com/app/apikey)
|
||||||
|
GEMINI_API_KEY=your_api_key_here
|
||||||
|
|
||||||
|
# Optional: Bring! shopping list integration
|
||||||
|
BRING_EMAIL=your_email@example.com
|
||||||
|
BRING_PASSWORD=your_password
|
||||||
|
|
||||||
|
# Optional: Text-to-Speech for cooking mode
|
||||||
|
TTS_URL=http://your-home-assistant:8123/api/events/tts_speak
|
||||||
|
TTS_TOKEN=your_long_lived_token
|
||||||
|
TTS_ENABLED=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web Server Configuration
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Apache (.htaccess)</strong></summary>
|
||||||
|
|
||||||
|
The app works out of the box with Apache if placed in the web root or a subdirectory. Make sure `mod_rewrite` is enabled and `AllowOverride All` is set.
|
||||||
|
|
||||||
|
```apache
|
||||||
|
<Directory /var/www/html/dispensa>
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Nginx</strong></summary>
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your-server.local;
|
||||||
|
root /var/www/html/dispensa;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deny access to sensitive files
|
||||||
|
location ~ /\.env { deny all; }
|
||||||
|
location ~ /data/ { deny all; }
|
||||||
|
location ~ /backup\.sh { deny all; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### HTTPS Setup (Recommended)
|
||||||
|
|
||||||
|
Camera access requires HTTPS on most mobile browsers. Options:
|
||||||
|
- **Let's Encrypt** with Certbot (for public-facing servers)
|
||||||
|
- **Self-signed certificate** (for local network only)
|
||||||
|
- **Reverse proxy** (e.g., Caddy, Traefik) with automatic TLS
|
||||||
|
|
||||||
|
### Cron Job (Optional)
|
||||||
|
|
||||||
|
Set up a cron job for smart shopping predictions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run every 5 minutes
|
||||||
|
*/5 * * * * php /path/to/dispensa/api/cron_smart_shopping.php >> /path/to/dispensa/data/cron.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup (Optional)
|
||||||
|
|
||||||
|
The included `backup.sh` creates local daily backups of your database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run daily at 3 AM
|
||||||
|
0 3 * * * /path/to/dispensa/backup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
dispensa/
|
||||||
|
├── index.html # Single-page application (SPA)
|
||||||
|
├── manifest.json # PWA manifest
|
||||||
|
├── .env.example # Configuration template
|
||||||
|
├── backup.sh # Local database backup script
|
||||||
|
├── LICENSE # MIT License
|
||||||
|
│
|
||||||
|
├── api/
|
||||||
|
│ ├── index.php # Main API router (all endpoints)
|
||||||
|
│ ├── database.php # SQLite schema, migrations, helpers
|
||||||
|
│ └── cron_smart_shopping.php # Background job for predictions
|
||||||
|
│
|
||||||
|
├── assets/
|
||||||
|
│ ├── css/style.css # All application styles
|
||||||
|
│ ├── js/app.js # All application logic
|
||||||
|
│ └── img/ # Static images
|
||||||
|
│
|
||||||
|
└── data/ # Runtime data (gitignored)
|
||||||
|
├── dispensa.db # SQLite database (auto-created)
|
||||||
|
├── backups/ # Local DB backups
|
||||||
|
└── *.json # Token/cache files
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
| Category | Action | Method | Description |
|
||||||
|
|----------|--------|--------|-------------|
|
||||||
|
| **Products** | `search_barcode` | GET | Find product by barcode |
|
||||||
|
| | `lookup_barcode` | GET | Look up barcode on Open Food Facts |
|
||||||
|
| | `product_save` | POST | Create or update a product |
|
||||||
|
| | `products_list` | GET | List all products |
|
||||||
|
| **Inventory** | `inventory_list` | GET | List inventory items |
|
||||||
|
| | `inventory_add` | POST | Add product to inventory |
|
||||||
|
| | `inventory_use` | POST | Use/consume from inventory |
|
||||||
|
| | `inventory_summary` | GET | Count by location |
|
||||||
|
| **AI** | `gemini_identify` | POST | Identify product from photo |
|
||||||
|
| | `gemini_expiry` | POST | Read expiry date from photo |
|
||||||
|
| | `gemini_chat` | POST | Chat with AI assistant |
|
||||||
|
| | `generate_recipe` | POST | Generate recipe from inventory |
|
||||||
|
| **Shopping** | `bring_list` | GET | Get Bring! shopping list |
|
||||||
|
| | `bring_add` | POST | Add items to Bring! |
|
||||||
|
| | `smart_shopping` | GET | Smart shopping predictions |
|
||||||
|
| **Settings** | `get_settings` | GET | Get server configuration |
|
||||||
|
| | `save_settings` | POST | Update server configuration |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Security Notes
|
||||||
|
|
||||||
|
- **Credentials** are stored in `.env` (server-side, never committed to Git)
|
||||||
|
- **Database** stays local — never pushed to remote repositories
|
||||||
|
- **API keys** are passed server-side only — never exposed to the browser
|
||||||
|
- The API uses **parameterized SQL queries** (PDO prepared statements) against injection
|
||||||
|
- **Input validation** on all inventory operations (quantity bounds, location whitelist)
|
||||||
|
- Consider adding **authentication** if the server is accessible from the internet
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run PHP's built-in server for local development
|
||||||
|
php -S localhost:8080 -t /path/to/dispensa
|
||||||
|
|
||||||
|
# Check PHP syntax
|
||||||
|
php -l api/index.php
|
||||||
|
php -l api/database.php
|
||||||
|
```
|
||||||
|
|
||||||
|
The application uses no build tools — edit files directly and refresh.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Roadmap
|
||||||
|
|
||||||
|
- [ ] Multi-language support (i18n)
|
||||||
|
- [ ] User authentication / multi-user support
|
||||||
|
- [ ] Docker container for easy deployment
|
||||||
|
- [ ] REST API documentation (OpenAPI/Swagger)
|
||||||
|
- [ ] Offline mode with service worker
|
||||||
|
- [ ] Export/import inventory data
|
||||||
|
- [ ] Notification system (Telegram, email) for expiring products
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please:
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch (`git checkout -b feature/my-feature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add my feature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/my-feature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the **MIT License** — see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👨💻 Author
|
||||||
|
|
||||||
|
**Stimpfl Daniel** — [dadaloop82@gmail.com](mailto:dadaloop82@gmail.com)
|
||||||
|
|
||||||
|
- GitHub: [@dadaloop82](https://github.com/dadaloop82)
|
||||||
+5
-1
@@ -1,6 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Database initialization and connection for Dispensa Manager
|
* Dispensa Manager - Database initialization, schema, and migrations.
|
||||||
|
* Uses SQLite with WAL journal mode for concurrent read/write performance.
|
||||||
|
*
|
||||||
|
* @author Stimpfl Daniel <dadaloop82@gmail.com>
|
||||||
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
define('DB_PATH', __DIR__ . '/../data/dispensa.db');
|
define('DB_PATH', __DIR__ . '/../data/dispensa.db');
|
||||||
|
|||||||
+70
-120
@@ -1,12 +1,44 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Dispensa Manager - Main API Router
|
* Dispensa Manager - Main API Router
|
||||||
* Handles all CRUD operations for products and inventory
|
* Handles all CRUD operations for products, inventory, shopping lists,
|
||||||
|
* AI-powered features (Gemini), and third-party integrations (Bring!, DupliClick).
|
||||||
|
*
|
||||||
|
* @author Stimpfl Daniel <dadaloop82@gmail.com>
|
||||||
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// database.php must always be loaded (used both by HTTP router and cron)
|
// database.php must always be loaded (used both by HTTP router and cron)
|
||||||
require_once __DIR__ . '/database.php';
|
require_once __DIR__ . '/database.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load environment variables from .env file.
|
||||||
|
* Returns associative array of key => value pairs.
|
||||||
|
*/
|
||||||
|
function loadEnv(): array {
|
||||||
|
static $cache = null;
|
||||||
|
if ($cache !== null) return $cache;
|
||||||
|
$envFile = __DIR__ . '/../.env';
|
||||||
|
$cache = [];
|
||||||
|
if (file_exists($envFile)) {
|
||||||
|
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (strpos($line, '#') === 0 || strpos($line, '=') === false) continue;
|
||||||
|
list($key, $val) = explode('=', $line, 2);
|
||||||
|
$cache[trim($key)] = trim($val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single environment variable, with optional default.
|
||||||
|
*/
|
||||||
|
function env(string $key, string $default = ''): string {
|
||||||
|
$vars = loadEnv();
|
||||||
|
return $vars[$key] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
// When included by the cron script, skip HTTP headers and routing entirely
|
// When included by the cron script, skip HTTP headers and routing entirely
|
||||||
if (!defined('CRON_MODE')) {
|
if (!defined('CRON_MODE')) {
|
||||||
|
|
||||||
@@ -586,8 +618,8 @@ function listInventory(PDO $db): void {
|
|||||||
|
|
||||||
function addToInventory(PDO $db): void {
|
function addToInventory(PDO $db): void {
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
$productId = $input['product_id'] ?? 0;
|
$productId = (int)($input['product_id'] ?? 0);
|
||||||
$quantity = $input['quantity'] ?? 1;
|
$quantity = (float)($input['quantity'] ?? 1);
|
||||||
$location = $input['location'] ?? 'dispensa';
|
$location = $input['location'] ?? 'dispensa';
|
||||||
$expiry = $input['expiry_date'] ?? null;
|
$expiry = $input['expiry_date'] ?? null;
|
||||||
$unit = $input['unit'] ?? null;
|
$unit = $input['unit'] ?? null;
|
||||||
@@ -598,6 +630,21 @@ function addToInventory(PDO $db): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate quantity bounds
|
||||||
|
if ($quantity <= 0 || $quantity > 100000) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Invalid quantity']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate location
|
||||||
|
$validLocations = ['dispensa', 'frigo', 'freezer', 'altro'];
|
||||||
|
if (!in_array($location, $validLocations)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Invalid location']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If a different unit was specified, update the product's unit
|
// If a different unit was specified, update the product's unit
|
||||||
if ($unit) {
|
if ($unit) {
|
||||||
$stmt = $db->prepare("UPDATE products SET unit = ?, default_quantity = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?");
|
$stmt = $db->prepare("UPDATE products SET unit = ?, default_quantity = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?");
|
||||||
@@ -1157,47 +1204,29 @@ function getStats(PDO $db): void {
|
|||||||
// ===== SETTINGS =====
|
// ===== SETTINGS =====
|
||||||
|
|
||||||
function getServerSettings(): void {
|
function getServerSettings(): void {
|
||||||
$envFile = __DIR__ . '/../.env';
|
// Return values for client — passwords are never exposed
|
||||||
$envVars = [];
|
$geminiKey = env('GEMINI_API_KEY');
|
||||||
if (file_exists($envFile)) {
|
$bringEmail = env('BRING_EMAIL');
|
||||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES);
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
if (strpos($line, '#') === 0 || strpos($line, '=') === false) continue;
|
|
||||||
list($key, $val) = explode('=', $line, 2);
|
|
||||||
$envVars[trim($key)] = trim($val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return masked versions for security
|
|
||||||
$geminiKey = $envVars['GEMINI_API_KEY'] ?? '';
|
|
||||||
$bringEmail = $envVars['BRING_EMAIL'] ?? '';
|
|
||||||
$bringPassword = $envVars['BRING_PASSWORD'] ?? '';
|
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'gemini_key' => $geminiKey,
|
'gemini_key' => $geminiKey,
|
||||||
'gemini_key_set' => !empty($geminiKey),
|
'gemini_key_set' => !empty($geminiKey),
|
||||||
'bring_email' => $bringEmail,
|
'bring_email' => $bringEmail,
|
||||||
'bring_password_set' => !empty($bringPassword)
|
'bring_password_set' => !empty(env('BRING_PASSWORD')),
|
||||||
|
'tts_url' => env('TTS_URL'),
|
||||||
|
'tts_token' => env('TTS_TOKEN'),
|
||||||
|
'tts_method' => env('TTS_METHOD', 'POST'),
|
||||||
|
'tts_auth_type' => env('TTS_AUTH_TYPE', 'bearer'),
|
||||||
|
'tts_content_type' => env('TTS_CONTENT_TYPE', 'application/json'),
|
||||||
|
'tts_payload_key' => env('TTS_PAYLOAD_KEY', 'message'),
|
||||||
|
'tts_enabled' => env('TTS_ENABLED', 'false') === 'true',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSettings(): void {
|
function saveSettings(): void {
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
$envFile = __DIR__ . '/../.env';
|
$envFile = __DIR__ . '/../.env';
|
||||||
|
$envVars = loadEnv();
|
||||||
// Read existing .env content
|
|
||||||
$envContent = '';
|
|
||||||
$envVars = [];
|
|
||||||
if (file_exists($envFile)) {
|
|
||||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES);
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
if (strpos($line, '#') === 0 || strpos($line, '=') === false) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
list($key, $val) = explode('=', $line, 2);
|
|
||||||
$envVars[trim($key)] = trim($val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update values from input — only overwrite if new value is non-empty
|
// Update values from input — only overwrite if new value is non-empty
|
||||||
if (!empty($input['gemini_key'])) {
|
if (!empty($input['gemini_key'])) {
|
||||||
@@ -1227,22 +1256,7 @@ function saveSettings(): void {
|
|||||||
// ===== GEMINI AI FUNCTIONS =====
|
// ===== GEMINI AI FUNCTIONS =====
|
||||||
|
|
||||||
function geminiReadExpiry(): void {
|
function geminiReadExpiry(): void {
|
||||||
// Load API key from .env
|
$apiKey = env('GEMINI_API_KEY');
|
||||||
$envFile = __DIR__ . '/../.env';
|
|
||||||
$apiKey = '';
|
|
||||||
if (file_exists($envFile)) {
|
|
||||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
if (strpos($line, '#') === 0) continue;
|
|
||||||
if (strpos($line, '=') !== false) {
|
|
||||||
list($key, $val) = explode('=', $line, 2);
|
|
||||||
if (trim($key) === 'GEMINI_API_KEY') {
|
|
||||||
$apiKey = trim($val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($apiKey)) {
|
if (empty($apiKey)) {
|
||||||
echo json_encode(['success' => false, 'error' => 'no_api_key']);
|
echo json_encode(['success' => false, 'error' => 'no_api_key']);
|
||||||
return;
|
return;
|
||||||
@@ -1330,22 +1344,7 @@ function geminiReadExpiry(): void {
|
|||||||
|
|
||||||
// ===== GEMINI CHAT =====
|
// ===== GEMINI CHAT =====
|
||||||
function geminiChat(PDO $db): void {
|
function geminiChat(PDO $db): void {
|
||||||
// Load API key
|
$apiKey = env('GEMINI_API_KEY');
|
||||||
$envFile = __DIR__ . '/../.env';
|
|
||||||
$apiKey = '';
|
|
||||||
if (file_exists($envFile)) {
|
|
||||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
if (strpos($line, '#') === 0) continue;
|
|
||||||
if (strpos($line, '=') !== false) {
|
|
||||||
list($key, $val) = explode('=', $line, 2);
|
|
||||||
if (trim($key) === 'GEMINI_API_KEY') {
|
|
||||||
$apiKey = trim($val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($apiKey)) {
|
if (empty($apiKey)) {
|
||||||
echo json_encode(['success' => false, 'error' => 'no_api_key']);
|
echo json_encode(['success' => false, 'error' => 'no_api_key']);
|
||||||
return;
|
return;
|
||||||
@@ -1497,22 +1496,7 @@ PROMPT;
|
|||||||
|
|
||||||
// ===== RECIPE GENERATION WITH GEMINI =====
|
// ===== RECIPE GENERATION WITH GEMINI =====
|
||||||
function generateRecipe(PDO $db): void {
|
function generateRecipe(PDO $db): void {
|
||||||
// Load API key from .env
|
$apiKey = env('GEMINI_API_KEY');
|
||||||
$envFile = __DIR__ . '/../.env';
|
|
||||||
$apiKey = '';
|
|
||||||
if (file_exists($envFile)) {
|
|
||||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
if (strpos($line, '#') === 0) continue;
|
|
||||||
if (strpos($line, '=') !== false) {
|
|
||||||
list($key, $val) = explode('=', $line, 2);
|
|
||||||
if (trim($key) === 'GEMINI_API_KEY') {
|
|
||||||
$apiKey = trim($val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($apiKey)) {
|
if (empty($apiKey)) {
|
||||||
echo json_encode(['success' => false, 'error' => 'no_api_key']);
|
echo json_encode(['success' => false, 'error' => 'no_api_key']);
|
||||||
return;
|
return;
|
||||||
@@ -2034,22 +2018,7 @@ PROMPT;
|
|||||||
|
|
||||||
// ===== GEMINI AI PRODUCT IDENTIFICATION =====
|
// ===== GEMINI AI PRODUCT IDENTIFICATION =====
|
||||||
function geminiIdentifyProduct(): void {
|
function geminiIdentifyProduct(): void {
|
||||||
// Load API key
|
$apiKey = env('GEMINI_API_KEY');
|
||||||
$envFile = __DIR__ . '/../.env';
|
|
||||||
$apiKey = '';
|
|
||||||
if (file_exists($envFile)) {
|
|
||||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
if (strpos($line, '#') === 0) continue;
|
|
||||||
if (strpos($line, '=') !== false) {
|
|
||||||
list($key, $val) = explode('=', $line, 2);
|
|
||||||
if (trim($key) === 'GEMINI_API_KEY') {
|
|
||||||
$apiKey = trim($val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($apiKey)) {
|
if (empty($apiKey)) {
|
||||||
echo json_encode(['success' => false, 'error' => 'no_api_key']);
|
echo json_encode(['success' => false, 'error' => 'no_api_key']);
|
||||||
return;
|
return;
|
||||||
@@ -2200,26 +2169,9 @@ function searchOpenFoodFacts(string $searchTerms, string $name, string $brand):
|
|||||||
|
|
||||||
// ===== BRING! SHOPPING LIST INTEGRATION =====
|
// ===== BRING! SHOPPING LIST INTEGRATION =====
|
||||||
|
|
||||||
function loadEnvVars(): array {
|
|
||||||
$envFile = __DIR__ . '/../.env';
|
|
||||||
$vars = [];
|
|
||||||
if (file_exists($envFile)) {
|
|
||||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
if (strpos($line, '#') === 0) continue;
|
|
||||||
if (strpos($line, '=') !== false) {
|
|
||||||
list($key, $val) = explode('=', $line, 2);
|
|
||||||
$vars[trim($key)] = trim($val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $vars;
|
|
||||||
}
|
|
||||||
|
|
||||||
function bringAuth(): ?array {
|
function bringAuth(): ?array {
|
||||||
$env = loadEnvVars();
|
$email = env('BRING_EMAIL');
|
||||||
$email = $env['BRING_EMAIL'] ?? '';
|
$password = env('BRING_PASSWORD');
|
||||||
$password = $env['BRING_PASSWORD'] ?? '';
|
|
||||||
|
|
||||||
if (empty($email) || empty($password)) {
|
if (empty($email) || empty($password)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -2955,8 +2907,7 @@ function smartShopping(PDO $db): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bringSuggestItems(PDO $db): void {
|
function bringSuggestItems(PDO $db): void {
|
||||||
$env = loadEnvVars();
|
$apiKey = env('GEMINI_API_KEY');
|
||||||
$apiKey = $env['GEMINI_API_KEY'] ?? '';
|
|
||||||
|
|
||||||
if (empty($apiKey)) {
|
if (empty($apiKey)) {
|
||||||
echo json_encode(['success' => false, 'error' => 'API Key Gemini non configurata']);
|
echo json_encode(['success' => false, 'error' => 'API Key Gemini non configurata']);
|
||||||
@@ -3441,8 +3392,7 @@ function dupliclickExtractSpecKeywords(string $spec): string {
|
|||||||
* Use Gemini AI to pick the best product from search results
|
* Use Gemini AI to pick the best product from search results
|
||||||
*/
|
*/
|
||||||
function aiSelectBestProduct(string $itemName, string $spec, array $products, string $customPrompt = ''): ?array {
|
function aiSelectBestProduct(string $itemName, string $spec, array $products, string $customPrompt = ''): ?array {
|
||||||
$env = loadEnvVars();
|
$apiKey = env('GEMINI_API_KEY');
|
||||||
$apiKey = $env['GEMINI_API_KEY'] ?? '';
|
|
||||||
if (empty($apiKey)) return null;
|
if (empty($apiKey)) return null;
|
||||||
|
|
||||||
$defaultPrompt = "Sei un assistente per la spesa online. Ti viene dato il nome di un prodotto che l'utente vuole comprare (con eventuale descrizione tra parentesi) e una lista di prodotti trovati nel catalogo del supermercato.
|
$defaultPrompt = "Sei un assistente per la spesa online. Ti viene dato il nome di un prodotto che l'utente vuole comprare (con eventuale descrizione tra parentesi) e una lista di prodotti trovati nel catalogo del supermercato.
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Dispensa Manager - UI Styles
|
||||||
|
* Mobile-first PWA design with CSS custom properties.
|
||||||
|
*
|
||||||
|
* @author Stimpfl Daniel <dadaloop82@gmail.com>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
/* ===== CSS VARIABLES & RESET ===== */
|
/* ===== CSS VARIABLES & RESET ===== */
|
||||||
:root {
|
:root {
|
||||||
--primary: #2d5016;
|
--primary: #2d5016;
|
||||||
|
|||||||
+24
-5
@@ -1,6 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Dispensa Manager - Main Application JS
|
* Dispensa Manager - Main Application JS
|
||||||
* Complete pantry management with barcode scanning and AI identification
|
* Complete pantry management with barcode scanning, AI identification,
|
||||||
|
* Bring! shopping list integration, recipe generation, and TTS cooking mode.
|
||||||
|
*
|
||||||
|
* @author Stimpfl Daniel <dadaloop82@gmail.com>
|
||||||
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ===== REMOTE LOGGING =====
|
// ===== REMOTE LOGGING =====
|
||||||
@@ -722,13 +726,13 @@ async function loadSettingsUI() {
|
|||||||
}
|
}
|
||||||
// TTS settings — init defaults on first load
|
// TTS settings — init defaults on first load
|
||||||
if (!s._tts_initialized) {
|
if (!s._tts_initialized) {
|
||||||
s.tts_url = s.tts_url || 'http://192.168.1.133:8123/api/events/noemi_speak';
|
s.tts_url = s.tts_url || '';
|
||||||
s.tts_token = s.tts_token || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI2OGQ3Njk1N2E0MjY0ZTBjOWQ4YjczZDY4ZDVmMWJlZCIsImlhdCI6MTc2MDI1NjIxNSwiZXhwIjoyMDc1NjE2MjE1fQ.X5UyMMPd7wTA6Gh11Nzg7Ox-enlDDom_lJIAJruUtcE';
|
s.tts_token = s.tts_token || '';
|
||||||
s.tts_payload_key = s.tts_payload_key || 'message';
|
s.tts_payload_key = s.tts_payload_key || 'message';
|
||||||
s.tts_method = s.tts_method || 'POST';
|
s.tts_method = s.tts_method || 'POST';
|
||||||
s.tts_auth_type = s.tts_auth_type || 'bearer';
|
s.tts_auth_type = s.tts_auth_type || 'bearer';
|
||||||
s.tts_content_type = s.tts_content_type || 'application/json';
|
s.tts_content_type = s.tts_content_type || 'application/json';
|
||||||
s.tts_enabled = s.tts_enabled !== undefined ? s.tts_enabled : true;
|
s.tts_enabled = s.tts_enabled !== undefined ? s.tts_enabled : false;
|
||||||
s._tts_initialized = true;
|
s._tts_initialized = true;
|
||||||
saveSettingsToStorage(s);
|
saveSettingsToStorage(s);
|
||||||
}
|
}
|
||||||
@@ -762,6 +766,21 @@ async function loadSettingsUI() {
|
|||||||
if (!s.bring_email && serverSettings.bring_email) {
|
if (!s.bring_email && serverSettings.bring_email) {
|
||||||
document.getElementById('setting-bring-email').value = serverSettings.bring_email;
|
document.getElementById('setting-bring-email').value = serverSettings.bring_email;
|
||||||
}
|
}
|
||||||
|
// Load TTS defaults from server .env if not set locally
|
||||||
|
if (!s.tts_url && serverSettings.tts_url) {
|
||||||
|
s.tts_url = serverSettings.tts_url;
|
||||||
|
s.tts_token = serverSettings.tts_token || '';
|
||||||
|
s.tts_method = serverSettings.tts_method || 'POST';
|
||||||
|
s.tts_auth_type = serverSettings.tts_auth_type || 'bearer';
|
||||||
|
s.tts_content_type = serverSettings.tts_content_type || 'application/json';
|
||||||
|
s.tts_payload_key = serverSettings.tts_payload_key || 'message';
|
||||||
|
s.tts_enabled = serverSettings.tts_enabled || false;
|
||||||
|
saveSettingsToStorage(s);
|
||||||
|
// Update UI fields with server values
|
||||||
|
if (ttsUrlEl) ttsUrlEl.value = s.tts_url;
|
||||||
|
if (ttsTokenEl) ttsTokenEl.value = s.tts_token;
|
||||||
|
if (ttsEnabledEl) ttsEnabledEl.checked = s.tts_enabled;
|
||||||
|
}
|
||||||
} catch(e) { /* ignore */ }
|
} catch(e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7416,7 +7435,7 @@ function renderCookingStep() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _buildTtsRequest(text, s) {
|
function _buildTtsRequest(text, s) {
|
||||||
const url = s.tts_url || 'http://192.168.1.133:8123/api/events/noemi_speak';
|
const url = s.tts_url || '';
|
||||||
const method = s.tts_method || 'POST';
|
const method = s.tts_method || 'POST';
|
||||||
const authType = s.tts_auth_type || 'bearer';
|
const authType = s.tts_auth_type || 'bearer';
|
||||||
const token = s.tts_token || '';
|
const token = s.tts_token || '';
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Daily backup of Dispensa database to GitHub
|
# Daily backup of Dispensa database (local only)
|
||||||
# Runs via cron: commits and pushes data/dispensa.db
|
# The database is NOT pushed to remote repositories.
|
||||||
|
# Runs via cron: creates a local timestamped backup copy
|
||||||
|
#
|
||||||
|
# Example crontab entry:
|
||||||
|
# 0 3 * * * /var/www/html/dispensa/backup.sh
|
||||||
|
|
||||||
cd /var/www/html/dispensa || exit 1
|
INSTALL_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
BACKUP_DIR="${INSTALL_DIR}/data/backups"
|
||||||
|
|
||||||
# Only commit if there are actual changes
|
mkdir -p "$BACKUP_DIR"
|
||||||
if git diff --quiet data/ 2>/dev/null && git diff --cached --quiet data/ 2>/dev/null; then
|
|
||||||
# Check for untracked files in data/
|
DB_FILE="${INSTALL_DIR}/data/dispensa.db"
|
||||||
if [ -z "$(git ls-files --others --exclude-standard data/)" ]; then
|
if [ ! -f "$DB_FILE" ]; then
|
||||||
exit 0 # Nothing changed
|
exit 0
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
DATE=$(date '+%Y-%m-%d %H:%M')
|
DATE=$(date '+%Y-%m-%d_%H%M')
|
||||||
git add data/dispensa.db
|
cp "$DB_FILE" "${BACKUP_DIR}/dispensa_${DATE}.db"
|
||||||
git commit -m "📦 Backup database automatico - $DATE"
|
|
||||||
git push
|
# Keep only the last 7 backups
|
||||||
|
ls -t "${BACKUP_DIR}"/dispensa_*.db 2>/dev/null | tail -n +8 | xargs -r rm --
|
||||||
|
|||||||
-2585
File diff suppressed because it is too large
Load Diff
@@ -1,32 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
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
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"success":true,"items":[{"product_id":69,"name":"Cipolla Dorata","brand":"","category":"verdura","unit":"pz","current_qty":1,"default_qty":0,"package_unit":"","pct_left":11,"use_count":17,"buy_count":1,"daily_rate":0.36,"uses_per_month":16.6,"days_since_last_use":1,"days_left":3,"expiry_date":"2026-04-13","days_to_expiry":3,"is_opened":false,"urgency":"high","reasons":["Quasi finito (11%)","Scade tra 3gg"],"score":135,"on_bring":true,"locations":"frigo"},{"product_id":3,"name":"Cracker integrali","brand":"Barilla,Mulino Bianco","category":"en:snacks","unit":"conf","current_qty":2,"default_qty":25,"package_unit":"g","pct_left":12,"use_count":6,"buy_count":1,"daily_rate":0.46,"uses_per_month":5.9,"days_since_last_use":12,"days_left":4,"expiry_date":"2026-05-28","days_to_expiry":48,"is_opened":true,"urgency":"high","reasons":["Quasi finito (12%)"],"score":90,"on_bring":false,"locations":"dispensa"},{"product_id":47,"name":"Lenticchie","brand":"Primia","category":"en:plant-based-foods-and-beverages","unit":"conf","current_qty":2,"default_qty":400,"package_unit":"g","pct_left":1,"use_count":5,"buy_count":4,"daily_rate":29.32,"uses_per_month":4.9,"days_since_last_use":6,"days_left":0,"expiry_date":"2029-12-13","days_to_expiry":1343,"is_opened":false,"urgency":"high","reasons":["Quasi finito (1%)"],"score":90,"on_bring":false,"locations":"dispensa"},{"product_id":129,"name":"Latte di Montagna","brand":"Mila","category":"en:dairies","unit":"conf","current_qty":4,"default_qty":1000,"package_unit":"ml","pct_left":160,"use_count":17,"buy_count":6,"daily_rate":0.43,"uses_per_month":20,"days_since_last_use":1,"days_left":9,"expiry_date":"2026-04-11","days_to_expiry":1,"is_opened":true,"urgency":"medium","reasons":["Scade tra 1gg"],"score":55,"on_bring":false,"locations":"frigo"},{"product_id":122,"name":"Carote","brand":"Bell'orto","category":"en:plant-based-foods-and-beverages","unit":"g","current_qty":750,"default_qty":750,"package_unit":"","pct_left":100,"use_count":3,"buy_count":2,"daily_rate":29.42,"uses_per_month":3.5,"days_since_last_use":11,"days_left":25,"expiry_date":"2026-04-13","days_to_expiry":3,"is_opened":false,"urgency":"medium","reasons":["Scade tra 3gg"],"score":40,"on_bring":false,"locations":"dispensa"},{"product_id":188,"name":"Melone Retato","brand":"Iper Poli","category":"frutta","unit":"g","current_qty":498,"default_qty":1400,"package_unit":"","pct_left":36,"use_count":1,"buy_count":1,"daily_rate":332.81,"uses_per_month":0.5,"days_since_last_use":1,"days_left":1,"expiry_date":"2026-04-12","days_to_expiry":2,"is_opened":true,"urgency":"medium","reasons":["Scade tra 2gg"],"score":40,"on_bring":false,"locations":"frigo"},{"product_id":5,"name":"Fette biscottate Integrali","brand":"Primia","category":"en:plant-based-foods-and-beverages","unit":"conf","current_qty":2.5,"default_qty":36,"package_unit":"g","pct_left":21,"use_count":2,"buy_count":1,"daily_rate":0.24,"uses_per_month":2,"days_since_last_use":2,"days_left":10,"expiry_date":"2026-06-06","days_to_expiry":57,"is_opened":true,"urgency":"low","reasons":["Scorta bassa (21%)"],"score":30,"on_bring":false,"locations":"dispensa"},{"product_id":132,"name":"Noci sgusciate","brand":"Fruttbella","category":"conserve","unit":"g","current_qty":60,"default_qty":200,"package_unit":"","pct_left":30,"use_count":4,"buy_count":1,"daily_rate":5.49,"uses_per_month":4.7,"days_since_last_use":6,"days_left":11,"expiry_date":"2026-05-29","days_to_expiry":49,"is_opened":true,"urgency":"low","reasons":["Scorta bassa (30%)"],"score":30,"on_bring":false,"locations":"dispensa"},{"product_id":136,"name":"Biscotti Pastefrolle","brand":"Balocco","category":"snack","unit":"g","current_qty":350,"default_qty":700,"package_unit":"","pct_left":67,"use_count":4,"buy_count":2,"daily_rate":27.47,"uses_per_month":4.7,"days_since_last_use":6,"days_left":13,"expiry_date":null,"days_to_expiry":999,"is_opened":false,"urgency":"low","reasons":["Previsto esaurimento tra ~13gg"],"score":25,"on_bring":false,"locations":"dispensa"}],"cached_at":"2026-04-10T05:15:02+00:00","cached_ts":1775798102}
|
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<meta name="theme-color" content="#2d5016">
|
<meta name="theme-color" content="#2d5016">
|
||||||
|
<meta name="author" content="Stimpfl Daniel">
|
||||||
|
<meta name="description" content="Self-hosted pantry manager with barcode scanning, AI identification, and shopping list integration.">
|
||||||
<title>Dispensa Manager</title>
|
<title>Dispensa Manager</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏠</text></svg>">
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏠</text></svg>">
|
||||||
|
|||||||
Reference in New Issue
Block a user