diff --git a/api/cron_smart_shopping.php b/api/cron_smart_shopping.php index 867f777..6fdbce9 100644 --- a/api/cron_smart_shopping.php +++ b/api/cron_smart_shopping.php @@ -79,6 +79,19 @@ try { echo '[' . date('Y-m-d H:i:s') . '] Shelf life pre-warm warning: ' . $pe->getMessage() . "\n"; } + // ── DB cleanup (retention policy) ──────────────────────────────────── + // Delete old recipes and transactions based on .env retention settings. + try { + ob_start(); + dbCleanup($db); + ob_end_clean(); + echo '[' . date('Y-m-d H:i:s') . '] DB cleanup done' + . ' (recipes >' . env('RECIPE_RETENTION_DAYS','7') . 'd' + . ', tx >' . env('TRANSACTION_RETENTION_DAYS','7') . 'd' . ")\n"; + } catch (Throwable $ce) { + echo '[' . date('Y-m-d H:i:s') . '] DB cleanup warning: ' . $ce->getMessage() . "\n"; + } + } catch (Throwable $e) { $msg = $e->getMessage(); echo '[' . date('Y-m-d H:i:s') . '] ERROR: ' . $msg . "\n"; diff --git a/api/index.php b/api/index.php index 8c576e4..20fafd2 100644 --- a/api/index.php +++ b/api/index.php @@ -409,7 +409,13 @@ if (($_GET['action'] ?? '') === 'health_check') { } } - // Legacy DB still present alongside evershelf.db → warn + // Auto-delete legacy dispensa.db if evershelf.db already exists (it's just an empty leftover) + if ($hasLegacy && file_exists($dbPath) && filesize($legacyDb) < 1024) { + @unlink($legacyDb); + $hasLegacy = false; + } + + // Legacy DB still present alongside evershelf.db → warn (should be rare now) $checks['db_legacy'] = [ 'ok' => !$hasLegacy, 'optional' => true, @@ -898,6 +904,10 @@ try { checkUpdate(); break; + case 'db_cleanup': + dbCleanup(getDB()); + break; + case 'gemini_product_hint': geminiProductHint(); break; @@ -2492,14 +2502,19 @@ function getStats(PDO $db): void { LIMIT 4 ")->fetchAll(); - // Expired - $expired = $db->query(" + // Expired — vacuum-sealed items get extra days beyond printed expiry before being flagged + $vacExtDays = (int)env('VACUUM_EXPIRY_EXTENSION_DAYS', '30'); + $expiredStmt = $db->prepare(" SELECT i.*, p.name, p.brand, p.category, p.unit, p.default_quantity, p.package_unit, COALESCE(i.vacuum_sealed, 0) as vacuum_sealed FROM inventory i JOIN products p ON i.product_id = p.id - WHERE i.expiry_date IS NOT NULL AND i.expiry_date < date('now') AND i.quantity > 0 + WHERE i.expiry_date IS NOT NULL + AND julianday('now') - julianday(i.expiry_date) > CASE WHEN COALESCE(i.vacuum_sealed,0)=1 THEN ? ELSE 0 END + AND i.quantity > 0 ORDER BY i.expiry_date ASC - ")->fetchAll(); + "); + $expiredStmt->execute([$vacExtDays]); + $expired = $expiredStmt->fetchAll(); // Opened (items with opened_at set by the app, OR fractional-qty items as legacy fallback) // opened_at IS NOT NULL → already has recalculated expiry_date stored when first opened @@ -2899,9 +2914,31 @@ function getServerSettings(): void { 'price_country' => env('PRICE_COUNTRY', 'Italia'), 'price_currency' => env('PRICE_CURRENCY', 'EUR'), 'price_update_months' => (int)env('PRICE_UPDATE_MONTHS', '3'), + 'recipe_retention_days' => (int)env('RECIPE_RETENTION_DAYS', '7'), + 'transaction_retention_days' => (int)env('TRANSACTION_RETENTION_DAYS', '7'), + 'vacuum_expiry_extension_days' => (int)env('VACUUM_EXPIRY_EXTENSION_DAYS', '30'), ]); } +function dbCleanup(?PDO $db = null): void { + $recipeDays = max(1, (int)env('RECIPE_RETENTION_DAYS', '7')); + $txDays = max(1, (int)env('TRANSACTION_RETENTION_DAYS', '7')); + $pdo = $db ?? getDB(); + try { + // Delete old recipes (generated recipe plans) + $pdo->prepare("DELETE FROM recipes WHERE date < date('now', ? || ' days')") + ->execute(["-$recipeDays"]); + // Delete old transactions (keep at least the last $txDays of history) + $pdo->prepare("DELETE FROM transactions WHERE created_at < datetime('now', ? || ' days') AND undone = 0") + ->execute(["-$txDays"]); + // Compact the database + $pdo->exec('VACUUM'); + echo json_encode(['success' => true, 'recipe_retention_days' => $recipeDays, 'transaction_retention_days' => $txDays]); + } catch (Throwable $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + } +} + function saveSettings(): void { // Require SETTINGS_TOKEN if configured $requiredToken = env('SETTINGS_TOKEN'); @@ -2957,9 +2994,12 @@ function saveSettings(): void { ]; // Integer keys $intMap = [ - 'default_persons' => 'DEFAULT_PERSONS', - 'screensaver_timeout' => 'SCREENSAVER_TIMEOUT', - 'price_update_months' => 'PRICE_UPDATE_MONTHS', + 'default_persons' => 'DEFAULT_PERSONS', + 'screensaver_timeout' => 'SCREENSAVER_TIMEOUT', + 'price_update_months' => 'PRICE_UPDATE_MONTHS', + 'recipe_retention_days' => 'RECIPE_RETENTION_DAYS', + 'transaction_retention_days' => 'TRANSACTION_RETENTION_DAYS', + 'vacuum_expiry_extension_days'=> 'VACUUM_EXPIRY_EXTENSION_DAYS', ]; // Float keys $floatMap = [ diff --git a/assets/js/app.js b/assets/js/app.js index fd0a3ac..86dcd75 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -3116,6 +3116,8 @@ async function saveSettings() { scale_gateway_url: s.scale_gateway_url, meal_plan_enabled: s.meal_plan_enabled, screensaver_enabled: s.screensaver_enabled, + screensaver_timeout: s.screensaver_timeout || 5, + zerowaste_tips_enabled: s.zerowaste_tips_enabled, tts_enabled: s.tts_enabled, tts_url: s.tts_url, tts_token: s.tts_token, @@ -3133,6 +3135,9 @@ async function saveSettings() { price_country: s.price_country, price_currency: s.price_currency, price_update_months: s.price_update_months, + recipe_retention_days: s.recipe_retention_days || 7, + transaction_retention_days: s.transaction_retention_days || 7, + vacuum_expiry_extension_days: s.vacuum_expiry_extension_days || 30, }, tokenHeader); const statusEl = document.getElementById('settings-status'); if (result.success) { diff --git a/index.html b/index.html index c10fc75..02e4a52 100644 --- a/index.html +++ b/index.html @@ -1652,6 +1652,6 @@ - +