From 56e68b72f8c21222d9aafa958abf39d5e4f289e4 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Mon, 18 May 2026 06:45:56 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Info=20tab=20v3=20=E2=80=94=20clean=20m?= =?UTF-8?q?onth/year=20stats,=20currency=20to=20Info=20tab,=20Gemini=20cos?= =?UTF-8?q?ts=20from=20.env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .env: GEMINI_COST_* rates configurable (4 new vars, defaults to current Google pricing) - api/index.php: GEMINI_COST defines read from env() with fallback; added SHOPPING_NAME_CACHE_PATH - api/index.php: gemini_usage output — clean month_stats/year_stats (no tracked/retro split) updated token estimates: price 700/250, shelf 650/120, cat 280/40, shopping_name 250/40 added 'pricing' key to response (current rates); removed food_facts from estimate - index.html: currency selector moved from tab-api to tab-info as first card (global setting) - app.js: _renderInfoTab() rewritten — just month + year sections, no retro framing cost displayed in user's currency (price_currency) with expanded multi-currency conversion - translations: settings.info.currency_title/hint/year_label added; retro/tracked keys removed --- api/index.php | 211 +++++++++++++++++++++++-------------------- assets/js/app.js | 135 +++++++++++++-------------- index.html | 41 +++++---- translations/de.json | 9 +- translations/en.json | 9 +- translations/it.json | 9 +- 6 files changed, 214 insertions(+), 200 deletions(-) diff --git a/api/index.php b/api/index.php index a9bc7c0..d5e5c7b 100644 --- a/api/index.php +++ b/api/index.php @@ -16,19 +16,19 @@ define('_GH_TK_ENC', '23580718460c2c444031290243627e7971622b29030a3e4d50001e45261659420b6e110a423f30447133205b425a577971561f32762b0b034e0b3e56106d5945020406254a3a4647592a1a611c66687a0b672043700f34757900014004'); define('_GH_TK_KEY', 'D1sp3ns4!Ev3r#26'); define('GH_REPO', 'dadaloop82/EverShelf'); -define('PRICE_CACHE_PATH', __DIR__ . '/../data/shopping_price_cache.json'); -define('CATEGORY_CACHE_PATH', __DIR__ . '/../data/category_ai_cache.json'); -define('SHELF_CACHE_PATH', __DIR__ . '/../data/opened_shelf_cache.json'); -define('FOODFACTS_CACHE_PATH',__DIR__ . '/../data/food_facts_cache.json'); -define('BRING_TOKEN_PATH', __DIR__ . '/../data/bring_token.json'); -define('AI_USAGE_PATH', __DIR__ . '/../data/ai_usage.json'); -// Gemini pricing (USD per 1M tokens) — overridable via .env -// gemini-2.5-flash: $0.15 input / $0.60 output -// gemini-2.0-flash: $0.10 input / $0.40 output -define('GEMINI_COST_25F_IN', 0.15); -define('GEMINI_COST_25F_OUT', 0.60); -define('GEMINI_COST_20F_IN', 0.10); -define('GEMINI_COST_20F_OUT', 0.40); +define('PRICE_CACHE_PATH', __DIR__ . '/../data/shopping_price_cache.json'); +define('CATEGORY_CACHE_PATH', __DIR__ . '/../data/category_ai_cache.json'); +define('SHELF_CACHE_PATH', __DIR__ . '/../data/opened_shelf_cache.json'); +define('FOODFACTS_CACHE_PATH', __DIR__ . '/../data/food_facts_cache.json'); +define('SHOPPING_NAME_CACHE_PATH', __DIR__ . '/../data/shopping_name_cache.json'); +define('BRING_TOKEN_PATH', __DIR__ . '/../data/bring_token.json'); +define('AI_USAGE_PATH', __DIR__ . '/../data/ai_usage.json'); +// Gemini pricing (USD per 1M tokens) — configurable in .env (GEMINI_COST_25F_IN etc.) +// Defaults: gemini-2.5-flash $0.15/M in · $0.60/M out — gemini-2.0-flash $0.10/M in · $0.40/M out +define('GEMINI_COST_25F_IN', (float)(getenv('GEMINI_COST_25F_IN') ?: 0.15)); +define('GEMINI_COST_25F_OUT', (float)(getenv('GEMINI_COST_25F_OUT') ?: 0.60)); +define('GEMINI_COST_20F_IN', (float)(getenv('GEMINI_COST_20F_IN') ?: 0.10)); +define('GEMINI_COST_20F_OUT', (float)(getenv('GEMINI_COST_20F_OUT') ?: 0.40)); /** Decode the XOR-obfuscated GitHub token at runtime. */ function _ghToken(): string { @@ -151,84 +151,82 @@ if (($_GET['action'] ?? '') === 'gemini_usage') { header('Content-Type: application/json; charset=utf-8'); // ── Cost helper ─────────────────────────────────────────────────────────── - $calcCost = function(array $bucket): float { - $cost = 0.0; - foreach (($bucket['by_model'] ?? []) as $mdl => $mu) { - $inRate = str_contains($mdl, '2.5') ? GEMINI_COST_25F_IN : GEMINI_COST_20F_IN; - $outRate = str_contains($mdl, '2.5') ? GEMINI_COST_25F_OUT : GEMINI_COST_20F_OUT; - $cost += ($mu['in'] / 1_000_000) * $inRate + ($mu['out'] / 1_000_000) * $outRate; - } - if ($cost === 0.0 && ($bucket['input_tokens'] ?? 0) > 0) { - $cost = ($bucket['input_tokens'] / 1_000_000) * GEMINI_COST_25F_IN - + ($bucket['output_tokens'] ?? 0) / 1_000_000 * GEMINI_COST_25F_OUT; - } - return round($cost, 6); + $calcCost = function(int $tokIn, int $tokOut, string $modelHint = '2.5'): float { + $inRate = str_contains($modelHint, '2.5') ? GEMINI_COST_25F_IN : GEMINI_COST_20F_IN; + $outRate = str_contains($modelHint, '2.5') ? GEMINI_COST_25F_OUT : GEMINI_COST_20F_OUT; + return round(($tokIn / 1_000_000) * $inRate + ($tokOut / 1_000_000) * $outRate, 6); }; // ── Tracked usage (ai_usage.json) ──────────────────────────────────────── - $aiData = file_exists(AI_USAGE_PATH) ? (json_decode(file_get_contents(AI_USAGE_PATH), true) ?: []) : []; - $month = date('Y-m'); - $year = date('Y'); - $cur = $aiData[$month] ?? ['input_tokens' => 0, 'output_tokens' => 0, 'calls' => 0, 'by_action' => [], 'by_model' => []]; + $aiData = file_exists(AI_USAGE_PATH) ? (json_decode(file_get_contents(AI_USAGE_PATH), true) ?: []) : []; + $month = date('Y-m'); + $year = date('Y'); + $cur = $aiData[$month] ?? ['input_tokens' => 0, 'output_tokens' => 0, 'calls' => 0, 'by_action' => [], 'by_model' => []]; - // Yearly totals (sum all months of current year) + // Yearly tracked totals $yearBucket = ['input_tokens' => 0, 'output_tokens' => 0, 'calls' => 0, 'by_model' => []]; foreach ($aiData as $k => $v) { if (!str_starts_with($k, $year)) continue; - $yearBucket['input_tokens'] += (int)($v['input_tokens'] ?? 0); + $yearBucket['input_tokens'] += (int)($v['input_tokens'] ?? 0); $yearBucket['output_tokens'] += (int)($v['output_tokens'] ?? 0); $yearBucket['calls'] += (int)($v['calls'] ?? 0); foreach (($v['by_model'] ?? []) as $mdl => $mu) { - if (!isset($yearBucket['by_model'][$mdl])) { - $yearBucket['by_model'][$mdl] = ['in' => 0, 'out' => 0, 'calls' => 0]; - } + if (!isset($yearBucket['by_model'][$mdl])) $yearBucket['by_model'][$mdl] = ['in' => 0, 'out' => 0, 'calls' => 0]; $yearBucket['by_model'][$mdl]['in'] += $mu['in'] ?? 0; $yearBucket['by_model'][$mdl]['out'] += $mu['out'] ?? 0; $yearBucket['by_model'][$mdl]['calls'] += $mu['calls'] ?? 0; } } - // ── Retroactive estimate (from cache files — before tracking started) ──── - // Token averages per action type (empirical estimates): - // price lookup : ~350 in + ~125 out - // category : ~200 in + ~30 out - // shelf life : ~500 in + ~80 out + // ── 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')); - $retroMonthCalls = 0; $retroYearCalls = 0; - $retroMonthIn = 0; $retroYearIn = 0; - $retroMonthOut = 0; $retroYearOut = 0; + $estMonthIn = 0; $estMonthOut = 0; $estMonthCalls = 0; + $estYearIn = 0; $estYearOut = 0; $estYearCalls = 0; // Price cache $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) { $retroMonthCalls++; $retroMonthIn += 350; $retroMonthOut += 125; } - if ($v['cached_at'] >= $yearStart) { $retroYearCalls++; $retroYearIn += 350; $retroYearOut += 125; } + 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) + // 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) { $retroMonthCalls++; $retroMonthIn += 500; $retroMonthOut += 80; } - if (($v['ts'] ?? 0) >= $yearStart) { $retroYearCalls++; $retroYearIn += 500; $retroYearOut += 80; } + 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 as year only) + // Category AI cache (no timestamps — count all as year) $catCache = file_exists(CATEGORY_CACHE_PATH) ? (json_decode(file_get_contents(CATEGORY_CACHE_PATH), true) ?: []) : []; $catTotal = count($catCache); - $retroYearCalls += $catTotal; $retroYearIn += $catTotal * 200; $retroYearOut += $catTotal * 30; + $estYearCalls += $catTotal; $estYearIn += $catTotal * 280; $estYearOut += $catTotal * 40; + // Shopping name cache (no timestamps — count all as year) + $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; - $retroMonthCostUsd = round(($retroMonthIn / 1_000_000) * GEMINI_COST_25F_IN - + ($retroMonthOut / 1_000_000) * GEMINI_COST_25F_OUT, 6); - $retroYearCostUsd = round(($retroYearIn / 1_000_000) * GEMINI_COST_25F_IN - + ($retroYearOut / 1_000_000) * GEMINI_COST_25F_OUT, 6); - - // Only expose retro if there is actual tracked data gap (calls tracked < retro estimate) - $hasRetro = ($retroMonthCalls > $cur['calls']); + // 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 = []; @@ -257,65 +255,61 @@ if (($_GET['action'] ?? '') === 'gemini_usage') { } // ── Backup info ─────────────────────────────────────────────────────────── - $backupDir = dirname(__DIR__) . '/data/backups'; + $backupDir = dirname(__DIR__) . '/data/backups'; $backupFiles = is_dir($backupDir) ? (glob($backupDir . '/*.db') ?: []) : []; rsort($backupFiles); $lastBackupTs = $backupFiles ? (int)filemtime($backupFiles[0]) : 0; $lastBackupBytes = $backupFiles ? (int)filesize($backupFiles[0]) : 0; // ── Bring! token expiry ─────────────────────────────────────────────────── - $bringToken = file_exists(BRING_TOKEN_PATH) - ? (json_decode(file_get_contents(BRING_TOKEN_PATH), true) ?: []) : []; $bringExpiresTs = (int)($bringToken['expires'] ?? 0); + $bringToken = file_exists(BRING_TOKEN_PATH) + ? (json_decode(file_get_contents(BRING_TOKEN_PATH), true) ?: []) : []; + $bringExpiresTs = (int)($bringToken['expires'] ?? 0); echo json_encode([ 'month' => $month, 'year' => $year, - // Tracked (ai_usage.json — since tracking start) - 'tracked' => [ - 'calls' => (int)$cur['calls'], - 'input_tokens' => (int)$cur['input_tokens'], - 'output_tokens' => (int)$cur['output_tokens'], - 'cost_usd' => $calcCost($cur), - 'by_action' => $cur['by_action'] ?? [], - 'by_model' => $cur['by_model'] ?? [], + // Current month (tracked + estimated from cache) + 'month_stats' => [ + 'calls' => $monthCalls, + 'input_tokens' => $monthIn, + 'output_tokens'=> $monthOut, + 'cost_usd' => $calcCost($monthIn, $monthOut), + 'by_action' => $cur['by_action'] ?? [], + 'by_model' => $cur['by_model'] ?? [], ], - // Yearly tracked totals - 'year_tracked' => [ - 'calls' => (int)$yearBucket['calls'], - 'input_tokens' => (int)$yearBucket['input_tokens'], - 'output_tokens' => (int)$yearBucket['output_tokens'], - 'cost_usd' => $calcCost($yearBucket), + // Current year (tracked + estimated from cache) + 'year_stats' => [ + 'calls' => $yearCalls, + 'input_tokens' => $yearIn, + 'output_tokens'=> $yearOut, + 'cost_usd' => $calcCost($yearIn, $yearOut), ], - // Retroactive estimate from cache files (before tracking started) - 'retro' => $hasRetro ? [ - 'calls_month' => $retroMonthCalls, - 'calls_year' => $retroYearCalls, - 'tok_in_month' => $retroMonthIn, - 'tok_out_month'=> $retroMonthOut, - 'tok_in_year' => $retroYearIn, - 'tok_out_year' => $retroYearOut, - 'cost_month' => $retroMonthCostUsd, - 'cost_year' => $retroYearCostUsd, - ] : null, - // DB activity 'db' => array_merge( array_map('intval', $dbStats), ['bytes' => file_exists(DB_PATH) ? (int)filesize(DB_PATH) : 0] ), - // Cache sizes + // Cache item counts 'caches' => [ - 'price' => count($priceCache), - 'shelf' => count($shelfCache), - 'category' => $catTotal, - 'foodfacts' => count(file_exists(FOODFACTS_CACHE_PATH) + 'price' => count($priceCache), + 'shelf' => count($shelfCache), + 'category' => $catTotal, + 'names' => $nameTotal, + 'foodfacts'=> count(file_exists(FOODFACTS_CACHE_PATH) ? (json_decode(file_get_contents(FOODFACTS_CACHE_PATH), true) ?: []) : []), ], + // Current Gemini pricing (from .env / defaults) + 'pricing' => [ + '2.5-flash' => ['in' => GEMINI_COST_25F_IN, 'out' => GEMINI_COST_25F_OUT], + '2.0-flash' => ['in' => GEMINI_COST_20F_IN, 'out' => GEMINI_COST_20F_OUT], + ], + // System 'log_bytes' => $logBytes, 'log_level' => EverLog::levelName(), @@ -324,18 +318,43 @@ if (($_GET['action'] ?? '') === 'gemini_usage') { 'last_backup_bytes' => $lastBackupBytes, 'bring_expires_ts' => $bringExpiresTs, - // History (last 12 months) + // History (last 13 months for trend) 'history' => array_map(fn($k, $v) => [ - 'month' => $k, - 'input_tokens' => (int)($v['input_tokens'] ?? 0), - 'output_tokens' => (int)($v['output_tokens'] ?? 0), - 'calls' => (int)($v['calls'] ?? 0), - 'cost_usd' => $calcCost($v), + 'month' => $k, + 'input_tokens' => (int)($v['input_tokens'] ?? 0), + 'output_tokens'=> (int)($v['output_tokens'] ?? 0), + 'calls' => (int)($v['calls'] ?? 0), + 'cost_usd' => $calcCost((int)($v['input_tokens'] ?? 0), (int)($v['output_tokens'] ?? 0)), ], array_keys($aiData), array_values($aiData)), ], JSON_UNESCAPED_UNICODE); exit; } +// ── Health check — startup diagnostic (no rate-limit, no auth required) ────── + + // ── Tracked usage (ai_usage.json) ──────────────────────────────────────── + $aiData = file_exists(AI_USAGE_PATH) ? (json_decode(file_get_contents(AI_USAGE_PATH), true) ?: []) : []; + $month = date('Y-m'); + $year = date('Y'); + $cur = $aiData[$month] ?? ['input_tokens' => 0, 'output_tokens' => 0, 'calls' => 0, 'by_action' => [], 'by_model' => []]; + + // Yearly totals (sum all 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; + $yearBucket['input_tokens'] += (int)($v['input_tokens'] ?? 0); + $yearBucket['output_tokens'] += (int)($v['output_tokens'] ?? 0); + $yearBucket['calls'] += (int)($v['calls'] ?? 0); + foreach (($v['by_model'] ?? []) as $mdl => $mu) { + if (!isset($yearBucket['by_model'][$mdl])) { + $yearBucket['by_model'][$mdl] = ['in' => 0, 'out' => 0, 'calls' => 0]; + } + $yearBucket['by_model'][$mdl]['in'] += $mu['in'] ?? 0; + $yearBucket['by_model'][$mdl]['out'] += $mu['out'] ?? 0; + $yearBucket['by_model'][$mdl]['calls'] += $mu['calls'] ?? 0; + } + } + // ── Health check — startup diagnostic (no rate-limit, no auth required) ────── if (($_GET['action'] ?? '') === 'health_check') { $checks = []; diff --git a/assets/js/app.js b/assets/js/app.js index 4819d71..dd0bcab 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -2233,16 +2233,27 @@ async function _renderInfoTab() { const monthLabel = new Intl.DateTimeFormat(locale, {month:'long', year:'numeric'}) .format(new Date(parseInt(yr), parseInt(mo)-1, 1)); - // Cost → display currency + // Cost → user currency const toCurr = (usd) => { if (!usd) return '—'; - const c = s.price_currency; - const v = c === 'EUR' ? usd * 0.92 : c === 'GBP' ? usd * 0.79 : usd; - const sym = c === 'EUR' ? '€' : c === 'GBP' ? '£' : '$'; - return sym + v.toFixed(4); + const c = s.price_currency || 'EUR'; + let v = usd, sym = '$'; + if (c === 'EUR') { v = usd * 0.92; sym = '€'; } + else if (c === 'GBP') { v = usd * 0.79; sym = '£'; } + else if (c === 'CHF') { v = usd * 0.90; sym = 'CHF '; } + else if (c === 'CAD') { v = usd * 1.36; sym = 'CA$'; } + else if (c === 'AUD') { v = usd * 1.54; sym = 'A$'; } + else if (c === 'BRL') { v = usd * 5.20; sym = 'R$'; } + else if (c === 'JPY') { v = usd * 155; sym = '¥'; } + else if (c === 'SEK') { v = usd * 10.4; sym = 'kr'; } + else if (c === 'NOK') { v = usd * 10.6; sym = 'kr'; } + else if (c === 'DKK') { v = usd * 6.85; sym = 'kr'; } + else if (c === 'PLN') { v = usd * 3.98; sym = 'zł'; } + const decimals = (c === 'JPY') ? 1 : 4; + return sym + v.toFixed(decimals); }; - const fmtTok = n => n >= 1_000_000 ? (n/1_000_000).toFixed(2)+'M' - : n >= 1_000 ? Math.round(n/1_000)+'K' : String(n||0); + const fmtTok = n => n >= 1_000_000 ? (n/1_000_000).toFixed(2)+'M' + : n >= 1_000 ? Math.round(n/1_000)+'K' : String(n||0); const fmtBytes = b => b > 1048576 ? (b/1048576).toFixed(1)+' MB' : b > 1024 ? Math.round(b/1024)+' KB' : (b||0)+' B'; const fmtDate = ts => ts ? new Intl.DateTimeFormat(locale, {day:'2-digit', month:'short', hour:'2-digit', minute:'2-digit'}).format(new Date(ts*1000)) : '—'; @@ -2251,71 +2262,53 @@ async function _renderInfoTab() {
${val}
${label}
`; + const sectionHeader = (label) => + `
${label}
`; // ── AI Usage card ──────────────────────────────────────────────────── if (aiEl) { - const tr = d.tracked || {}; - const retro = d.retro; - const yr_t = d.year_tracked || {}; + const ms = d.month_stats || {}; + const ys = d.year_stats || {}; - // Update card subtitle dynamically const hintEl = aiEl.closest('.settings-card')?.querySelector('.info-ai-subtitle'); - if (hintEl) hintEl.textContent = t('settings.info.ai_overview').replace('{month}', monthLabel); + if (hintEl) hintEl.textContent = t('settings.info.ai_overview'); - // Tracked section - let trackedHtml = ''; - if (tr.calls > 0) { - const actionRows = Object.entries(tr.by_action || {}) - .sort((a,b) => b[1]-a[1]).slice(0, 8) - .map(([k,v]) => `${k}${v} ${t('settings.info.calls_unit')}`).join(''); - const modelRows = Object.entries(tr.by_model || {}) - .map(([m,mv]) => `${m}${fmtTok((mv.in||0)+(mv.out||0))}`).join(''); - trackedHtml = ` -
-
${t('settings.info.tracked_section')}
-
- ${pill(tr.calls, t('settings.info.ai_calls'))} - ${pill(fmtTok((tr.input_tokens||0)+(tr.output_tokens||0)), t('settings.info.total_tokens'))} - ${pill(toCurr(tr.cost_usd), t('settings.info.est_cost'), '#15803d')} -
- ${actionRows ? `
${t('settings.info.by_action')}${actionRows}
` : ''} - ${modelRows ? `
${t('settings.info.by_model')}${modelRows}
` : ''} -
`; - } + const msIn = ms.input_tokens || 0; + const msOut = ms.output_tokens || 0; + const ysIn = ys.input_tokens || 0; + const ysOut = ys.output_tokens || 0; - // Retroactive estimate section - let retroHtml = ''; - if (retro && retro.calls_month > 0) { - retroHtml = ` -
-
- ${t('settings.info.retro_section').replace('{month}', monthLabel)} -
-
- ${pill('~'+retro.calls_month, t('settings.info.ai_calls'))} - ${pill('~'+fmtTok((retro.tok_in_month||0)+(retro.tok_out_month||0)), t('settings.info.total_tokens'))} - ${pill('~'+toCurr(retro.cost_month), t('settings.info.est_cost'), '#92400e')} -
-

${t('settings.info.retro_note')}

-
`; - } + // Month section + const actionRows = Object.entries(ms.by_action || {}) + .sort((a,b) => b[1]-a[1]).slice(0, 8) + .map(([k,v]) => `${k}${v} ${t('settings.info.calls_unit')}`).join(''); + const modelRows = Object.entries(ms.by_model || {}) + .map(([m,mv]) => `${m}${fmtTok((mv.in||0)+(mv.out||0))}`).join(''); - // Yearly section - const yearTotCalls = (yr_t.calls||0) + (retro?.calls_year||0); - const yearTotTokIn = (yr_t.input_tokens||0) + (retro?.tok_in_year||0); - const yearTotTokOut = (yr_t.output_tokens||0) + (retro?.tok_out_year||0); - const yearTotCost = (yr_t.cost_usd||0) + (retro?.cost_year||0); - const yearHtml = ` -
-
${t('settings.info.year_section').replace('{year}', d.year)}
+ const monthHtml = ` +
+ ${sectionHeader(monthLabel)}
- ${pill('~'+yearTotCalls, t('settings.info.ai_calls'))} - ${pill('~'+fmtTok(yearTotTokIn+yearTotTokOut), t('settings.info.total_tokens'))} - ${pill('~'+toCurr(yearTotCost), t('settings.info.est_cost'), '#15803d')} + ${pill(ms.calls || 0, t('settings.info.ai_calls'))} + ${pill('~'+fmtTok(msIn+msOut), t('settings.info.total_tokens'))} + ${pill('~'+toCurr(ms.cost_usd), t('settings.info.est_cost'), '#15803d')} +
+ ${actionRows ? `
${t('settings.info.by_action')}${actionRows}
` : ''} + ${modelRows ? `
${t('settings.info.by_model')}${modelRows}
` : ''} +
`; + + // Year section + const yearHtml = ` +
+ ${sectionHeader(t('settings.info.year_label').replace('{year}', d.year))} +
+ ${pill('~'+(ys.calls || 0), t('settings.info.ai_calls'))} + ${pill('~'+fmtTok(ysIn+ysOut), t('settings.info.total_tokens'))} + ${pill('~'+toCurr(ys.cost_usd), t('settings.info.est_cost'), '#15803d')}
`; - aiEl.innerHTML = trackedHtml + retroHtml + yearHtml + aiEl.innerHTML = monthHtml + yearHtml + `

${t('settings.info.pricing_note')}

`; } @@ -2323,14 +2316,12 @@ async function _renderInfoTab() { const invEl = document.getElementById('info-inv-content'); if (invEl && d.db) { const db = d.db; - const expColor = db.expired > 0 ? '#dc2626' : ''; - const soonColor = db.expiring_soon > 0 ? '#d97706' : ''; invEl.innerHTML = `
${pill(db.inventory_active, t('settings.info.inv_active'))} ${pill(db.products_total, t('settings.info.inv_products'))} - ${pill(db.expiring_soon, t('settings.info.inv_expiring'), soonColor)} - ${pill(db.expired, t('settings.info.inv_expired'), expColor)} + ${pill(db.expiring_soon, t('settings.info.inv_expiring'), db.expiring_soon > 0 ? '#d97706' : '')} + ${pill(db.expired, t('settings.info.inv_expired'), db.expired > 0 ? '#dc2626' : '')} ${pill(db.finished, t('settings.info.inv_finished'))}
`; } @@ -2341,11 +2332,11 @@ async function _renderInfoTab() { const db = d.db; actEl.innerHTML = `
- ${pill(db.tx_month, t('settings.info.act_tx_month'))} - ${pill(db.restock_month, t('settings.info.act_restock'))} - ${pill(db.use_month, t('settings.info.act_use'))} - ${pill(db.products_month, t('settings.info.act_new_products'))} - ${pill(db.tx_year, t('settings.info.act_tx_year'))} + ${pill(db.tx_month, t('settings.info.act_tx_month'))} + ${pill(db.restock_month, t('settings.info.act_restock'))} + ${pill(db.use_month, t('settings.info.act_use'))} + ${pill(db.products_month, t('settings.info.act_new_products'))} + ${pill(db.tx_year, t('settings.info.act_tx_year'))}
`; } @@ -2353,18 +2344,17 @@ async function _renderInfoTab() { 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 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('//'); sysEl.innerHTML = `
- ${pill(fmtBytes(db.bytes), t('settings.info.db_size'))} + ${pill(fmtBytes(db.bytes), t('settings.info.db_size'))} ${pill(fmtBytes(d.log_bytes), t('settings.info.log_size'))} ${pill(`${d.log_level||'INFO'}`, t('settings.info.log_level'))}
@@ -2391,6 +2381,7 @@ async function _renderInfoTab() { } } + /** * Populate the About section with the current app version from the server. */ diff --git a/index.html b/index.html index fc5cecd..380b60e 100644 --- a/index.html +++ b/index.html @@ -904,23 +904,6 @@
-
- - -
@@ -1339,6 +1322,30 @@
+ +
+

💱 Valuta

+

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

+
+ +
+
+ +
+

Gemini AI — Token Usage

diff --git a/translations/de.json b/translations/de.json index 2441e8b..62e08f3 100644 --- a/translations/de.json +++ b/translations/de.json @@ -780,10 +780,6 @@ "log_size": "Protokolle", "log_level": "Log-Level", "ai_overview": "KI-Nutzungsübersicht, Inventar und Systemstatus", - "tracked_section": "Erfasst (seit Monitoring-Start)", - "retro_section": "Rückwirkende Schätzung — {month}", - "retro_note": "Schätzung basiert auf KI-Cache-Dateien, die vor der Monitoring-Aktivierung generiert wurden.", - "year_section": "Jahr {year} — Gesamtschätzung", "calls_unit": "Aufrufe", "inv_title": "Inventar", "inv_active": "Aktiv", @@ -801,7 +797,10 @@ "cache_entries": "Produkte", "last_backup": "Letztes Backup", "bring_days": "Token läuft in {n} Tagen ab", - "bring_expired": "Token abgelaufen" + "bring_expired": "Token abgelaufen", + "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." } }, "expiry": { diff --git a/translations/en.json b/translations/en.json index 25574bd..d158184 100644 --- a/translations/en.json +++ b/translations/en.json @@ -780,10 +780,6 @@ "log_size": "Logs", "log_level": "Log level", "ai_overview": "AI usage overview, inventory and system status", - "tracked_section": "Tracked (since monitoring start)", - "retro_section": "Retroactive estimate — {month}", - "retro_note": "Estimate based on AI cache files generated before monitoring was activated.", - "year_section": "Year {year} — total estimate", "calls_unit": "calls", "inv_title": "Inventory", "inv_active": "Active", @@ -801,7 +797,10 @@ "cache_entries": "products", "last_backup": "Last backup", "bring_days": "token expires in {n} days", - "bring_expired": "token expired" + "bring_expired": "token expired", + "year_label": "Year {year}", + "currency_title": "Currency", + "currency_hint": "The currency used for all costs and prices in the app." } }, "expiry": { diff --git a/translations/it.json b/translations/it.json index 1294aba..9219a5a 100644 --- a/translations/it.json +++ b/translations/it.json @@ -780,10 +780,6 @@ "log_size": "Log", "log_level": "Livello log", "ai_overview": "Prospetto utilizzo AI, inventario e stato del sistema", - "tracked_section": "Tracciato (da inizio monitoraggio)", - "retro_section": "Stima retroattiva — {month}", - "retro_note": "Stima basata sulle voci presenti nei file di cache AI, generati prima dell'attivazione del monitoraggio.", - "year_section": "Anno {year} — totale stimato", "calls_unit": "call", "inv_title": "Inventario", "inv_active": "Attivi", @@ -801,7 +797,10 @@ "cache_entries": "prodotti", "last_backup": "Ultimo backup", "bring_days": "token scade tra {n} giorni", - "bring_expired": "token scaduto" + "bring_expired": "token scaduto", + "year_label": "Anno {year}", + "currency_title": "Valuta", + "currency_hint": "La valuta usata per tutti i costi e i prezzi nell'app." } }, "expiry": {