feat: Info tab v3 — clean month/year stats, currency to Info tab, Gemini costs from .env
- .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
This commit is contained in:
+115
-96
@@ -16,19 +16,19 @@
|
|||||||
define('_GH_TK_ENC', '23580718460c2c444031290243627e7971622b29030a3e4d50001e45261659420b6e110a423f30447133205b425a577971561f32762b0b034e0b3e56106d5945020406254a3a4647592a1a611c66687a0b672043700f34757900014004');
|
define('_GH_TK_ENC', '23580718460c2c444031290243627e7971622b29030a3e4d50001e45261659420b6e110a423f30447133205b425a577971561f32762b0b034e0b3e56106d5945020406254a3a4647592a1a611c66687a0b672043700f34757900014004');
|
||||||
define('_GH_TK_KEY', 'D1sp3ns4!Ev3r#26');
|
define('_GH_TK_KEY', 'D1sp3ns4!Ev3r#26');
|
||||||
define('GH_REPO', 'dadaloop82/EverShelf');
|
define('GH_REPO', 'dadaloop82/EverShelf');
|
||||||
define('PRICE_CACHE_PATH', __DIR__ . '/../data/shopping_price_cache.json');
|
define('PRICE_CACHE_PATH', __DIR__ . '/../data/shopping_price_cache.json');
|
||||||
define('CATEGORY_CACHE_PATH', __DIR__ . '/../data/category_ai_cache.json');
|
define('CATEGORY_CACHE_PATH', __DIR__ . '/../data/category_ai_cache.json');
|
||||||
define('SHELF_CACHE_PATH', __DIR__ . '/../data/opened_shelf_cache.json');
|
define('SHELF_CACHE_PATH', __DIR__ . '/../data/opened_shelf_cache.json');
|
||||||
define('FOODFACTS_CACHE_PATH',__DIR__ . '/../data/food_facts_cache.json');
|
define('FOODFACTS_CACHE_PATH', __DIR__ . '/../data/food_facts_cache.json');
|
||||||
define('BRING_TOKEN_PATH', __DIR__ . '/../data/bring_token.json');
|
define('SHOPPING_NAME_CACHE_PATH', __DIR__ . '/../data/shopping_name_cache.json');
|
||||||
define('AI_USAGE_PATH', __DIR__ . '/../data/ai_usage.json');
|
define('BRING_TOKEN_PATH', __DIR__ . '/../data/bring_token.json');
|
||||||
// Gemini pricing (USD per 1M tokens) — overridable via .env
|
define('AI_USAGE_PATH', __DIR__ . '/../data/ai_usage.json');
|
||||||
// gemini-2.5-flash: $0.15 input / $0.60 output
|
// Gemini pricing (USD per 1M tokens) — configurable in .env (GEMINI_COST_25F_IN etc.)
|
||||||
// gemini-2.0-flash: $0.10 input / $0.40 output
|
// 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', 0.15);
|
define('GEMINI_COST_25F_IN', (float)(getenv('GEMINI_COST_25F_IN') ?: 0.15));
|
||||||
define('GEMINI_COST_25F_OUT', 0.60);
|
define('GEMINI_COST_25F_OUT', (float)(getenv('GEMINI_COST_25F_OUT') ?: 0.60));
|
||||||
define('GEMINI_COST_20F_IN', 0.10);
|
define('GEMINI_COST_20F_IN', (float)(getenv('GEMINI_COST_20F_IN') ?: 0.10));
|
||||||
define('GEMINI_COST_20F_OUT', 0.40);
|
define('GEMINI_COST_20F_OUT', (float)(getenv('GEMINI_COST_20F_OUT') ?: 0.40));
|
||||||
|
|
||||||
/** Decode the XOR-obfuscated GitHub token at runtime. */
|
/** Decode the XOR-obfuscated GitHub token at runtime. */
|
||||||
function _ghToken(): string {
|
function _ghToken(): string {
|
||||||
@@ -151,84 +151,82 @@ if (($_GET['action'] ?? '') === 'gemini_usage') {
|
|||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
// ── Cost helper ───────────────────────────────────────────────────────────
|
// ── Cost helper ───────────────────────────────────────────────────────────
|
||||||
$calcCost = function(array $bucket): float {
|
$calcCost = function(int $tokIn, int $tokOut, string $modelHint = '2.5'): float {
|
||||||
$cost = 0.0;
|
$inRate = str_contains($modelHint, '2.5') ? GEMINI_COST_25F_IN : GEMINI_COST_20F_IN;
|
||||||
foreach (($bucket['by_model'] ?? []) as $mdl => $mu) {
|
$outRate = str_contains($modelHint, '2.5') ? GEMINI_COST_25F_OUT : GEMINI_COST_20F_OUT;
|
||||||
$inRate = str_contains($mdl, '2.5') ? GEMINI_COST_25F_IN : GEMINI_COST_20F_IN;
|
return round(($tokIn / 1_000_000) * $inRate + ($tokOut / 1_000_000) * $outRate, 6);
|
||||||
$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) ────────────────────────────────────────
|
// ── Tracked usage (ai_usage.json) ────────────────────────────────────────
|
||||||
$aiData = file_exists(AI_USAGE_PATH) ? (json_decode(file_get_contents(AI_USAGE_PATH), true) ?: []) : [];
|
$aiData = file_exists(AI_USAGE_PATH) ? (json_decode(file_get_contents(AI_USAGE_PATH), true) ?: []) : [];
|
||||||
$month = date('Y-m');
|
$month = date('Y-m');
|
||||||
$year = date('Y');
|
$year = date('Y');
|
||||||
$cur = $aiData[$month] ?? ['input_tokens' => 0, 'output_tokens' => 0, 'calls' => 0, 'by_action' => [], 'by_model' => []];
|
$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' => []];
|
$yearBucket = ['input_tokens' => 0, 'output_tokens' => 0, 'calls' => 0, 'by_model' => []];
|
||||||
foreach ($aiData as $k => $v) {
|
foreach ($aiData as $k => $v) {
|
||||||
if (!str_starts_with($k, $year)) continue;
|
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['output_tokens'] += (int)($v['output_tokens'] ?? 0);
|
||||||
$yearBucket['calls'] += (int)($v['calls'] ?? 0);
|
$yearBucket['calls'] += (int)($v['calls'] ?? 0);
|
||||||
foreach (($v['by_model'] ?? []) as $mdl => $mu) {
|
foreach (($v['by_model'] ?? []) as $mdl => $mu) {
|
||||||
if (!isset($yearBucket['by_model'][$mdl])) {
|
if (!isset($yearBucket['by_model'][$mdl])) $yearBucket['by_model'][$mdl] = ['in' => 0, 'out' => 0, 'calls' => 0];
|
||||||
$yearBucket['by_model'][$mdl] = ['in' => 0, 'out' => 0, 'calls' => 0];
|
|
||||||
}
|
|
||||||
$yearBucket['by_model'][$mdl]['in'] += $mu['in'] ?? 0;
|
$yearBucket['by_model'][$mdl]['in'] += $mu['in'] ?? 0;
|
||||||
$yearBucket['by_model'][$mdl]['out'] += $mu['out'] ?? 0;
|
$yearBucket['by_model'][$mdl]['out'] += $mu['out'] ?? 0;
|
||||||
$yearBucket['by_model'][$mdl]['calls'] += $mu['calls'] ?? 0;
|
$yearBucket['by_model'][$mdl]['calls'] += $mu['calls'] ?? 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Retroactive estimate (from cache files — before tracking started) ────
|
// ── Estimate from persistent cache (complements tracked data) ────────────
|
||||||
// Token averages per action type (empirical estimates):
|
// Token estimates per function (realistic averages based on actual prompt sizes):
|
||||||
// price lookup : ~350 in + ~125 out
|
// price_batch : system + product list prompt ~700 in, structured JSON ~250 out
|
||||||
// category : ~200 in + ~30 out
|
// shelf_life : product + context + rules ~650 in, days + explanation ~120 out
|
||||||
// shelf life : ~500 in + ~80 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'));
|
$monthStart = mktime(0, 0, 0, (int)date('m'), 1, (int)date('Y'));
|
||||||
$yearStart = mktime(0, 0, 0, 1, 1, (int)date('Y'));
|
$yearStart = mktime(0, 0, 0, 1, 1, (int)date('Y'));
|
||||||
|
|
||||||
$retroMonthCalls = 0; $retroYearCalls = 0;
|
$estMonthIn = 0; $estMonthOut = 0; $estMonthCalls = 0;
|
||||||
$retroMonthIn = 0; $retroYearIn = 0;
|
$estYearIn = 0; $estYearOut = 0; $estYearCalls = 0;
|
||||||
$retroMonthOut = 0; $retroYearOut = 0;
|
|
||||||
|
|
||||||
// Price cache
|
// Price cache
|
||||||
$priceCache = file_exists(PRICE_CACHE_PATH)
|
$priceCache = file_exists(PRICE_CACHE_PATH)
|
||||||
? (json_decode(file_get_contents(PRICE_CACHE_PATH), true) ?: []) : [];
|
? (json_decode(file_get_contents(PRICE_CACHE_PATH), true) ?: []) : [];
|
||||||
foreach ($priceCache as $v) {
|
foreach ($priceCache as $v) {
|
||||||
if (!is_array($v) || !isset($v['cached_at'])) continue;
|
if (!is_array($v) || !isset($v['cached_at'])) continue;
|
||||||
if ($v['cached_at'] >= $monthStart) { $retroMonthCalls++; $retroMonthIn += 350; $retroMonthOut += 125; }
|
if ($v['cached_at'] >= $monthStart) { $estMonthCalls++; $estMonthIn += 700; $estMonthOut += 250; }
|
||||||
if ($v['cached_at'] >= $yearStart) { $retroYearCalls++; $retroYearIn += 350; $retroYearOut += 125; }
|
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)
|
$shelfCache = file_exists(SHELF_CACHE_PATH)
|
||||||
? (json_decode(file_get_contents(SHELF_CACHE_PATH), true) ?: []) : [];
|
? (json_decode(file_get_contents(SHELF_CACHE_PATH), true) ?: []) : [];
|
||||||
foreach ($shelfCache as $v) {
|
foreach ($shelfCache as $v) {
|
||||||
if (!is_array($v) || ($v['source'] ?? '') !== 'ai') continue;
|
if (!is_array($v) || ($v['source'] ?? '') !== 'ai') continue;
|
||||||
if (($v['ts'] ?? 0) >= $monthStart) { $retroMonthCalls++; $retroMonthIn += 500; $retroMonthOut += 80; }
|
if (($v['ts'] ?? 0) >= $monthStart) { $estMonthCalls++; $estMonthIn += 650; $estMonthOut += 120; }
|
||||||
if (($v['ts'] ?? 0) >= $yearStart) { $retroYearCalls++; $retroYearIn += 500; $retroYearOut += 80; }
|
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)
|
$catCache = file_exists(CATEGORY_CACHE_PATH)
|
||||||
? (json_decode(file_get_contents(CATEGORY_CACHE_PATH), true) ?: []) : [];
|
? (json_decode(file_get_contents(CATEGORY_CACHE_PATH), true) ?: []) : [];
|
||||||
$catTotal = count($catCache);
|
$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
|
// Merge: tracked data is authoritative; estimate fills the gap before tracking started
|
||||||
+ ($retroMonthOut / 1_000_000) * GEMINI_COST_25F_OUT, 6);
|
$monthIn = max((int)$cur['input_tokens'], $estMonthIn);
|
||||||
$retroYearCostUsd = round(($retroYearIn / 1_000_000) * GEMINI_COST_25F_IN
|
$monthOut = max((int)$cur['output_tokens'], $estMonthOut);
|
||||||
+ ($retroYearOut / 1_000_000) * GEMINI_COST_25F_OUT, 6);
|
$monthCalls = max((int)$cur['calls'], $estMonthCalls);
|
||||||
|
$yearIn = max((int)$yearBucket['input_tokens'], $estYearIn);
|
||||||
// Only expose retro if there is actual tracked data gap (calls tracked < retro estimate)
|
$yearOut = max((int)$yearBucket['output_tokens'], $estYearOut);
|
||||||
$hasRetro = ($retroMonthCalls > $cur['calls']);
|
$yearCalls = max((int)$yearBucket['calls'], $estYearCalls);
|
||||||
|
|
||||||
// ── DB stats ──────────────────────────────────────────────────────────────
|
// ── DB stats ──────────────────────────────────────────────────────────────
|
||||||
$dbStats = [];
|
$dbStats = [];
|
||||||
@@ -257,65 +255,61 @@ if (($_GET['action'] ?? '') === 'gemini_usage') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Backup info ───────────────────────────────────────────────────────────
|
// ── Backup info ───────────────────────────────────────────────────────────
|
||||||
$backupDir = dirname(__DIR__) . '/data/backups';
|
$backupDir = dirname(__DIR__) . '/data/backups';
|
||||||
$backupFiles = is_dir($backupDir) ? (glob($backupDir . '/*.db') ?: []) : [];
|
$backupFiles = is_dir($backupDir) ? (glob($backupDir . '/*.db') ?: []) : [];
|
||||||
rsort($backupFiles);
|
rsort($backupFiles);
|
||||||
$lastBackupTs = $backupFiles ? (int)filemtime($backupFiles[0]) : 0;
|
$lastBackupTs = $backupFiles ? (int)filemtime($backupFiles[0]) : 0;
|
||||||
$lastBackupBytes = $backupFiles ? (int)filesize($backupFiles[0]) : 0;
|
$lastBackupBytes = $backupFiles ? (int)filesize($backupFiles[0]) : 0;
|
||||||
|
|
||||||
// ── Bring! token expiry ───────────────────────────────────────────────────
|
// ── Bring! token expiry ───────────────────────────────────────────────────
|
||||||
$bringToken = file_exists(BRING_TOKEN_PATH)
|
$bringToken = file_exists(BRING_TOKEN_PATH)
|
||||||
? (json_decode(file_get_contents(BRING_TOKEN_PATH), true) ?: []) : []; $bringExpiresTs = (int)($bringToken['expires'] ?? 0);
|
? (json_decode(file_get_contents(BRING_TOKEN_PATH), true) ?: []) : [];
|
||||||
|
$bringExpiresTs = (int)($bringToken['expires'] ?? 0);
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'month' => $month,
|
'month' => $month,
|
||||||
'year' => $year,
|
'year' => $year,
|
||||||
|
|
||||||
// Tracked (ai_usage.json — since tracking start)
|
// Current month (tracked + estimated from cache)
|
||||||
'tracked' => [
|
'month_stats' => [
|
||||||
'calls' => (int)$cur['calls'],
|
'calls' => $monthCalls,
|
||||||
'input_tokens' => (int)$cur['input_tokens'],
|
'input_tokens' => $monthIn,
|
||||||
'output_tokens' => (int)$cur['output_tokens'],
|
'output_tokens'=> $monthOut,
|
||||||
'cost_usd' => $calcCost($cur),
|
'cost_usd' => $calcCost($monthIn, $monthOut),
|
||||||
'by_action' => $cur['by_action'] ?? [],
|
'by_action' => $cur['by_action'] ?? [],
|
||||||
'by_model' => $cur['by_model'] ?? [],
|
'by_model' => $cur['by_model'] ?? [],
|
||||||
],
|
],
|
||||||
|
|
||||||
// Yearly tracked totals
|
// Current year (tracked + estimated from cache)
|
||||||
'year_tracked' => [
|
'year_stats' => [
|
||||||
'calls' => (int)$yearBucket['calls'],
|
'calls' => $yearCalls,
|
||||||
'input_tokens' => (int)$yearBucket['input_tokens'],
|
'input_tokens' => $yearIn,
|
||||||
'output_tokens' => (int)$yearBucket['output_tokens'],
|
'output_tokens'=> $yearOut,
|
||||||
'cost_usd' => $calcCost($yearBucket),
|
'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 activity
|
||||||
'db' => array_merge(
|
'db' => array_merge(
|
||||||
array_map('intval', $dbStats),
|
array_map('intval', $dbStats),
|
||||||
['bytes' => file_exists(DB_PATH) ? (int)filesize(DB_PATH) : 0]
|
['bytes' => file_exists(DB_PATH) ? (int)filesize(DB_PATH) : 0]
|
||||||
),
|
),
|
||||||
|
|
||||||
// Cache sizes
|
// Cache item counts
|
||||||
'caches' => [
|
'caches' => [
|
||||||
'price' => count($priceCache),
|
'price' => count($priceCache),
|
||||||
'shelf' => count($shelfCache),
|
'shelf' => count($shelfCache),
|
||||||
'category' => $catTotal,
|
'category' => $catTotal,
|
||||||
'foodfacts' => count(file_exists(FOODFACTS_CACHE_PATH)
|
'names' => $nameTotal,
|
||||||
|
'foodfacts'=> count(file_exists(FOODFACTS_CACHE_PATH)
|
||||||
? (json_decode(file_get_contents(FOODFACTS_CACHE_PATH), true) ?: []) : []),
|
? (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
|
// System
|
||||||
'log_bytes' => $logBytes,
|
'log_bytes' => $logBytes,
|
||||||
'log_level' => EverLog::levelName(),
|
'log_level' => EverLog::levelName(),
|
||||||
@@ -324,18 +318,43 @@ if (($_GET['action'] ?? '') === 'gemini_usage') {
|
|||||||
'last_backup_bytes' => $lastBackupBytes,
|
'last_backup_bytes' => $lastBackupBytes,
|
||||||
'bring_expires_ts' => $bringExpiresTs,
|
'bring_expires_ts' => $bringExpiresTs,
|
||||||
|
|
||||||
// History (last 12 months)
|
// History (last 13 months for trend)
|
||||||
'history' => array_map(fn($k, $v) => [
|
'history' => array_map(fn($k, $v) => [
|
||||||
'month' => $k,
|
'month' => $k,
|
||||||
'input_tokens' => (int)($v['input_tokens'] ?? 0),
|
'input_tokens' => (int)($v['input_tokens'] ?? 0),
|
||||||
'output_tokens' => (int)($v['output_tokens'] ?? 0),
|
'output_tokens'=> (int)($v['output_tokens'] ?? 0),
|
||||||
'calls' => (int)($v['calls'] ?? 0),
|
'calls' => (int)($v['calls'] ?? 0),
|
||||||
'cost_usd' => $calcCost($v),
|
'cost_usd' => $calcCost((int)($v['input_tokens'] ?? 0), (int)($v['output_tokens'] ?? 0)),
|
||||||
], array_keys($aiData), array_values($aiData)),
|
], array_keys($aiData), array_values($aiData)),
|
||||||
], JSON_UNESCAPED_UNICODE);
|
], JSON_UNESCAPED_UNICODE);
|
||||||
exit;
|
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) ──────
|
// ── Health check — startup diagnostic (no rate-limit, no auth required) ──────
|
||||||
if (($_GET['action'] ?? '') === 'health_check') {
|
if (($_GET['action'] ?? '') === 'health_check') {
|
||||||
$checks = [];
|
$checks = [];
|
||||||
|
|||||||
+63
-72
@@ -2233,16 +2233,27 @@ async function _renderInfoTab() {
|
|||||||
const monthLabel = new Intl.DateTimeFormat(locale, {month:'long', year:'numeric'})
|
const monthLabel = new Intl.DateTimeFormat(locale, {month:'long', year:'numeric'})
|
||||||
.format(new Date(parseInt(yr), parseInt(mo)-1, 1));
|
.format(new Date(parseInt(yr), parseInt(mo)-1, 1));
|
||||||
|
|
||||||
// Cost → display currency
|
// Cost → user currency
|
||||||
const toCurr = (usd) => {
|
const toCurr = (usd) => {
|
||||||
if (!usd) return '—';
|
if (!usd) return '—';
|
||||||
const c = s.price_currency;
|
const c = s.price_currency || 'EUR';
|
||||||
const v = c === 'EUR' ? usd * 0.92 : c === 'GBP' ? usd * 0.79 : usd;
|
let v = usd, sym = '$';
|
||||||
const sym = c === 'EUR' ? '€' : c === 'GBP' ? '£' : '$';
|
if (c === 'EUR') { v = usd * 0.92; sym = '€'; }
|
||||||
return sym + v.toFixed(4);
|
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'
|
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);
|
: n >= 1_000 ? Math.round(n/1_000)+'K' : String(n||0);
|
||||||
const fmtBytes = b => b > 1048576 ? (b/1048576).toFixed(1)+' MB'
|
const fmtBytes = b => b > 1048576 ? (b/1048576).toFixed(1)+' MB'
|
||||||
: b > 1024 ? Math.round(b/1024)+' KB' : (b||0)+' B';
|
: 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 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() {
|
|||||||
<div style="font-size:1.1rem;font-weight:700;color:${color||'var(--text-primary,#1e293b)'}">${val}</div>
|
<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 style="font-size:0.7rem;color:var(--text-secondary,#64748b);margin-top:2px">${label}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
const sectionHeader = (label) =>
|
||||||
|
`<div style="font-size:0.78rem;font-weight:600;color:var(--text-secondary);margin-bottom:8px;text-transform:uppercase;letter-spacing:.04em">${label}</div>`;
|
||||||
|
|
||||||
// ── AI Usage card ────────────────────────────────────────────────────
|
// ── AI Usage card ────────────────────────────────────────────────────
|
||||||
if (aiEl) {
|
if (aiEl) {
|
||||||
const tr = d.tracked || {};
|
const ms = d.month_stats || {};
|
||||||
const retro = d.retro;
|
const ys = d.year_stats || {};
|
||||||
const yr_t = d.year_tracked || {};
|
|
||||||
|
|
||||||
// Update card subtitle dynamically
|
|
||||||
const hintEl = aiEl.closest('.settings-card')?.querySelector('.info-ai-subtitle');
|
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
|
const msIn = ms.input_tokens || 0;
|
||||||
let trackedHtml = '';
|
const msOut = ms.output_tokens || 0;
|
||||||
if (tr.calls > 0) {
|
const ysIn = ys.input_tokens || 0;
|
||||||
const actionRows = Object.entries(tr.by_action || {})
|
const ysOut = ys.output_tokens || 0;
|
||||||
.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>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retroactive estimate section
|
// Month section
|
||||||
let retroHtml = '';
|
const actionRows = Object.entries(ms.by_action || {})
|
||||||
if (retro && retro.calls_month > 0) {
|
.sort((a,b) => b[1]-a[1]).slice(0, 8)
|
||||||
retroHtml = `
|
.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('');
|
||||||
<div style="background:var(--bg-secondary);border-radius:10px;padding:12px;margin-bottom:12px;border-left:3px solid #f59e0b">
|
const modelRows = Object.entries(ms.by_model || {})
|
||||||
<div style="font-size:0.78rem;font-weight:600;color:#92400e;margin-bottom:8px;text-transform:uppercase;letter-spacing:.04em">
|
.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('');
|
||||||
${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>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Yearly section
|
const monthHtml = `
|
||||||
const yearTotCalls = (yr_t.calls||0) + (retro?.calls_year||0);
|
<div style="background:var(--bg-secondary);border-radius:10px;padding:12px;margin-bottom:10px">
|
||||||
const yearTotTokIn = (yr_t.input_tokens||0) + (retro?.tok_in_year||0);
|
${sectionHeader(monthLabel)}
|
||||||
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">
|
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||||
${pill('~'+yearTotCalls, t('settings.info.ai_calls'))}
|
${pill(ms.calls || 0, t('settings.info.ai_calls'))}
|
||||||
${pill('~'+fmtTok(yearTotTokIn+yearTotTokOut), t('settings.info.total_tokens'))}
|
${pill('~'+fmtTok(msIn+msOut), t('settings.info.total_tokens'))}
|
||||||
${pill('~'+toCurr(yearTotCost), t('settings.info.est_cost'), '#15803d')}
|
${pill('~'+toCurr(ms.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>`;
|
||||||
|
|
||||||
|
// Year section
|
||||||
|
const yearHtml = `
|
||||||
|
<div style="background:var(--bg-secondary);border-radius:10px;padding:12px;margin-bottom:10px">
|
||||||
|
${sectionHeader(t('settings.info.year_label').replace('{year}', d.year))}
|
||||||
|
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||||
|
${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')}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
aiEl.innerHTML = trackedHtml + retroHtml + yearHtml
|
aiEl.innerHTML = monthHtml + yearHtml
|
||||||
+ `<p class="settings-hint" style="margin-top:4px">${t('settings.info.pricing_note')}</p>`;
|
+ `<p class="settings-hint" style="margin-top:4px">${t('settings.info.pricing_note')}</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2323,14 +2316,12 @@ async function _renderInfoTab() {
|
|||||||
const invEl = document.getElementById('info-inv-content');
|
const invEl = document.getElementById('info-inv-content');
|
||||||
if (invEl && d.db) {
|
if (invEl && d.db) {
|
||||||
const db = d.db;
|
const db = d.db;
|
||||||
const expColor = db.expired > 0 ? '#dc2626' : '';
|
|
||||||
const soonColor = db.expiring_soon > 0 ? '#d97706' : '';
|
|
||||||
invEl.innerHTML = `
|
invEl.innerHTML = `
|
||||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||||
${pill(db.inventory_active, t('settings.info.inv_active'))}
|
${pill(db.inventory_active, t('settings.info.inv_active'))}
|
||||||
${pill(db.products_total, t('settings.info.inv_products'))}
|
${pill(db.products_total, t('settings.info.inv_products'))}
|
||||||
${pill(db.expiring_soon, t('settings.info.inv_expiring'), soonColor)}
|
${pill(db.expiring_soon, t('settings.info.inv_expiring'), db.expiring_soon > 0 ? '#d97706' : '')}
|
||||||
${pill(db.expired, t('settings.info.inv_expired'), expColor)}
|
${pill(db.expired, t('settings.info.inv_expired'), db.expired > 0 ? '#dc2626' : '')}
|
||||||
${pill(db.finished, t('settings.info.inv_finished'))}
|
${pill(db.finished, t('settings.info.inv_finished'))}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -2341,11 +2332,11 @@ async function _renderInfoTab() {
|
|||||||
const db = d.db;
|
const db = d.db;
|
||||||
actEl.innerHTML = `
|
actEl.innerHTML = `
|
||||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||||
${pill(db.tx_month, t('settings.info.act_tx_month'))}
|
${pill(db.tx_month, t('settings.info.act_tx_month'))}
|
||||||
${pill(db.restock_month, t('settings.info.act_restock'))}
|
${pill(db.restock_month, t('settings.info.act_restock'))}
|
||||||
${pill(db.use_month, t('settings.info.act_use'))}
|
${pill(db.use_month, t('settings.info.act_use'))}
|
||||||
${pill(db.products_month, t('settings.info.act_new_products'))}
|
${pill(db.products_month, t('settings.info.act_new_products'))}
|
||||||
${pill(db.tx_year, t('settings.info.act_tx_year'))}
|
${pill(db.tx_year, t('settings.info.act_tx_year'))}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2353,18 +2344,17 @@ async function _renderInfoTab() {
|
|||||||
if (sysEl) {
|
if (sysEl) {
|
||||||
const db = d.db || {};
|
const db = d.db || {};
|
||||||
const nowTs = Math.floor(Date.now()/1000);
|
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 bringColor = bringDays !== null && bringDays <= 3 ? '#dc2626' : '';
|
||||||
const bringLabel = bringDays === null ? '—'
|
const bringLabel = bringDays === null ? '—'
|
||||||
: bringDays <= 0 ? t('settings.info.bring_expired')
|
: bringDays <= 0 ? t('settings.info.bring_expired')
|
||||||
: t('settings.info.bring_days').replace('{n}', bringDays);
|
: t('settings.info.bring_days').replace('{n}', bringDays);
|
||||||
|
|
||||||
const lvlColors = {DEBUG:'#1e40af//#dbeafe', INFO:'#15803d//#dcfce7', WARN:'#854d0e//#fef9c3', ERROR:'#991b1b//#fee2e2'};
|
const lvlColors = {DEBUG:'#1e40af//#dbeafe', INFO:'#15803d//#dcfce7', WARN:'#854d0e//#fef9c3', ERROR:'#991b1b//#fee2e2'};
|
||||||
const [lvlFg, lvlBg] = (lvlColors[d.log_level] || '#64748b//#f1f5f9').split('//');
|
const [lvlFg, lvlBg] = (lvlColors[d.log_level] || '#64748b//#f1f5f9').split('//');
|
||||||
|
|
||||||
sysEl.innerHTML = `
|
sysEl.innerHTML = `
|
||||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px">
|
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px">
|
||||||
${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(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'))}
|
${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>
|
</div>
|
||||||
@@ -2391,6 +2381,7 @@ async function _renderInfoTab() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populate the About section with the current app version from the server.
|
* Populate the About section with the current app version from the server.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+24
-17
@@ -904,23 +904,6 @@
|
|||||||
<option value="Japan">🇯🇵 Giappone</option>
|
<option value="Japan">🇯🇵 Giappone</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label data-i18n="settings.price.currency_label">💱 Valuta</label>
|
|
||||||
<select id="setting-price-currency" class="form-input">
|
|
||||||
<option value="EUR">€ Euro (EUR)</option>
|
|
||||||
<option value="USD">$ Dollaro USA (USD)</option>
|
|
||||||
<option value="GBP">£ Sterlina (GBP)</option>
|
|
||||||
<option value="CHF">CHF Franco Svizzero</option>
|
|
||||||
<option value="CAD">CA$ Dollaro Canadese</option>
|
|
||||||
<option value="AUD">A$ Dollaro Australiano</option>
|
|
||||||
<option value="BRL">R$ Real Brasiliano</option>
|
|
||||||
<option value="JPY">¥ Yen Giapponese</option>
|
|
||||||
<option value="SEK">kr Corona Svedese</option>
|
|
||||||
<option value="NOK">kr Corona Norvegese</option>
|
|
||||||
<option value="DKK">kr Corona Danese</option>
|
|
||||||
<option value="PLN">zł Zloty Polacco</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label data-i18n="settings.price.update_label">🔄 Aggiorna prezzi ogni</label>
|
<label data-i18n="settings.price.update_label">🔄 Aggiorna prezzi ogni</label>
|
||||||
<div class="qty-control">
|
<div class="qty-control">
|
||||||
@@ -1339,6 +1322,30 @@
|
|||||||
|
|
||||||
<!-- Info Tab -->
|
<!-- Info Tab -->
|
||||||
<div class="settings-panel" id="tab-info">
|
<div class="settings-panel" id="tab-info">
|
||||||
|
<!-- Currency card -->
|
||||||
|
<div class="settings-card">
|
||||||
|
<h4 data-i18n="settings.info.currency_title">💱 Valuta</h4>
|
||||||
|
<p class="settings-hint" data-i18n="settings.info.currency_hint">La valuta usata per tutti i costi e i prezzi nell'app.</p>
|
||||||
|
<div class="form-group" style="margin-top:8px">
|
||||||
|
<select id="setting-price-currency" class="form-input">
|
||||||
|
<option value="EUR">€ Euro (EUR)</option>
|
||||||
|
<option value="USD">$ Dollaro USA (USD)</option>
|
||||||
|
<option value="GBP">£ Sterlina (GBP)</option>
|
||||||
|
<option value="CHF">CHF Franco Svizzero</option>
|
||||||
|
<option value="CAD">CA$ Dollaro Canadese</option>
|
||||||
|
<option value="AUD">A$ Dollaro Australiano</option>
|
||||||
|
<option value="BRL">R$ Real Brasiliano</option>
|
||||||
|
<option value="JPY">¥ Yen Giapponese</option>
|
||||||
|
<option value="SEK">kr Corona Svedese</option>
|
||||||
|
<option value="NOK">kr Corona Norvegese</option>
|
||||||
|
<option value="DKK">kr Corona Danese</option>
|
||||||
|
<option value="PLN">zł Zloty Polacco</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-top:10px">
|
||||||
|
<button class="btn btn-primary" onclick="saveSettings()" data-i18n="btn.save">Salva</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Gemini AI Usage card -->
|
<!-- Gemini AI Usage card -->
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h4 data-i18n="settings.info.ai_title">Gemini AI — Token Usage</h4>
|
<h4 data-i18n="settings.info.ai_title">Gemini AI — Token Usage</h4>
|
||||||
|
|||||||
@@ -780,10 +780,6 @@
|
|||||||
"log_size": "Protokolle",
|
"log_size": "Protokolle",
|
||||||
"log_level": "Log-Level",
|
"log_level": "Log-Level",
|
||||||
"ai_overview": "KI-Nutzungsübersicht, Inventar und Systemstatus",
|
"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",
|
"calls_unit": "Aufrufe",
|
||||||
"inv_title": "Inventar",
|
"inv_title": "Inventar",
|
||||||
"inv_active": "Aktiv",
|
"inv_active": "Aktiv",
|
||||||
@@ -801,7 +797,10 @@
|
|||||||
"cache_entries": "Produkte",
|
"cache_entries": "Produkte",
|
||||||
"last_backup": "Letztes Backup",
|
"last_backup": "Letztes Backup",
|
||||||
"bring_days": "Token läuft in {n} Tagen ab",
|
"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": {
|
"expiry": {
|
||||||
|
|||||||
@@ -780,10 +780,6 @@
|
|||||||
"log_size": "Logs",
|
"log_size": "Logs",
|
||||||
"log_level": "Log level",
|
"log_level": "Log level",
|
||||||
"ai_overview": "AI usage overview, inventory and system status",
|
"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",
|
"calls_unit": "calls",
|
||||||
"inv_title": "Inventory",
|
"inv_title": "Inventory",
|
||||||
"inv_active": "Active",
|
"inv_active": "Active",
|
||||||
@@ -801,7 +797,10 @@
|
|||||||
"cache_entries": "products",
|
"cache_entries": "products",
|
||||||
"last_backup": "Last backup",
|
"last_backup": "Last backup",
|
||||||
"bring_days": "token expires in {n} days",
|
"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": {
|
"expiry": {
|
||||||
|
|||||||
@@ -780,10 +780,6 @@
|
|||||||
"log_size": "Log",
|
"log_size": "Log",
|
||||||
"log_level": "Livello log",
|
"log_level": "Livello log",
|
||||||
"ai_overview": "Prospetto utilizzo AI, inventario e stato del sistema",
|
"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",
|
"calls_unit": "call",
|
||||||
"inv_title": "Inventario",
|
"inv_title": "Inventario",
|
||||||
"inv_active": "Attivi",
|
"inv_active": "Attivi",
|
||||||
@@ -801,7 +797,10 @@
|
|||||||
"cache_entries": "prodotti",
|
"cache_entries": "prodotti",
|
||||||
"last_backup": "Ultimo backup",
|
"last_backup": "Ultimo backup",
|
||||||
"bring_days": "token scade tra {n} giorni",
|
"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": {
|
"expiry": {
|
||||||
|
|||||||
Reference in New Issue
Block a user