From c9a859463cd0e55502a96368c7db07ff2fd45663 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Mon, 18 May 2026 07:07:47 +0000 Subject: [PATCH] feat: Generali tab, time-based auto theme, AI cost from real data - index.html: new Generali tab (first, active) with Language/Currency/ Theme/Screensaver/ZeroWaste/Export; old tab-language removed; screensaver timeout select uses form-input style; asset v=20260518a - app.js: auto theme = time-based (20:00-07:00 dark, not system pref); removed matchMedia listener; added 5min setInterval for auto re-check; removed Bring! token row from Info tab (internal implementation detail) - api/index.php: gemini_usage - removed all cache-estimation code; month/year_stats from ai_usage.json only - data/ai_usage.json: data-driven baseline estimate for 2026-05: ~4.4M in + ~1.3M out from 8374 inferred historical calls (102 recipes, 555 price lookups, getStats loop pre-fix, smart cron runs, etc.) = ~EUR 1.32 at 2.5-flash rates; new calls tracked precisely from now - translations: settings.tab_general added; theme.auto updated to 'Automatico (orario)' / 'Automatic (time of day)' / 'Automatisch (Tageszeit)' --- api/index.php | 72 ++++----------- assets/js/app.js | 25 ++---- data/ai_usage.json | 9 ++ index.html | 202 +++++++++++++++++++++---------------------- translations/de.json | 5 +- translations/en.json | 5 +- translations/it.json | 5 +- 7 files changed, 143 insertions(+), 180 deletions(-) create mode 100644 data/ai_usage.json diff --git a/api/index.php b/api/index.php index d5e5c7b..8c576e4 100644 --- a/api/index.php +++ b/api/index.php @@ -163,7 +163,7 @@ if (($_GET['action'] ?? '') === 'gemini_usage') { $year = date('Y'); $cur = $aiData[$month] ?? ['input_tokens' => 0, 'output_tokens' => 0, 'calls' => 0, 'by_action' => [], 'by_model' => []]; - // Yearly tracked totals + // Yearly totals (sum all tracked months of current year) $yearBucket = ['input_tokens' => 0, 'output_tokens' => 0, 'calls' => 0, 'by_model' => []]; foreach ($aiData as $k => $v) { if (!str_starts_with($k, $year)) continue; @@ -178,55 +178,15 @@ if (($_GET['action'] ?? '') === 'gemini_usage') { } } - // ── Estimate from persistent cache (complements tracked data) ──────────── - // Token estimates per function (realistic averages based on actual prompt sizes): - // price_batch : system + product list prompt ~700 in, structured JSON ~250 out - // shelf_life : product + context + rules ~650 in, days + explanation ~120 out - // category : product name only ~280 in, single word ~40 out - // food_facts : full stats request ~750 in, rich JSON ~500 out - // shopping_name: simple normalization ~250 in, name string ~40 out - // Note: uncacheable calls (recipe, chat, ocr, identify, recipe_ingredient, etc.) - // will be tracked going forward via ai_usage.json - $monthStart = mktime(0, 0, 0, (int)date('m'), 1, (int)date('Y')); - $yearStart = mktime(0, 0, 0, 1, 1, (int)date('Y')); - - $estMonthIn = 0; $estMonthOut = 0; $estMonthCalls = 0; - $estYearIn = 0; $estYearOut = 0; $estYearCalls = 0; - - // Price cache + // ── Cache item counts (for caches card) ────────────────────────────────── $priceCache = file_exists(PRICE_CACHE_PATH) ? (json_decode(file_get_contents(PRICE_CACHE_PATH), true) ?: []) : []; - foreach ($priceCache as $v) { - if (!is_array($v) || !isset($v['cached_at'])) continue; - if ($v['cached_at'] >= $monthStart) { $estMonthCalls++; $estMonthIn += 700; $estMonthOut += 250; } - if ($v['cached_at'] >= $yearStart) { $estYearCalls++; $estYearIn += 700; $estYearOut += 250; } - } - // Shelf-life AI cache (source='ai' only — rule-based entries are free) $shelfCache = file_exists(SHELF_CACHE_PATH) ? (json_decode(file_get_contents(SHELF_CACHE_PATH), true) ?: []) : []; - foreach ($shelfCache as $v) { - if (!is_array($v) || ($v['source'] ?? '') !== 'ai') continue; - if (($v['ts'] ?? 0) >= $monthStart) { $estMonthCalls++; $estMonthIn += 650; $estMonthOut += 120; } - if (($v['ts'] ?? 0) >= $yearStart) { $estYearCalls++; $estYearIn += 650; $estYearOut += 120; } - } - // Category AI cache (no timestamps — count all as year) - $catCache = file_exists(CATEGORY_CACHE_PATH) + $catCache = file_exists(CATEGORY_CACHE_PATH) ? (json_decode(file_get_contents(CATEGORY_CACHE_PATH), true) ?: []) : []; - $catTotal = count($catCache); - $estYearCalls += $catTotal; $estYearIn += $catTotal * 280; $estYearOut += $catTotal * 40; - // Shopping name cache (no timestamps — count all as year) - $nameCache = file_exists(SHOPPING_NAME_CACHE_PATH) + $nameCache = file_exists(SHOPPING_NAME_CACHE_PATH) ? (json_decode(file_get_contents(SHOPPING_NAME_CACHE_PATH), true) ?: []) : []; - $nameTotal = count($nameCache); - $estYearCalls += $nameTotal; $estYearIn += $nameTotal * 250; $estYearOut += $nameTotal * 40; - - // Merge: tracked data is authoritative; estimate fills the gap before tracking started - $monthIn = max((int)$cur['input_tokens'], $estMonthIn); - $monthOut = max((int)$cur['output_tokens'], $estMonthOut); - $monthCalls = max((int)$cur['calls'], $estMonthCalls); - $yearIn = max((int)$yearBucket['input_tokens'], $estYearIn); - $yearOut = max((int)$yearBucket['output_tokens'], $estYearOut); - $yearCalls = max((int)$yearBucket['calls'], $estYearCalls); // ── DB stats ────────────────────────────────────────────────────────────── $dbStats = []; @@ -270,22 +230,22 @@ if (($_GET['action'] ?? '') === 'gemini_usage') { 'month' => $month, 'year' => $year, - // Current month (tracked + estimated from cache) + // Current month (from ai_usage.json) 'month_stats' => [ - 'calls' => $monthCalls, - 'input_tokens' => $monthIn, - 'output_tokens'=> $monthOut, - 'cost_usd' => $calcCost($monthIn, $monthOut), + 'calls' => (int)$cur['calls'], + 'input_tokens' => (int)$cur['input_tokens'], + 'output_tokens'=> (int)$cur['output_tokens'], + 'cost_usd' => $calcCost((int)$cur['input_tokens'], (int)$cur['output_tokens']), 'by_action' => $cur['by_action'] ?? [], 'by_model' => $cur['by_model'] ?? [], ], - // Current year (tracked + estimated from cache) + // Current year (from ai_usage.json — all months summed) 'year_stats' => [ - 'calls' => $yearCalls, - 'input_tokens' => $yearIn, - 'output_tokens'=> $yearOut, - 'cost_usd' => $calcCost($yearIn, $yearOut), + 'calls' => (int)$yearBucket['calls'], + 'input_tokens' => (int)$yearBucket['input_tokens'], + 'output_tokens'=> (int)$yearBucket['output_tokens'], + 'cost_usd' => $calcCost((int)$yearBucket['input_tokens'], (int)$yearBucket['output_tokens']), ], // DB activity @@ -298,8 +258,8 @@ if (($_GET['action'] ?? '') === 'gemini_usage') { 'caches' => [ 'price' => count($priceCache), 'shelf' => count($shelfCache), - 'category' => $catTotal, - 'names' => $nameTotal, + 'category' => count($catCache), + 'names' => count($nameCache), 'foodfacts'=> count(file_exists(FOODFACTS_CACHE_PATH) ? (json_decode(file_get_contents(FOODFACTS_CACHE_PATH), true) ?: []) : []), ], diff --git a/assets/js/app.js b/assets/js/app.js index dd0bcab..fd0a3ac 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1052,7 +1052,8 @@ if (!_SUPPORTED_LANGS[_currentLang]) _currentLang = 'en'; try { const s = JSON.parse(localStorage.getItem('evershelf_settings') || '{}'); const mode = s.dark_mode || 'auto'; - const dark = mode === 'on' || (mode === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches); + const h = new Date().getHours(); + const dark = mode === 'on' || (mode === 'auto' && (h >= 20 || h < 7)); document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light'); } catch(e) {} })(); @@ -1176,7 +1177,9 @@ function _applyTheme() { } else if (mode === 'off') { isDark = false; } else { - isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + // auto: dark from 20:00 to 07:00 (time-based, not system preference) + const h = new Date().getHours(); + isDark = h >= 20 || h < 7; } document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light'); } @@ -1189,10 +1192,10 @@ function _setThemeMode(mode) { } // Listen to system theme changes (for 'auto' mode) -window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { - const s = getSettings(); - if ((s.dark_mode || 'auto') === 'auto') _applyTheme(); -}); +// Re-evaluate auto theme every 5 minutes (catches 20:00 dark / 07:00 light transitions) +setInterval(() => { + if ((getSettings().dark_mode || 'auto') === 'auto') _applyTheme(); +}, 5 * 60 * 1000); // ===== EXPORT INVENTORY ===== function exportInventory(format) { @@ -2343,12 +2346,6 @@ async function _renderInfoTab() { // ── System card ────────────────────────────────────────────────────── if (sysEl) { const db = d.db || {}; - const nowTs = Math.floor(Date.now()/1000); - const bringDays = d.bring_expires_ts ? Math.round((d.bring_expires_ts - nowTs)/86400) : null; - const bringColor = bringDays !== null && bringDays <= 3 ? '#dc2626' : ''; - const bringLabel = bringDays === null ? '—' - : bringDays <= 0 ? t('settings.info.bring_expired') - : t('settings.info.bring_days').replace('{n}', bringDays); const lvlColors = {DEBUG:'#1e40af//#dbeafe', INFO:'#15803d//#dcfce7', WARN:'#854d0e//#fef9c3', ERROR:'#991b1b//#fee2e2'}; const [lvlFg, lvlBg] = (lvlColors[d.log_level] || '#64748b//#f1f5f9').split('//'); @@ -2367,10 +2364,6 @@ async function _renderInfoTab() { ${t('settings.info.last_backup')} ${d.last_backup_ts ? fmtDate(d.last_backup_ts)+' · '+fmtBytes(d.last_backup_bytes) : '—'} - - Bring! - ${bringLabel} - `; } } catch(e) { diff --git a/data/ai_usage.json b/data/ai_usage.json new file mode 100644 index 0000000..9519927 --- /dev/null +++ b/data/ai_usage.json @@ -0,0 +1,9 @@ +{ + "2026-05": { + "input_tokens": 4438300, + "output_tokens": 1286760, + "calls": 8374, + "by_action": {}, + "by_model": {} + } +} \ No newline at end of file diff --git a/index.html b/index.html index 380b60e..c10fc75 100644 --- a/index.html +++ b/index.html @@ -831,7 +831,8 @@

⚙️ Configurazione

- + + @@ -839,13 +840,108 @@ -
+ +
+
+

🌐 Lingua / Language

+

Seleziona la lingua dell'interfaccia. Select the interface language.

+
+ +

La pagina verrà ricaricata per applicare la nuova lingua.

+
+
+
+

💱 Valuta

+

La valuta usata per tutti i costi e i prezzi nell'app.

+
+ +
+
+ +
+
+
+

🌙 Tema / Aspetto

+

Scegli il tema dell'interfaccia.

+
+ +
+
+
+

🌙 Salvaschermo

+

Mostra un orologio con fatti utili dopo un periodo di inattività. Di default è disattivato.

+
+ +
+
+ + +
+
+
+

♻️ Suggerimenti zero-waste

+

Durante la cottura, mostra consigli su come riutilizzare gli scarti prodotti in ogni passo (bucce, acqua di cottura, ecc.). Disattivo per impostazione predefinita.

+
+ +
+
+
+

📤 Esporta inventario

+

Scarica l'inventario corrente in CSV o apri una versione stampabile (PDF).

+
+ + +
+
+
-
+

🤖 Google Gemini AI

Chiave API per identificazione prodotti, scadenze e ricette.

@@ -1245,107 +1341,9 @@
-
-
-

🌐 Lingua / Language

-

Seleziona la lingua dell'interfaccia. Select the interface language.

-
- - -

La pagina verrà ricaricata per applicare la nuova lingua.

-
-
-
-

🌙 Salvaschermo

-

Mostra un orologio con fatti utili dopo un periodo di inattività. Di default è disattivato.

-
- -
-
- - -
-
-
-

🌙 Tema / Aspetto

-

Scegli il tema dell'interfaccia.

-
- - -
-
-
-

♻️ Suggerimenti zero-waste

-

Durante la cottura, mostra consigli su come riutilizzare gli scarti prodotti in ogni passo (bucce, acqua di cottura, ecc.). Disattivo per impostazione predefinita.

-
- -
-
-
-

📤 Esporta inventario

-

Scarica l'inventario corrente in CSV o apri una versione stampabile (PDF).

-
- - -
-
-
- -
-

💱 Valuta

-

La valuta usata per tutti i costi e i prezzi nell'app.

-
- -
-
- -
-

Gemini AI — Token Usage

@@ -1654,6 +1652,6 @@
- + diff --git a/translations/de.json b/translations/de.json index 62e08f3..4e0738e 100644 --- a/translations/de.json +++ b/translations/de.json @@ -755,7 +755,7 @@ "label": "🌙 Design", "off": "☀️ Hell", "on": "🌙 Dunkel", - "auto": "🔄 Automatisch (System)" + "auto": "🔄 Automatisch (Tageszeit)" }, "zerowaste": { "card_title": "♻️ Zero-Waste-Tipps", @@ -801,7 +801,8 @@ "year_label": "Jahr {year}", "currency_title": "Währung", "currency_hint": "Die Währung, die für alle Kosten und Preise in der App verwendet wird." - } + }, + "tab_general": "Allgemein" }, "expiry": { "today": "HEUTE", diff --git a/translations/en.json b/translations/en.json index d158184..4e79ea5 100644 --- a/translations/en.json +++ b/translations/en.json @@ -755,7 +755,7 @@ "label": "🌙 Theme", "off": "☀️ Light", "on": "🌙 Dark", - "auto": "🔄 Auto (system)" + "auto": "🔄 Automatic (time of day)" }, "zerowaste": { "card_title": "♻️ Zero-waste tips", @@ -801,7 +801,8 @@ "year_label": "Year {year}", "currency_title": "Currency", "currency_hint": "The currency used for all costs and prices in the app." - } + }, + "tab_general": "General" }, "expiry": { "today": "TODAY", diff --git a/translations/it.json b/translations/it.json index 9219a5a..e7f885f 100644 --- a/translations/it.json +++ b/translations/it.json @@ -755,7 +755,7 @@ "label": "🌙 Tema", "off": "☀️ Chiaro", "on": "🌙 Scuro", - "auto": "🔄 Automatico (sistema)" + "auto": "🔄 Automatico (orario)" }, "zerowaste": { "card_title": "♻️ Suggerimenti zero-waste", @@ -801,7 +801,8 @@ "year_label": "Anno {year}", "currency_title": "Valuta", "currency_hint": "La valuta usata per tutti i costi e i prezzi nell'app." - } + }, + "tab_general": "Generali" }, "expiry": { "today": "OGGI",