feat: Info tab enriched — retroactive AI estimate, annual totals, inventory & activity stats
api/index.php: - gemini_usage: retroactive AI call estimate from cache files (price/shelf/category) with per-entry token estimates (price ~475tok, shelf ~580tok, category ~230tok) - yearly totals: sum tracked months + retro estimate for full 2026 view - DB activity stats: products, inventory, transactions, expired, expiring_soon - cache stats: price (255), shelf (30), category (7), foodfacts (10) - system info: last backup timestamp+size, Bring! token expiry - new constants: SHELF_CACHE_PATH, FOODFACTS_CACHE_PATH, BRING_TOKEN_PATH assets/js/app.js: - _renderInfoTab(): full rewrite — 4 cards (AI, Inventory, Activity, System) - month displayed as localized name via Intl.DateTimeFormat (es. 'maggio 2026') - tracked section shown when calls > 0; retro estimate always shown if gap exists - year section: tracked + retro combined total - pill() helper for consistent stat display index.html: 4 cards with ids info-ai-content, info-inv-content, info-act-content, info-system-content translations: updated settings.info.* keys in it/en/de (overview subtitle, retro labels, inv/act/system keys)
This commit is contained in:
+172
-29
@@ -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;
|
||||
}
|
||||
|
||||
+145
-55
@@ -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='') =>
|
||||
`<div style="background:var(--bg-secondary);border:1px solid var(--border-color,#e2e8f0);border-radius:10px;padding:8px 14px;min-width:70px;text-align:center${color ? ';border-color:'+color : ''}">
|
||||
<div style="font-size:1.1rem;font-weight:700;color:${color||'var(--text-primary,#1e293b)'}">${val}</div>
|
||||
<div style="font-size:0.7rem;color:var(--text-secondary,#64748b);margin-top:2px">${label}</div>
|
||||
</div>`;
|
||||
|
||||
// ── AI Usage card ────────────────────────────────────────────────────
|
||||
if (aiEl) {
|
||||
const totalTok = (d.input_tokens || 0) + (d.output_tokens || 0);
|
||||
const costUsd = d.cost_usd || 0;
|
||||
const tr = d.tracked || {};
|
||||
const retro = d.retro;
|
||||
const yr_t = d.year_tracked || {};
|
||||
|
||||
// Convert cost to display currency (rough fixed rates)
|
||||
let costDisplay = '$' + costUsd.toFixed(4);
|
||||
if (s.price_currency === 'EUR') costDisplay = '€' + (costUsd * 0.92).toFixed(4);
|
||||
if (s.price_currency === 'GBP') costDisplay = '£' + (costUsd * 0.79).toFixed(4);
|
||||
// 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);
|
||||
|
||||
// By-action breakdown
|
||||
const actions = d.by_action || {};
|
||||
const actionRows = Object.entries(actions)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([k, v]) => `<tr><td style="padding:2px 8px 2px 0;color:var(--text-secondary);font-size:0.82rem">${k}</td><td style="font-variant-numeric:tabular-nums;font-size:0.82rem">${v} calls</td></tr>`)
|
||||
.join('');
|
||||
// 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]) => `<tr><td style="padding:3px 12px 3px 0;color:var(--text-secondary);font-size:0.82rem">${k}</td><td style="font-variant-numeric:tabular-nums;font-size:0.82rem"><strong>${v}</strong> ${t('settings.info.calls_unit')}</td></tr>`).join('');
|
||||
const modelRows = Object.entries(tr.by_model || {})
|
||||
.map(([m,mv]) => `<tr><td style="padding:3px 12px 3px 0;color:var(--text-secondary);font-size:0.82rem">${m}</td><td style="font-variant-numeric:tabular-nums;font-size:0.82rem"><strong>${fmtTok((mv.in||0)+(mv.out||0))}</strong></td></tr>`).join('');
|
||||
trackedHtml = `
|
||||
<div style="margin-bottom:12px">
|
||||
<div style="font-size:0.78rem;font-weight:600;color:var(--text-secondary);margin-bottom:8px;text-transform:uppercase;letter-spacing:.04em">${t('settings.info.tracked_section')}</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
${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')}
|
||||
</div>
|
||||
${actionRows ? `<details style="margin-top:8px"><summary style="font-size:0.82rem;cursor:pointer;color:var(--text-secondary)">${t('settings.info.by_action')}</summary><table style="margin-top:6px;border-collapse:collapse">${actionRows}</table></details>` : ''}
|
||||
${modelRows ? `<details style="margin-top:4px"><summary style="font-size:0.82rem;cursor:pointer;color:var(--text-secondary)">${t('settings.info.by_model')}</summary><table style="margin-top:6px;border-collapse:collapse">${modelRows}</table></details>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// By-model breakdown
|
||||
const models = d.by_model || {};
|
||||
const modelRows = Object.entries(models)
|
||||
.map(([m, mv]) => `<tr><td style="padding:2px 8px 2px 0;color:var(--text-secondary);font-size:0.82rem">${m}</td><td style="font-variant-numeric:tabular-nums;font-size:0.82rem">${((mv.in||0)+(mv.out||0)).toLocaleString()} tok</td></tr>`)
|
||||
.join('');
|
||||
// Retroactive estimate section
|
||||
let retroHtml = '';
|
||||
if (retro && retro.calls_month > 0) {
|
||||
retroHtml = `
|
||||
<div style="background:var(--bg-secondary);border-radius:10px;padding:12px;margin-bottom:12px;border-left:3px solid #f59e0b">
|
||||
<div style="font-size:0.78rem;font-weight:600;color:#92400e;margin-bottom:8px;text-transform:uppercase;letter-spacing:.04em">
|
||||
${t('settings.info.retro_section').replace('{month}', monthLabel)}
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px">
|
||||
${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')}
|
||||
</div>
|
||||
<p style="font-size:0.75rem;color:#92400e;margin:0">${t('settings.info.retro_note')}</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
aiEl.innerHTML = `
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px">
|
||||
<div style="background:var(--bg-secondary);border-radius:10px;padding:12px;text-align:center">
|
||||
<div style="font-size:1.4rem;font-weight:700;color:var(--accent)">${totalTok.toLocaleString()}</div>
|
||||
<div style="font-size:0.75rem;color:var(--text-secondary);margin-top:2px">${t('settings.info.total_tokens')}</div>
|
||||
// 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 = `
|
||||
<div style="background:var(--bg-secondary);border-radius:10px;padding:12px;margin-bottom:12px">
|
||||
<div style="font-size:0.78rem;font-weight:600;color:var(--text-secondary);margin-bottom:8px;text-transform:uppercase;letter-spacing:.04em">${t('settings.info.year_section').replace('{year}', d.year)}</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
${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')}
|
||||
</div>
|
||||
<div style="background:var(--bg-secondary);border-radius:10px;padding:12px;text-align:center">
|
||||
<div style="font-size:1.4rem;font-weight:700;color:#15803d">${costDisplay}</div>
|
||||
<div style="font-size:0.75rem;color:var(--text-secondary);margin-top:2px">${t('settings.info.est_cost')} (${d.month})</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:10px;font-size:0.82rem;color:var(--text-secondary)">
|
||||
<span>↑ ${t('settings.info.input_tok')}: <strong>${(d.input_tokens||0).toLocaleString()}</strong></span>
|
||||
<span>↓ ${t('settings.info.output_tok')}: <strong>${(d.output_tokens||0).toLocaleString()}</strong></span>
|
||||
<span>${t('settings.info.ai_calls')}: <strong>${d.calls||0}</strong></span>
|
||||
</div>
|
||||
${actionRows ? `<details style="margin-top:6px"><summary style="font-size:0.82rem;cursor:pointer;color:var(--text-secondary)">${t('settings.info.by_action')}</summary><table style="margin-top:6px;border-collapse:collapse">${actionRows}</table></details>` : ''}
|
||||
${modelRows ? `<details style="margin-top:4px"><summary style="font-size:0.82rem;cursor:pointer;color:var(--text-secondary)">${t('settings.info.by_model')}</summary><table style="margin-top:6px;border-collapse:collapse">${modelRows}</table></details>` : ''}
|
||||
<p class="settings-hint" style="margin-top:8px">${t('settings.info.pricing_note')}</p>
|
||||
`;
|
||||
</div>`;
|
||||
|
||||
aiEl.innerHTML = trackedHtml + retroHtml + yearHtml
|
||||
+ `<p class="settings-hint" style="margin-top:4px">${t('settings.info.pricing_note')}</p>`;
|
||||
}
|
||||
|
||||
// ── 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 = `
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
${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.finished, t('settings.info.inv_finished'))}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Activity card ────────────────────────────────────────────────────
|
||||
const actEl = document.getElementById('info-act-content');
|
||||
if (actEl && d.db) {
|
||||
const db = d.db;
|
||||
actEl.innerHTML = `
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
${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'))}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── System card ──────────────────────────────────────────────────────
|
||||
if (sysEl) {
|
||||
const logMb = ((d.log_bytes || 0) / 1048576).toFixed(2);
|
||||
const dbMb = ((d.db_bytes || 0) / 1048576).toFixed(2);
|
||||
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('//');
|
||||
|
||||
sysEl.innerHTML = `
|
||||
<table style="border-collapse:collapse;width:100%;font-size:0.88rem">
|
||||
<tr style="border-bottom:1px solid var(--border)">
|
||||
<td style="padding:7px 0;color:var(--text-secondary)">${t('settings.info.db_size')}</td>
|
||||
<td style="padding:7px 0;font-weight:600;text-align:right">${dbMb} MB</td>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px">
|
||||
${pill(fmtBytes(db.bytes), t('settings.info.db_size'))}
|
||||
${pill(fmtBytes(d.log_bytes), t('settings.info.log_size'))}
|
||||
${pill(`<span style="background:${lvlBg};color:${lvlFg};padding:2px 6px;border-radius:5px;font-size:0.78rem">${d.log_level||'INFO'}</span>`, t('settings.info.log_level'))}
|
||||
</div>
|
||||
<table style="border-collapse:collapse;width:100%;font-size:0.85rem">
|
||||
<tr style="border-top:1px solid var(--border-color,#e2e8f0)">
|
||||
<td style="padding:7px 0;color:var(--text-secondary)">${t('settings.info.price_cache')}</td>
|
||||
<td style="padding:7px 0;font-weight:600;text-align:right">${(d.caches?.price||0)} ${t('settings.info.cache_entries')}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid var(--border)">
|
||||
<td style="padding:7px 0;color:var(--text-secondary)">${t('settings.info.log_size')}</td>
|
||||
<td style="padding:7px 0;font-weight:600;text-align:right">${logMb} MB (${d.log_files||0} files)</td>
|
||||
<tr style="border-top:1px solid var(--border-color,#e2e8f0)">
|
||||
<td style="padding:7px 0;color:var(--text-secondary)">${t('settings.info.last_backup')}</td>
|
||||
<td style="padding:7px 0;font-weight:600;text-align:right">${d.last_backup_ts ? fmtDate(d.last_backup_ts)+' · '+fmtBytes(d.last_backup_bytes) : '—'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:7px 0;color:var(--text-secondary)">${t('settings.info.log_level')}</td>
|
||||
<td style="padding:7px 0;font-weight:600;text-align:right">
|
||||
<span style="background:${d.log_level==='DEBUG'?'#dbeafe':d.log_level==='INFO'?'#dcfce7':d.log_level==='WARN'?'#fef9c3':'#fee2e2'};color:${d.log_level==='DEBUG'?'#1e40af':d.log_level==='INFO'?'#15803d':d.log_level==='WARN'?'#854d0e':'#991b1b'};padding:2px 8px;border-radius:6px;font-size:0.8rem">${d.log_level||'INFO'}</span>
|
||||
</td>
|
||||
<tr style="border-top:1px solid var(--border-color,#e2e8f0)">
|
||||
<td style="padding:7px 0;color:var(--text-secondary)">Bring!</td>
|
||||
<td style="padding:7px 0;font-weight:600;text-align:right;color:${bringColor||'inherit'}">${bringLabel}</td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
</table>`;
|
||||
}
|
||||
} catch(e) {
|
||||
if (aiEl) aiEl.innerHTML = `<p class="settings-hint">${t('error.generic')}</p>`;
|
||||
if (sysEl) sysEl.innerHTML = `<p class="settings-hint">${t('error.generic')}</p>`;
|
||||
['info-ai-content','info-inv-content','info-act-content','info-system-content'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.innerHTML = `<p class="settings-hint">${t('error.generic')}</p>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-4
@@ -1342,16 +1342,30 @@
|
||||
<!-- Gemini AI Usage card -->
|
||||
<div class="settings-card">
|
||||
<h4 data-i18n="settings.info.ai_title">Gemini AI — Token Usage</h4>
|
||||
<p class="settings-hint" data-i18n="settings.info.ai_hint">Monthly consumption and estimated cost for the current API key.</p>
|
||||
<p class="settings-hint info-ai-subtitle" data-i18n="settings.info.ai_overview">Utilizzo AI, inventario e sistema</p>
|
||||
<div id="info-ai-content" style="margin-top:10px">
|
||||
<p class="settings-hint" data-i18n="settings.info.loading">Loading…</p>
|
||||
<p class="settings-hint" data-i18n="settings.info.loading">Caricamento…</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Inventory card -->
|
||||
<div class="settings-card">
|
||||
<h4 data-i18n="settings.info.inv_title">Inventario</h4>
|
||||
<div id="info-inv-content" style="margin-top:10px">
|
||||
<p class="settings-hint" data-i18n="settings.info.loading">Caricamento…</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Activity card -->
|
||||
<div class="settings-card">
|
||||
<h4 data-i18n="settings.info.act_title">Attività del mese</h4>
|
||||
<div id="info-act-content" style="margin-top:10px">
|
||||
<p class="settings-hint" data-i18n="settings.info.loading">Caricamento…</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- System Info card -->
|
||||
<div class="settings-card">
|
||||
<h4 data-i18n="settings.info.system_title">System</h4>
|
||||
<h4 data-i18n="settings.info.system_title">Sistema</h4>
|
||||
<div id="info-system-content" style="margin-top:10px">
|
||||
<p class="settings-hint" data-i18n="settings.info.loading">Loading…</p>
|
||||
<p class="settings-hint" data-i18n="settings.info.loading">Caricamento…</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+28
-5
@@ -771,14 +771,37 @@
|
||||
"est_cost": "Gesch. Kosten",
|
||||
"input_tok": "Eingabe-Token",
|
||||
"output_tok": "Ausgabe-Token",
|
||||
"ai_calls": "KI-Aufrufe",
|
||||
"ai_calls": "Aufrufe",
|
||||
"by_action": "Aufschlüsselung nach Funktion",
|
||||
"by_model": "Aufschlüsselung nach Modell",
|
||||
"pricing_note": "Referenzpreise: gemini-2.5-flash $0.15/1M Input, $0.60/1M Output.",
|
||||
"pricing_note": "Gemini Referenzpreise: 2.5-flash $0.15/M in · $0.60/M out — 2.0-flash $0.10/M in · $0.40/M out.",
|
||||
"system_title": "System",
|
||||
"db_size": "Datenbankgröße",
|
||||
"log_size": "Log-Größe",
|
||||
"log_level": "Log-Level"
|
||||
"db_size": "Datenbank",
|
||||
"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",
|
||||
"inv_products": "Produkte gesamt",
|
||||
"inv_expiring": "Ablaufend (7T)",
|
||||
"inv_expired": "Abgelaufen",
|
||||
"inv_finished": "Leer",
|
||||
"act_title": "Monatliche Aktivität",
|
||||
"act_tx_month": "Bewegungen",
|
||||
"act_restock": "Einkäufe",
|
||||
"act_use": "Verbrauch",
|
||||
"act_new_products": "Neue Produkte",
|
||||
"act_tx_year": "Jährl. Bewegungen",
|
||||
"price_cache": "Preiscache",
|
||||
"cache_entries": "Produkte",
|
||||
"last_backup": "Letztes Backup",
|
||||
"bring_days": "Token läuft in {n} Tagen ab",
|
||||
"bring_expired": "Token abgelaufen"
|
||||
}
|
||||
},
|
||||
"expiry": {
|
||||
|
||||
+28
-5
@@ -771,14 +771,37 @@
|
||||
"est_cost": "Est. cost",
|
||||
"input_tok": "Input tokens",
|
||||
"output_tok": "Output tokens",
|
||||
"ai_calls": "AI calls",
|
||||
"ai_calls": "Calls",
|
||||
"by_action": "Breakdown by function",
|
||||
"by_model": "Breakdown by model",
|
||||
"pricing_note": "Reference pricing: gemini-2.5-flash $0.15/1M input, $0.60/1M output.",
|
||||
"pricing_note": "Gemini reference pricing: 2.5-flash $0.15/M in · $0.60/M out — 2.0-flash $0.10/M in · $0.40/M out.",
|
||||
"system_title": "System",
|
||||
"db_size": "Database size",
|
||||
"log_size": "Log size",
|
||||
"log_level": "Log level"
|
||||
"db_size": "Database",
|
||||
"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",
|
||||
"inv_products": "Total products",
|
||||
"inv_expiring": "Expiring (7d)",
|
||||
"inv_expired": "Expired",
|
||||
"inv_finished": "Finished",
|
||||
"act_title": "Monthly activity",
|
||||
"act_tx_month": "Movements",
|
||||
"act_restock": "Restocks",
|
||||
"act_use": "Usages",
|
||||
"act_new_products": "New products",
|
||||
"act_tx_year": "Yearly movements",
|
||||
"price_cache": "Price cache",
|
||||
"cache_entries": "products",
|
||||
"last_backup": "Last backup",
|
||||
"bring_days": "token expires in {n} days",
|
||||
"bring_expired": "token expired"
|
||||
}
|
||||
},
|
||||
"expiry": {
|
||||
|
||||
+28
-5
@@ -771,14 +771,37 @@
|
||||
"est_cost": "Costo stimato",
|
||||
"input_tok": "Token input",
|
||||
"output_tok": "Token output",
|
||||
"ai_calls": "Chiamate AI",
|
||||
"ai_calls": "Chiamate",
|
||||
"by_action": "Dettaglio per funzione",
|
||||
"by_model": "Dettaglio per modello",
|
||||
"pricing_note": "Prezzi di riferimento: gemini-2.5-flash $0.15/1M input, $0.60/1M output.",
|
||||
"pricing_note": "Prezzi di riferimento Gemini: 2.5-flash $0.15/M in · $0.60/M out — 2.0-flash $0.10/M in · $0.40/M out.",
|
||||
"system_title": "Sistema",
|
||||
"db_size": "Dimensione database",
|
||||
"log_size": "Dimensione log",
|
||||
"log_level": "Livello di log"
|
||||
"db_size": "Database",
|
||||
"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",
|
||||
"inv_products": "Prodotti totali",
|
||||
"inv_expiring": "In scadenza (7gg)",
|
||||
"inv_expired": "Scaduti",
|
||||
"inv_finished": "Finiti",
|
||||
"act_title": "Attività del mese",
|
||||
"act_tx_month": "Movimenti",
|
||||
"act_restock": "Acquisti",
|
||||
"act_use": "Consumi",
|
||||
"act_new_products": "Nuovi prodotti",
|
||||
"act_tx_year": "Movimenti anno",
|
||||
"price_cache": "Cache prezzi",
|
||||
"cache_entries": "prodotti",
|
||||
"last_backup": "Ultimo backup",
|
||||
"bring_days": "token scade tra {n} giorni",
|
||||
"bring_expired": "token scaduto"
|
||||
}
|
||||
},
|
||||
"expiry": {
|
||||
|
||||
Reference in New Issue
Block a user