diff --git a/api/index.php b/api/index.php index 9eae095..a9bc7c0 100644 --- a/api/index.php +++ b/api/index.php @@ -18,6 +18,9 @@ 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 @@ -146,49 +149,189 @@ if (($_GET['action'] ?? '') === 'get_logs') { // ── Gemini token usage + cost estimate ──────────────────────────────────────── if (($_GET['action'] ?? '') === 'gemini_usage') { header('Content-Type: application/json; charset=utf-8'); - $data = file_exists(AI_USAGE_PATH) ? (json_decode(file_get_contents(AI_USAGE_PATH), true) ?: []) : []; - $month = date('Y-m'); - $cur = $data[$month] ?? ['input_tokens' => 0, 'output_tokens' => 0, 'calls' => 0, 'by_action' => [], 'by_model' => []]; - // Per-model cost calculation - $totalCost = 0.0; - foreach (($cur['by_model'] ?? []) as $mdl => $mu) { - $inRate = str_contains($mdl, '2.5') ? (float)(env('GEMINI_COST_INPUT_PER_1M') ?: GEMINI_COST_25F_IN) : (float)(env('GEMINI_COST_INPUT_PER_1M') ?: GEMINI_COST_20F_IN); - $outRate = str_contains($mdl, '2.5') ? (float)(env('GEMINI_COST_OUTPUT_PER_1M') ?: GEMINI_COST_25F_OUT) : (float)(env('GEMINI_COST_OUTPUT_PER_1M') ?: GEMINI_COST_20F_OUT); - $totalCost += ($mu['in'] / 1_000_000) * $inRate + ($mu['out'] / 1_000_000) * $outRate; - } - // Fallback if by_model not populated (old data) - if ($totalCost === 0.0 && ($cur['input_tokens'] > 0 || $cur['output_tokens'] > 0)) { - $inRate = (float)(env('GEMINI_COST_INPUT_PER_1M') ?: GEMINI_COST_25F_IN); - $outRate = (float)(env('GEMINI_COST_OUTPUT_PER_1M') ?: GEMINI_COST_25F_OUT); - $totalCost = ($cur['input_tokens'] / 1_000_000) * $inRate + ($cur['output_tokens'] / 1_000_000) * $outRate; + // ── 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); + }; + + // ── 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; + } } - // Log sizes — EverLog::listFiles() returns [{file, size_kb, mtime}, ...] + // ── 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 + $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; + + // 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; } + } + // Shelf-life AI cache (source='ai' only) + $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; } + } + // Category AI cache (no timestamps — count as year only) + $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; + + $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']); + + // ── DB stats ────────────────────────────────────────────────────────────── + $dbStats = []; + try { + $db = getDB(); + $row = $db->query("SELECT + (SELECT COUNT(*) FROM products) as products_total, + (SELECT COUNT(*) FROM inventory WHERE quantity > 0) as inventory_active, + (SELECT COUNT(*) FROM transactions WHERE undone=0 AND created_at >= date('now','start of month')) as tx_month, + (SELECT COUNT(*) FROM transactions WHERE undone=0 AND created_at >= date('now','start of year')) as tx_year, + (SELECT COUNT(*) FROM transactions WHERE type='in' AND undone=0 AND created_at >= date('now','start of month')) as restock_month, + (SELECT COUNT(*) FROM transactions WHERE type IN ('out','waste') AND undone=0 AND created_at >= date('now','start of month')) as use_month, + (SELECT COUNT(*) FROM products WHERE created_at >= date('now','start of month')) as products_month, + (SELECT COUNT(CASE WHEN expiry_date < date('now') AND quantity > 0 THEN 1 END) FROM inventory) as expired, + (SELECT COUNT(CASE WHEN expiry_date BETWEEN date('now') AND date('now','+7 days') AND quantity > 0 THEN 1 END) FROM inventory) as expiring_soon, + (SELECT COUNT(CASE WHEN quantity = 0 THEN 1 END) FROM inventory) as finished + ")->fetch(PDO::FETCH_ASSOC); + $dbStats = $row ?: []; + } catch (Throwable $e) { /* ignore */ } + + // ── Log info ────────────────────────────────────────────────────────────── $logFilesInfo = EverLog::listFiles(); $logBytes = 0; foreach ($logFilesInfo as $lf) { $logBytes += (int)(($lf['size_kb'] ?? 0) * 1024); } + // ── Backup info ─────────────────────────────────────────────────────────── + $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); + echo json_encode([ - 'month' => $month, - 'input_tokens' => (int)$cur['input_tokens'], - 'output_tokens' => (int)$cur['output_tokens'], - 'calls' => (int)$cur['calls'], - 'by_action' => $cur['by_action'] ?? [], - 'by_model' => $cur['by_model'] ?? [], - 'cost_usd' => round($totalCost, 6), - 'log_bytes' => $logBytes, - 'log_level' => EverLog::levelName(), - 'log_files' => count($logFilesInfo), - 'db_bytes' => file_exists(DB_PATH) ? filesize(DB_PATH) : 0, - 'history' => array_map(fn($k, $v) => [ + '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'] ?? [], + ], + + // 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), + ], + + // 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 + 'caches' => [ + 'price' => count($priceCache), + 'shelf' => count($shelfCache), + 'category' => $catTotal, + 'foodfacts' => count(file_exists(FOODFACTS_CACHE_PATH) + ? (json_decode(file_get_contents(FOODFACTS_CACHE_PATH), true) ?: []) : []), + ], + + // System + 'log_bytes' => $logBytes, + 'log_level' => EverLog::levelName(), + 'log_files' => count($logFilesInfo), + 'last_backup_ts' => $lastBackupTs, + 'last_backup_bytes' => $lastBackupBytes, + 'bring_expires_ts' => $bringExpiresTs, + + // History (last 12 months) + '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), - ], array_keys($data), array_values($data)), + 'cost_usd' => $calcCost($v), + ], array_keys($aiData), array_values($aiData)), ], JSON_UNESCAPED_UNICODE); exit; } diff --git a/assets/js/app.js b/assets/js/app.js index df4602a..4819d71 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -2225,79 +2225,169 @@ async function _renderInfoTab() { try { const d = await api('gemini_usage'); const s = getSettings(); - const sym = s.price_currency === 'USD' ? '$' : (s.price_currency === 'GBP' ? '£' : '€'); + + // ── Locale & helpers ───────────────────────────────────────────────── + const langMap = {it:'it-IT', en:'en-US', de:'de-DE', fr:'fr-FR', es:'es-ES'}; + const locale = langMap[s.language] || langMap[navigator.language?.slice(0,2)] || 'it-IT'; + const [yr, mo] = (d.month || '').split('-'); + const monthLabel = new Intl.DateTimeFormat(locale, {month:'long', year:'numeric'}) + .format(new Date(parseInt(yr), parseInt(mo)-1, 1)); + + // Cost → display 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 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)) : '—'; + const pill = (val, label, color='') => + `
${t('settings.info.retro_note')}
+${t('settings.info.pricing_note')}
- `; +${t('settings.info.pricing_note')}
`; + } + + // ── Inventory card ─────────────────────────────────────────────────── + 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 = ` +| ${t('settings.info.db_size')} | -${dbMb} MB | +
| ${t('settings.info.price_cache')} | +${(d.caches?.price||0)} ${t('settings.info.cache_entries')} |
| ${t('settings.info.log_size')} | -${logMb} MB (${d.log_files||0} files) | +
| ${t('settings.info.last_backup')} | +${d.last_backup_ts ? fmtDate(d.last_backup_ts)+' · '+fmtBytes(d.last_backup_bytes) : '—'} |
| ${t('settings.info.log_level')} | -- ${d.log_level||'INFO'} - | +
| Bring! | +${bringLabel} |
${t('error.generic')}
`; - if (sysEl) sysEl.innerHTML = `${t('error.generic')}
`; + ['info-ai-content','info-inv-content','info-act-content','info-system-content'].forEach(id => { + const el = document.getElementById(id); + if (el) el.innerHTML = `${t('error.generic')}
`; + }); } } diff --git a/index.html b/index.html index f00b1aa..fc5cecd 100644 --- a/index.html +++ b/index.html @@ -1342,16 +1342,30 @@Monthly consumption and estimated cost for the current API key.
+Utilizzo AI, inventario e sistema
Loading…
+Caricamento…
+Caricamento…
+Caricamento…
Loading…
+Caricamento…