chore: auto-merge develop → main

Triggered by: 4515ff7 i18n: replace all hardcoded Italian strings with English
This commit is contained in:
github-actions[bot]
2026-05-18 07:34:28 +00:00
6 changed files with 50 additions and 41 deletions
+2 -2
View File
@@ -39,7 +39,7 @@
## ✨ Features
> ⚙️ **New in v1.7.23 — Global settings tab, DB auto-cleanup, vacuum-sealed expiry**
> A new **Generali** tab groups all global settings (language, currency, theme, screensaver, zero-waste, export) in one place.
> A new **General** tab groups all global settings (language, currency, theme, screensaver, zero-waste, export) in one place.
> Recipes older than `RECIPE_RETENTION_DAYS` and transactions older than `TRANSACTION_RETENTION_DAYS` are deleted automatically every cron cycle, followed by a SQLite `VACUUM` to keep the database small.
> Vacuum-sealed products get an extended grace period (`VACUUM_EXPIRY_EXTENSION_DAYS`, default 30 days) before being flagged as expired.
> Auto theme now follows **time of day** (dark 20:0007:00) instead of the OS setting, making it server-friendly.
@@ -100,7 +100,7 @@
### 🌙 Appearance
- **Dark mode** — Three modes: Light, Dark, and Auto (time-based: dark from 20:00 to 07:00, light otherwise); applies immediately without page reload; auto mode re-evaluates every 5 minutes, so night/day transitions happen automatically even on always-on kiosk displays; theme is applied before the first render to prevent a white flash
- **Global settings tab** — A dedicated **⚙️ Generali** tab groups all system-wide settings (language, currency, theme, screensaver, zero-waste tips, export) at the top of the Settings panel
- **Global settings tab** — A dedicated **⚙️ General** tab groups all system-wide settings (language, currency, theme, screensaver, zero-waste tips, export) at the top of the Settings panel
### Database Maintenance
- **Automatic cleanup** — Recipes older than `RECIPE_RETENTION_DAYS` (default 7) and transactions older than `TRANSACTION_RETENTION_DAYS` (default 7) are deleted automatically on every cron cycle; SQLite `VACUUM` runs after each cleanup to keep the file compact
+33 -33
View File
@@ -391,7 +391,7 @@ if (($_GET['action'] ?? '') === 'health_check') {
'ok' => $freeBytes === false || $freeBytes > 50*1048576,
'value' => $freeMB !== null ? $freeMB.' MB liberi' : null,
'optional' => true,
'hint' => $freeBytes !== false && $freeBytes <= 50*1048576 ? 'Meno di 50 MB liberi — libera spazio sul disco' : null,
'hint' => $freeBytes !== false && $freeBytes <= 50*1048576 ? 'Less than 50 MB free — free up disk space' : null,
];
// ── 8. SQLite database ────────────────────────────────────────────────────
@@ -419,11 +419,11 @@ if (($_GET['action'] ?? '') === 'health_check') {
$checks['db_legacy'] = [
'ok' => !$hasLegacy,
'optional' => true,
'hint' => $hasLegacy ? 'Trovato vecchio dispensa.db — il file è ormai obsoleto, puoi eliminarlo manualmente' : null,
'hint' => $hasLegacy ? 'Legacy dispensa.db found — the file is obsolete, you can delete it manually' : null,
];
if ($isFresh) {
$checks['db_connect'] = ['ok' => true, 'fresh' => true, 'value' => 'nuovo impianto'];
$checks['db_connect'] = ['ok' => true, 'fresh' => true, 'value' => 'fresh install'];
$checks['db_tables'] = ['ok' => true, 'fresh' => true];
$checks['db_integrity'] = ['ok' => true, 'fresh' => true];
$checks['db_wal'] = ['ok' => true, 'fresh' => true, 'optional' => true];
@@ -441,7 +441,7 @@ if (($_GET['action'] ?? '') === 'health_check') {
$checks['db_connect'] = ['ok' => true, 'value' => basename($dbPath)];
} catch (\Throwable $e) {
$checks['db_connect'] = ['ok' => false, 'error' => $e->getMessage(),
'hint' => 'Impossibile aprire il database — verifica permessi su data/evershelf.db'];
'hint' => 'Cannot open the database — check permissions on data/evershelf.db'];
}
if ($dbConnOk && $pdo) {
@@ -452,7 +452,7 @@ if (($_GET['action'] ?? '') === 'health_check') {
$checks['db_tables'] = [
'ok' => empty($missing),
'missing' => $missing,
'hint' => !empty($missing) ? 'Tabelle mancanti: ' . implode(', ', $missing) . ' — esegui una chiamata API per auto-inizializzare il DB' : null,
'hint' => !empty($missing) ? 'Missing tables: ' . implode(', ', $missing) . ' — call any API endpoint to auto-initialize the DB' : null,
];
// Integrity
@@ -466,7 +466,7 @@ if (($_GET['action'] ?? '') === 'health_check') {
// WAL
$wal = $pdo->query("PRAGMA journal_mode")->fetchColumn();
$checks['db_wal'] = ['ok' => $wal === 'wal', 'value' => $wal, 'optional' => true,
'hint' => $wal !== 'wal' ? 'Modalità journal non ottimalesarà corretta automaticamente al primo avvio' : null];
'hint' => $wal !== 'wal' ? 'Journal mode not optimal — will be corrected automatically on next startup' : null];
// Size & rows
$checks['db_size'] = ['ok' => true, 'value' => round(filesize($dbPath)/1024).' KB', 'optional' => true];
@@ -474,7 +474,7 @@ if (($_GET['action'] ?? '') === 'health_check') {
$checks['db_row_count'] = ['ok' => true, 'value' => $cnt.' prodotti in inventario', 'optional' => true];
} else {
foreach (['db_tables', 'db_integrity'] as $k)
$checks[$k] = ['ok' => false, 'hint' => 'Impossibile verificare — connessione DB fallita'];
$checks[$k] = ['ok' => false, 'hint' => 'Cannot verify — DB connection failed'];
foreach (['db_wal', 'db_size', 'db_row_count'] as $k)
$checks[$k] = ['ok' => false, 'optional' => true];
}
@@ -492,10 +492,10 @@ if (($_GET['action'] ?? '') === 'health_check') {
$geminiKey = $envGet('GEMINI_API_KEY');
if (!empty($geminiKey)) {
$checks['gemini_key'] = ['ok' => strlen($geminiKey) > 20, 'optional' => true,
'hint' => strlen($geminiKey) <= 20 ? 'Chiave Gemini AI sembra troppo cortaverifica il valore in .env' : null];
'hint' => strlen($geminiKey) <= 20 ? 'Gemini AI key looks too short — check the value in .env' : null];
} else {
$checks['gemini_key'] = ['ok' => true, 'optional' => true,
'value' => 'non configurata', 'hint' => 'Configura GEMINI_API_KEY in .env per abilitare le funzioni AI'];
'value' => 'not configured', 'hint' => 'Set GEMINI_API_KEY in .env to enable AI features'];
}
// ── 11. Bring! — solo se EMAIL+PASSWORD sono impostate ───────────────────
@@ -511,7 +511,7 @@ if (($_GET['action'] ?? '') === 'health_check') {
$checks['tts_url'] = [
'ok' => !empty($ttsUrl),
'optional' => true,
'hint' => empty($ttsUrl) ? 'TTS_ENABLED=true ma TTS_URL non configurata' : null,
'hint' => empty($ttsUrl) ? 'TTS_ENABLED=true but TTS_URL not configured' : null,
];
}
@@ -521,7 +521,7 @@ if (($_GET['action'] ?? '') === 'health_check') {
$checks['scale_gateway'] = [
'ok' => !empty($scaleUrl),
'optional' => true,
'hint' => empty($scaleUrl) ? 'SCALE_ENABLED=true ma SCALE_GATEWAY_URL non configurata' : null,
'hint' => empty($scaleUrl) ? 'SCALE_ENABLED=true but SCALE_GATEWAY_URL not configured' : null,
];
}
@@ -546,7 +546,7 @@ if (($_GET['action'] ?? '') === 'health_check') {
curl_close($ch);
$internetOk = $httpCode > 0 || $curlErrNo === 0;
$checks['internet'] = ['ok' => $internetOk, 'optional' => true,
'hint' => !$internetOk ? 'Impossibile raggiungere i server Gemini — le funzioni AI non funzioneranno senza connessione internet' : null];
'hint' => !$internetOk ? 'Cannot reach Gemini servers — AI features will not work without an internet connection' : null];
}
// ── Compute overall result ────────────────────────────────────────────────
@@ -2129,7 +2129,7 @@ function updateInventory(PDO $db): void {
if (abs($diff) > 0.001) {
$txType = $diff > 0 ? 'in' : 'out';
$txQty = abs($diff);
$db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, ?, ?, ?, '[Correzione manuale]')")
$db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, ?, ?, ?, '[Manual correction]')")
->execute([$pid, $txType, $txQty, $loc]);
}
}
@@ -2331,7 +2331,7 @@ function undoTransaction(PDO $db): void {
}
}
// Log counter-transaction
$db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, 'out', ?, ?, '[Annullato]')")->execute([$productId, $quantity, $location]);
$db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, 'out', ?, ?, '[Undone]')")->execute([$productId, $quantity, $location]);
} elseif ($type === 'out' || $type === 'waste') {
// Reverse a USE: add quantity back to inventory
@@ -2345,7 +2345,7 @@ function undoTransaction(PDO $db): void {
$db->prepare("INSERT INTO inventory (product_id, location, quantity) VALUES (?, ?, ?)")->execute([$productId, $location, $quantity]);
}
// Log counter-transaction
$db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, 'in', ?, ?, '[Annullato]')")->execute([$productId, $quantity, $location]);
$db->prepare("INSERT INTO transactions (product_id, type, quantity, location, notes) VALUES (?, 'in', ?, ?, '[Undone]')")->execute([$productId, $quantity, $location]);
}
// Mark original as undone
@@ -3599,7 +3599,7 @@ function geminiChat(PDO $db): void {
$langName = recipeLangName($lang);
if (empty($message)) {
echo json_encode(['success' => false, 'error' => 'Messaggio vuoto']);
echo json_encode(['success' => false, 'error' => 'Empty message']);
return;
}
@@ -3703,7 +3703,7 @@ PROMPT;
$httpCode = $result['http_code'];
if ($httpCode !== 200) {
$errMsg = $result['data']['error']['message'] ?? 'Errore API Gemini';
$errMsg = $result['data']['error']['message'] ?? 'Gemini API error';
echo json_encode(['success' => false, 'error' => $errMsg, 'http_code' => $httpCode]);
return;
}
@@ -3711,7 +3711,7 @@ PROMPT;
$reply = $result['data']['candidates'][0]['content']['parts'][0]['text'] ?? '';
if (empty($reply)) {
echo json_encode(['success' => false, 'error' => 'Risposta vuota da Gemini']);
echo json_encode(['success' => false, 'error' => 'Empty response from Gemini']);
return;
}
@@ -5371,7 +5371,7 @@ PROMPT;
$httpCode = $result['http_code'];
if ($httpCode !== 200) {
$errMsg = $result['data']['error']['message'] ?? 'Errore API Gemini';
$errMsg = $result['data']['error']['message'] ?? 'Gemini API error';
echo json_encode(['success' => false, 'error' => $errMsg, 'http_code' => $httpCode]);
return;
}
@@ -5386,7 +5386,7 @@ PROMPT;
$identified = json_decode($text, true);
if (!$identified || empty($identified['name'])) {
echo json_encode(['success' => false, 'error' => 'Impossibile identificare il prodotto', 'raw' => $text]);
echo json_encode(['success' => false, 'error' => 'Cannot identify the product', 'raw' => $text]);
return;
}
@@ -6290,7 +6290,7 @@ function bringGetList(): void {
$auth = bringAuth();
if (!$auth) {
EverLog::info('bringGetList');
echo json_encode(['success' => false, 'error' => 'Credenziali Bring! non configurate. Aggiungi BRING_EMAIL e BRING_PASSWORD al file .env']);
echo json_encode(['success' => false, 'error' => 'Bring! credentials not configured. Add BRING_EMAIL and BRING_PASSWORD to .env']);
return;
}
@@ -6301,14 +6301,14 @@ function bringGetList(): void {
if ($lists && isset($lists['lists'][0]['listUuid'])) {
$listUUID = $lists['lists'][0]['listUuid'];
} else {
echo json_encode(['success' => false, 'error' => 'Nessuna lista Bring! trovata']);
echo json_encode(['success' => false, 'error' => 'No Bring! list found']);
return;
}
}
$data = bringRequest('GET', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}");
if (!$data) {
echo json_encode(['success' => false, 'error' => 'Errore nel recupero della lista']);
echo json_encode(['success' => false, 'error' => 'Error fetching the list']);
return;
}
@@ -6368,7 +6368,7 @@ function bringAddItems(): void {
$auth = bringAuth();
if (!$auth) {
EverLog::info('bringAddItems');
echo json_encode(['success' => false, 'error' => 'Credenziali Bring! non configurate']);
echo json_encode(['success' => false, 'error' => 'Bring! credentials not configured']);
return;
}
@@ -6377,7 +6377,7 @@ function bringAddItems(): void {
$listUUID = $input['listUUID'] ?? $auth['bringListUUID'];
if (empty($listUUID)) {
echo json_encode(['success' => false, 'error' => 'Lista non trovata']);
echo json_encode(['success' => false, 'error' => 'List not found']);
return;
}
@@ -6446,7 +6446,7 @@ function bringRemoveItem(): void {
$auth = bringAuth();
if (!$auth) {
EverLog::info('bringRemoveItem');
echo json_encode(['success' => false, 'error' => 'Credenziali Bring! non configurate']);
echo json_encode(['success' => false, 'error' => 'Bring! credentials not configured']);
return;
}
@@ -6455,7 +6455,7 @@ function bringRemoveItem(): void {
$listUUID = $input['listUUID'] ?? $auth['bringListUUID'];
if (empty($name) || empty($listUUID)) {
echo json_encode(['success' => false, 'error' => 'Parametri mancanti']);
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
return;
}
@@ -6494,19 +6494,19 @@ function bringCleanSpecs(): void {
$auth = bringAuth();
if (!$auth) {
EverLog::info('bringCleanSpecs');
echo json_encode(['success' => false, 'error' => 'Credenziali Bring! non configurate']);
echo json_encode(['success' => false, 'error' => 'Bring! credentials not configured']);
return;
}
$listUUID = $auth['bringListUUID'];
if (empty($listUUID)) {
echo json_encode(['success' => false, 'error' => 'Lista non trovata']);
echo json_encode(['success' => false, 'error' => 'List not found']);
return;
}
$data = bringRequest('GET', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}");
if (!$data || !isset($data['purchase'])) {
echo json_encode(['success' => false, 'error' => 'Errore nel recupero della lista']);
echo json_encode(['success' => false, 'error' => 'Error fetching the list']);
return;
}
@@ -6605,17 +6605,17 @@ function bringMigrateNames(PDO $db): void {
$auth = bringAuth();
if (!$auth) {
EverLog::info('bringMigrateNames');
echo json_encode(['success' => false, 'error' => 'Credenziali Bring! non configurate']);
echo json_encode(['success' => false, 'error' => 'Bring! credentials not configured']);
return;
}
$listUUID = $auth['bringListUUID'];
if (empty($listUUID)) {
echo json_encode(['success' => false, 'error' => 'Lista non trovata']);
echo json_encode(['success' => false, 'error' => 'List not found']);
return;
}
$data = bringRequest('GET', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}");
if (!$data || !isset($data['purchase'])) {
echo json_encode(['success' => false, 'error' => 'Errore nel recupero della lista']);
echo json_encode(['success' => false, 'error' => 'Error fetching the list']);
return;
}
+3 -3
View File
@@ -2744,7 +2744,7 @@ function _openKioskNativeSettings() {
_kioskBridge.openNativeSettings();
} catch(e) {
// Older APK without openNativeSettings bridge — inform user to update
showToast(t('settings.kiosk.native_update_hint') || 'Aggiorna l\'app kiosk per usare questa funzione', 'warning', 4000);
showToast(t('settings.kiosk.native_update_hint'), 'warning', 4000);
}
}
@@ -11452,14 +11452,14 @@ async function analyzeExpiryImage(dataUrl) {
if (expiryInput) {
expiryInput.value = result.expiry_date;
}
statusDiv.innerHTML = `<p style="color:var(--success);font-weight:600">✅ Data trovata: ${formatDate(result.expiry_date)}</p>`;
statusDiv.innerHTML = `<p style="color:var(--success);font-weight:600">✅ ${t('scanner.expiry_found')}: ${formatDate(result.expiry_date)}</p>`;
// Close modal after delay
setTimeout(() => closeExpiryScanner(), 1500);
} else if (result.error === 'no_api_key') {
statusDiv.innerHTML = `<p style="color:var(--warning)">${t('ai.no_api_key').replace(/\n/g, '<br>')}</p>`;
} else {
statusDiv.innerHTML = `<p style="color:var(--danger)">❌ Non riesco a leggere la data. ${result.raw_text ? '<br><small>Letto: ' + escapeHtml(result.raw_text) + '</small>' : ''}</p>
statusDiv.innerHTML = `<p style="color:var(--danger)">❌ ${t('scanner.expiry_read_fail')} ${result.raw_text ? '<br><small>' + t('scanner.expiry_raw_label') + ': ' + escapeHtml(result.raw_text) + '</small>' : ''}</p>
<button class="btn btn-secondary" onclick="retakeExpiry()" style="margin-top:8px">${t('btn.retry')}</button>`;
}
} catch (err) {
+4 -1
View File
@@ -1034,7 +1034,10 @@
"retake_btn": "🔄 Erneut aufnehmen",
"camera_error_hint": "Stelle sicher, dass du HTTPS verwendest und Kameraberechtigungen erteilt hast.<br>Du kannst den Barcode manuell eingeben oder die KI-Identifikation verwenden.",
"no_barcode": "Kein Barcode",
"save_new_btn": "🆕 Keines davon — als neu speichern"
"save_new_btn": "🆕 Keines davon — als neu speichern",
"expiry_found": "Datum gefunden",
"expiry_read_fail": "Datum konnte nicht gelesen werden.",
"expiry_raw_label": "Erkannt"
},
"lowstock": {
"title": "⚠️ Wird knapp!",
+4 -1
View File
@@ -1034,7 +1034,10 @@
"retake_btn": "🔄 Retake",
"camera_error_hint": "Ensure you use HTTPS and have granted camera permissions.<br>You can enter the barcode manually or use AI identification.",
"no_barcode": "No barcode",
"save_new_btn": "🆕 None of these — save as new"
"save_new_btn": "🆕 None of these — save as new",
"expiry_found": "Date found",
"expiry_read_fail": "Cannot read the date.",
"expiry_raw_label": "Read"
},
"lowstock": {
"title": "⚠️ Running low!",
+4 -1
View File
@@ -1034,7 +1034,10 @@
"retake_btn": "🔄 Riscatta",
"camera_error_hint": "Assicurati di usare HTTPS e di aver concesso i permessi della fotocamera.<br>Puoi inserire il barcode manualmente o usare l'identificazione AI.",
"no_barcode": "Senza barcode",
"save_new_btn": "🆕 Non è nessuno di questi — salva come nuovo"
"save_new_btn": "🆕 Non è nessuno di questi — salva come nuovo",
"expiry_found": "Data trovata",
"expiry_read_fail": "Non riesco a leggere la data.",
"expiry_raw_label": "Letto"
},
"lowstock": {
"title": "⚠️ Sta per finire!",