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:
dadaloop82
2026-04-10 05:24:27 +00:00
parent 35cf133be4
commit e0956c6043
16 changed files with 496 additions and 2768 deletions
+22
View File
@@ -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
View File
@@ -1,28 +1,31 @@
# Environment variables (secrets)
# Environment variables (secrets — copy .env.example to .env)
.env
# SQLite WAL/SHM temp files
# Data directory (user-specific runtime data)
data/dispensa.db
data/*.db-wal
data/*.db-shm
# Bring! auth token cache
data/backups/
data/cron.log
data/smart_shopping_cache.json
data/bring_token.json
data/bring_catalog.json
# DupliClick token cache
data/dupliclick_token.json
# Client debug log (runtime only)
data/client_debug.log
# SSL CA cert (local only)
ca.crt
# SSL certificates (local only)
data/*.crt
data/*.pem
*.crt
*.pem
# OS files
.DS_Store
Thumbs.db
# Editor
# Editor / IDE
*.swp
*.swo
*~
.idea/
.vscode/
+32
View File
@@ -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
+21
View File
@@ -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.
+280
View File
@@ -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: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![PHP](https://img.shields.io/badge/PHP-8.0+-blue.svg)](https://www.php.net/)
[![SQLite](https://img.shields.io/badge/SQLite-3-blue.svg)](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
View File
@@ -1,6 +1,10 @@
<?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');
+70 -120
View File
@@ -1,12 +1,44 @@
<?php
/**
* 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)
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
if (!defined('CRON_MODE')) {
@@ -586,8 +618,8 @@ function listInventory(PDO $db): void {
function addToInventory(PDO $db): void {
$input = json_decode(file_get_contents('php://input'), true);
$productId = $input['product_id'] ?? 0;
$quantity = $input['quantity'] ?? 1;
$productId = (int)($input['product_id'] ?? 0);
$quantity = (float)($input['quantity'] ?? 1);
$location = $input['location'] ?? 'dispensa';
$expiry = $input['expiry_date'] ?? null;
$unit = $input['unit'] ?? null;
@@ -598,6 +630,21 @@ function addToInventory(PDO $db): void {
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 ($unit) {
$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 =====
function getServerSettings(): void {
$envFile = __DIR__ . '/../.env';
$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);
}
}
// Return masked versions for security
$geminiKey = $envVars['GEMINI_API_KEY'] ?? '';
$bringEmail = $envVars['BRING_EMAIL'] ?? '';
$bringPassword = $envVars['BRING_PASSWORD'] ?? '';
// Return values for client — passwords are never exposed
$geminiKey = env('GEMINI_API_KEY');
$bringEmail = env('BRING_EMAIL');
echo json_encode([
'gemini_key' => $geminiKey,
'gemini_key_set' => !empty($geminiKey),
'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 {
$input = json_decode(file_get_contents('php://input'), true);
$envFile = __DIR__ . '/../.env';
// 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);
}
}
$envVars = loadEnv();
// Update values from input — only overwrite if new value is non-empty
if (!empty($input['gemini_key'])) {
@@ -1227,22 +1256,7 @@ function saveSettings(): void {
// ===== GEMINI AI FUNCTIONS =====
function geminiReadExpiry(): void {
// Load API key from .env
$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);
}
}
}
}
$apiKey = env('GEMINI_API_KEY');
if (empty($apiKey)) {
echo json_encode(['success' => false, 'error' => 'no_api_key']);
return;
@@ -1330,22 +1344,7 @@ function geminiReadExpiry(): void {
// ===== GEMINI CHAT =====
function geminiChat(PDO $db): void {
// Load 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);
}
}
}
}
$apiKey = env('GEMINI_API_KEY');
if (empty($apiKey)) {
echo json_encode(['success' => false, 'error' => 'no_api_key']);
return;
@@ -1497,22 +1496,7 @@ PROMPT;
// ===== RECIPE GENERATION WITH GEMINI =====
function generateRecipe(PDO $db): void {
// Load API key from .env
$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);
}
}
}
}
$apiKey = env('GEMINI_API_KEY');
if (empty($apiKey)) {
echo json_encode(['success' => false, 'error' => 'no_api_key']);
return;
@@ -2034,22 +2018,7 @@ PROMPT;
// ===== GEMINI AI PRODUCT IDENTIFICATION =====
function geminiIdentifyProduct(): void {
// Load 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);
}
}
}
}
$apiKey = env('GEMINI_API_KEY');
if (empty($apiKey)) {
echo json_encode(['success' => false, 'error' => 'no_api_key']);
return;
@@ -2200,26 +2169,9 @@ function searchOpenFoodFacts(string $searchTerms, string $name, string $brand):
// ===== 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 {
$env = loadEnvVars();
$email = $env['BRING_EMAIL'] ?? '';
$password = $env['BRING_PASSWORD'] ?? '';
$email = env('BRING_EMAIL');
$password = env('BRING_PASSWORD');
if (empty($email) || empty($password)) {
return null;
@@ -2955,8 +2907,7 @@ function smartShopping(PDO $db): void {
}
function bringSuggestItems(PDO $db): void {
$env = loadEnvVars();
$apiKey = $env['GEMINI_API_KEY'] ?? '';
$apiKey = env('GEMINI_API_KEY');
if (empty($apiKey)) {
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
*/
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;
$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.
+8
View File
@@ -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 ===== */
:root {
--primary: #2d5016;
+24 -5
View File
@@ -1,6 +1,10 @@
/**
* 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 =====
@@ -722,13 +726,13 @@ async function loadSettingsUI() {
}
// TTS settings — init defaults on first load
if (!s._tts_initialized) {
s.tts_url = s.tts_url || 'http://192.168.1.133:8123/api/events/noemi_speak';
s.tts_token = s.tts_token || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI2OGQ3Njk1N2E0MjY0ZTBjOWQ4YjczZDY4ZDVmMWJlZCIsImlhdCI6MTc2MDI1NjIxNSwiZXhwIjoyMDc1NjE2MjE1fQ.X5UyMMPd7wTA6Gh11Nzg7Ox-enlDDom_lJIAJruUtcE';
s.tts_url = s.tts_url || '';
s.tts_token = s.tts_token || '';
s.tts_payload_key = s.tts_payload_key || 'message';
s.tts_method = s.tts_method || 'POST';
s.tts_auth_type = s.tts_auth_type || 'bearer';
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;
saveSettingsToStorage(s);
}
@@ -762,6 +766,21 @@ async function loadSettingsUI() {
if (!s.bring_email && 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 */ }
}
@@ -7416,7 +7435,7 @@ function renderCookingStep() {
}
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 authType = s.tts_auth_type || 'bearer';
const token = s.tts_token || '';
+18 -13
View File
@@ -1,18 +1,23 @@
#!/bin/bash
# Daily backup of Dispensa database to GitHub
# Runs via cron: commits and pushes data/dispensa.db
# Daily backup of Dispensa database (local only)
# 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
if git diff --quiet data/ 2>/dev/null && git diff --cached --quiet data/ 2>/dev/null; then
# Check for untracked files in data/
if [ -z "$(git ls-files --others --exclude-standard data/)" ]; then
exit 0 # Nothing changed
fi
mkdir -p "$BACKUP_DIR"
DB_FILE="${INSTALL_DIR}/data/dispensa.db"
if [ ! -f "$DB_FILE" ]; then
exit 0
fi
DATE=$(date '+%Y-%m-%d %H:%M')
git add data/dispensa.db
git commit -m "📦 Backup database automatico - $DATE"
git push
DATE=$(date '+%Y-%m-%d_%H%M')
cp "$DB_FILE" "${BACKUP_DIR}/dispensa_${DATE}.db"
# Keep only the last 7 backups
ls -t "${BACKUP_DIR}"/dispensa_*.db 2>/dev/null | tail -n +8 | xargs -r rm --
View File
-2585
View File
File diff suppressed because it is too large Load Diff
-32
View File
@@ -1,32 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIFmTCCA4GgAwIBAgIUUH/pYPk0V275mGrJ3N8F1mPxotswDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCSVQxDTALBgNVBAgMBEhvbWUxDTALBgNVBAcMBEhvbWUx
FDASBgNVBAoMC0Rpc3BlbnNhIENBMRkwFwYDVQQDDBBEaXNwZW5zYSBSb290IENB
MB4XDTI2MDMxMDEwNDk1NVoXDTM2MDMwNzEwNDk1NVowXDELMAkGA1UEBhMCSVQx
DTALBgNVBAgMBEhvbWUxDTALBgNVBAcMBEhvbWUxFDASBgNVBAoMC0Rpc3BlbnNh
IENBMRkwFwYDVQQDDBBEaXNwZW5zYSBSb290IENBMIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEA5dgMhEqr5D0yyHltZWgvmDK9J9qukEgcHLxN98krgne1
P65plV5xWjsxPSOd1vrNWY5VKmDV3o91zANT9xkN//EREFwum8ozuIhsNTf69E+L
Nbshof0LKQE1/epOQzDOTy+OaGLXI4htN/W8FfJFhBw+xL6tCu9JM1jT5Qe/RoZ7
pljJL7lsHhnG0PkA7seXwKhQ6fZLPGXV+ixqrlsGmClczvM+9xYNLXFlo7ihDRrW
dirj3n/0bouGNxxVWPiSDSub8XZmohKA5drOR6OHzhQUOHXgx9LOWu8qJ9/SQ0zF
iiC0TQIKrNVb++x5gu4YRHBt2KWpEq2QzRD5OK/b0TC/rQUJE/i4I1y+GSo8Tktv
T6hqEk1cJ9c5wVqbCNd2D+ECCQ+eMQYtuNTWWk6SfL0dWUgWtvJJ8Ezn7zJE/G7T
KV1WgXpTnRypNzkY3XSv95mNAKDe52fH4LuH0SyZp8kYjSQ3Jz0wCAZSrozqXRic
dqn5qv4YOja/63TgDGjfJUwNfSnY0/z6q21chVAOYnAFWFE5IOdWAAwUcbpwXl0K
ruQvtc3QVOdJe0zNr3NxwZV2EtfF3NN84661XgM7xHx8w+1WSu7xL/fAVYV+Sd+a
zsqh7LGvOf8Gn37L8x67scnUhAntp+ItBF/fidlDHqfiuj2+cb+yeTIZ+zvTlW8C
AwEAAaNTMFEwHQYDVR0OBBYEFN4YOAYAEVI7FNyqtZq8H35PqNHkMB8GA1UdIwQY
MBaAFN4YOAYAEVI7FNyqtZq8H35PqNHkMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
hvcNAQELBQADggIBAKrtFYLE5/6u9A6kdUknaTjPchteql/wvQRrayYdpzXSLg3g
uKrdTkHysbbtkyXLe2KLIFVldMHtytoRj5pOT0ONY/Ba9oB9svxIsAyX6CneVh9e
lvRc64LFU/JBxZPgUIiPzkTFiB6WmEAHc3Q2RcwMjt1LhUVSmbYlbWz5FIvLcR3S
lWUKXHVxpXQ0e7YmrHFqg+l3p1TdFoB58ppKt4dQtC31i5il30ZCVFI+x1k19vwj
MusHniDwq/lHkqZo64aE+DpNlzgkvNSyL6yafvROrlcMGRJkaK1wwKum10vfWbB4
P1Bd9XOZ6Yt++McY85cMCVRtbu8Wm5EtnHEkmyVyBwIsdDJhfn/8Gwwhpy5tnu8A
Zd1J/NW3/qxnpKHsT9ebAM/3XRKpT/fvlbZBG8dTei/pDPLtyxMmbo+HRswjnmBh
yc7GK2iNPumLC3W1UxL7Ncdspj5k0o0xAz/ugOYN44n1Oxth0WnRo4sA3YoTqScR
slSCfUnXrZmWl3mIIsf2UFjV2doM4EReKsgu6uzHTcG4AHQCFi1fhNY2LnIPcqmy
1C8iQCskEr24OKBQUmdJaIZhUB3IXUmVh0fi8R+BGBxC372WleopjN3BubsL+u7h
QxgNJom9SmeoTT/5FbUTr3kGOxLiAuwPAvMIbep+l1BSQbosOZtKxObHkcUN
-----END CERTIFICATE-----
BIN
View File
Binary file not shown.
-1
View File
@@ -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}
+2
View File
@@ -6,6 +6,8 @@
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<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>
<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>">