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() {
${t('settings.info.retro_note')}
-${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 = `La valuta usata per tutti i costi e i prezzi nell'app.
+