Rene Fichtmueller 998e8d8aee feat: add OpenAI/ChatGPT passthrough + per-AI savings in Overview
- New openai-proxy.ts: POST /v1/chat/completions forwards client's API key
  to OpenAI, creates tracking ticket with full cost calculation for all
  gpt-4o, gpt-4o-mini, o1, o3-mini, gpt-4-turbo, gpt-3.5-turbo models
- GET /v1/models unified in openai-proxy (removed duplicate from anthropic-proxy)
- Dashboard: POST /v1/chat/completions forwarded to core (OPENAI_BASE_URL works)
- Overview 'Savings per AI' panel: shows emoji label, request count, cost,
  tokens in/out, and savings per provider (🟣 Claude 🟢 ChatGPT 🟡 Ollama)
- Old pipeline-based proxy.ts replaced by direct API passthroughs
2026-04-14 22:55:13 +02:00

1064 lines
54 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TokenVault — LLM Cost Intelligence</title>
<style>
:root {
--primary: #6366f1;
--primary-light: #818cf8;
--primary-dark: #4f46e5;
--bg: #f8fafc;
--surface: #ffffff;
--text: #1e293b;
--text-muted: #64748b;
--border: #e2e8f0;
--green: #22c55e;
--red: #ef4444;
--amber: #f59e0b;
--blue: #3b82f6;
--radius: 12px;
--shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);
}
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; background:var(--bg); color:var(--text); }
/* Header */
.header { background:var(--surface); border-bottom:1px solid var(--border); padding:16px 24px; display:flex; align-items:center; justify-content:space-between; }
.header h1 { font-size:20px; font-weight:700; display:flex; align-items:center; gap:8px; }
.header h1 span { color:var(--primary); }
.header .lang { display:flex; gap:4px; }
.header .lang button { padding:4px 10px; border:1px solid var(--border); border-radius:6px; background:var(--surface); cursor:pointer; font-size:12px; }
.header .lang button.active { background:var(--primary); color:#fff; border-color:var(--primary); }
/* Tabs */
.tabs { display:flex; gap:0; background:var(--surface); border-bottom:2px solid var(--border); padding:0 24px; overflow-x:auto; }
.tab { padding:12px 20px; cursor:pointer; font-size:14px; font-weight:500; color:var(--text-muted); border-bottom:2px solid transparent; margin-bottom:-2px; white-space:nowrap; }
.tab:hover { color:var(--text); }
.tab.active { color:var(--primary); border-bottom-color:var(--primary); }
/* Content */
.content { max-width:1400px; margin:0 auto; padding:24px; }
/* Cards */
.cards { display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:16px; margin-bottom:24px; }
.card { background:var(--surface); border-radius:var(--radius); padding:20px; box-shadow:var(--shadow); border:1px solid var(--border); }
.card .label { font-size:12px; font-weight:600; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-muted); margin-bottom:4px; }
.card .value { font-size:28px; font-weight:700; }
.card .sub { font-size:12px; color:var(--text-muted); margin-top:4px; }
.card .value.green { color:var(--green); }
.card .value.primary { color:var(--primary); }
.card .value.amber { color:var(--amber); }
/* Table */
.table-wrap { background:var(--surface); border-radius:var(--radius); box-shadow:var(--shadow); border:1px solid var(--border); overflow-x:auto; }
table { width:100%; border-collapse:collapse; font-size:13px; }
th { background:#f1f5f9; text-align:left; padding:10px 14px; font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-muted); border-bottom:1px solid var(--border); }
td { padding:10px 14px; border-bottom:1px solid var(--border); }
tr:last-child td { border-bottom:none; }
tr:hover { background:#f8fafc; }
/* Badges */
.badge { display:inline-block; padding:2px 8px; border-radius:6px; font-size:11px; font-weight:600; }
.badge.completed { background:#dcfce7; color:#166534; }
.badge.cached { background:#dbeafe; color:#1e40af; }
.badge.failed { background:#fef2f2; color:#991b1b; }
.badge.pending_review { background:#fef3c7; color:#92400e; }
/* Provider badges */
.provider { display:inline-flex; align-items:center; gap:4px; padding:2px 8px; border-radius:6px; font-size:11px; font-weight:600; }
.provider.anthropic { background:#fdf4ff; color:#86198f; }
.provider.openai { background:#f0fdf4; color:#166534; }
.provider.ollama { background:#eff6ff; color:#1e40af; }
.provider.google { background:#fef9c3; color:#854d0e; }
.provider.groq { background:#fce7f3; color:#9d174d; }
/* Section */
.section { margin-bottom:24px; }
.section h2 { font-size:16px; font-weight:600; margin-bottom:12px; color:var(--text); display:flex; align-items:center; gap:8px; }
/* Charts placeholder */
.chart-placeholder { background:var(--surface); border-radius:var(--radius); padding:40px; text-align:center; color:var(--text-muted); border:1px solid var(--border); box-shadow:var(--shadow); }
.chart-row { display:grid; grid-template-columns:2fr 1fr; gap:16px; margin-bottom:24px; }
@media (max-width: 768px) { .chart-row { grid-template-columns:1fr; } }
/* Loading */
.loading { text-align:center; padding:40px; color:var(--text-muted); }
.spin { display:inline-block; width:20px; height:20px; border:2px solid var(--border); border-top-color:var(--primary); border-radius:50%; animation:spin 0.6s linear infinite; }
@keyframes spin { to { transform:rotate(360deg); } }
/* Refresh button */
.refresh { padding:6px 14px; background:var(--primary); color:#fff; border:none; border-radius:8px; cursor:pointer; font-size:13px; font-weight:500; }
.refresh:hover { background:var(--primary-dark); }
/* Settings */
.settings-grid { display:grid; grid-template-columns:240px 1fr; gap:0; background:var(--surface); border-radius:var(--radius); box-shadow:var(--shadow); border:1px solid var(--border); min-height:600px; }
@media (max-width: 768px) { .settings-grid { grid-template-columns:1fr; } }
.settings-nav { border-right:1px solid var(--border); padding:8px 0; }
.settings-nav-item { padding:10px 20px; cursor:pointer; font-size:13px; font-weight:500; color:var(--text-muted); display:flex; align-items:center; gap:8px; transition:all 0.15s; }
.settings-nav-item:hover { background:#f1f5f9; color:var(--text); }
.settings-nav-item.active { background:#eef2ff; color:var(--primary); border-right:2px solid var(--primary); font-weight:600; }
.settings-panel { padding:24px; }
.settings-panel h3 { font-size:16px; font-weight:600; margin-bottom:4px; }
.settings-panel .desc { font-size:13px; color:var(--text-muted); margin-bottom:20px; }
.form-group { margin-bottom:20px; }
.form-group label { display:block; font-size:12px; font-weight:600; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-muted); margin-bottom:6px; }
.form-group input, .form-group select, .form-group textarea { width:100%; padding:8px 12px; border:1px solid var(--border); border-radius:8px; font-size:14px; background:var(--bg); transition:border-color 0.15s; }
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline:none; border-color:var(--primary); box-shadow:0 0 0 3px rgba(99,102,241,0.1); }
.form-group .hint { font-size:11px; color:var(--text-muted); margin-top:4px; }
.form-group input[type="password"] { font-family:monospace; letter-spacing:2px; }
.form-row { display:grid; grid-template-columns:1fr 1fr; gap:16px; }
@media (max-width: 768px) { .form-row { grid-template-columns:1fr; } }
.btn { padding:8px 16px; border:1px solid var(--border); border-radius:8px; font-size:13px; font-weight:500; cursor:pointer; transition:all 0.15s; }
.btn-primary { background:var(--primary); color:#fff; border-color:var(--primary); }
.btn-primary:hover { background:var(--primary-dark); }
.btn-danger { background:var(--red); color:#fff; border-color:var(--red); }
.btn-outline { background:var(--surface); color:var(--text); }
.btn-outline:hover { background:#f1f5f9; }
.btn-group { display:flex; gap:8px; margin-top:16px; }
.toggle { position:relative; display:inline-block; width:44px; height:24px; }
.toggle input { opacity:0; width:0; height:0; }
.toggle .slider { position:absolute; inset:0; background:#cbd5e1; border-radius:24px; cursor:pointer; transition:0.2s; }
.toggle .slider:before { content:''; position:absolute; height:18px; width:18px; left:3px; bottom:3px; background:#fff; border-radius:50%; transition:0.2s; }
.toggle input:checked + .slider { background:var(--primary); }
.toggle input:checked + .slider:before { transform:translateX(20px); }
.divider { height:1px; background:var(--border); margin:24px 0; }
.status-dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:4px; }
.status-dot.green { background:var(--green); }
.status-dot.red { background:var(--red); }
.status-dot.amber { background:var(--amber); }
.key-mask { font-family:monospace; font-size:13px; color:var(--text-muted); }
.provider-card { background:var(--bg); border:1px solid var(--border); border-radius:var(--radius); padding:16px; margin-bottom:12px; }
.provider-card h4 { font-size:14px; font-weight:600; margin-bottom:8px; display:flex; align-items:center; gap:6px; }
</style>
</head>
<body>
<div class="header">
<h1><span>Token</span>Vault <small style="font-size:12px;color:var(--text-muted);margin-left:4px">v0.1.0</small></h1>
<div style="display:flex;align-items:center;gap:12px">
<div class="lang">
<button class="active" onclick="setLang('en')">EN</button>
<button onclick="setLang('de')">DE</button>
</div>
<button class="refresh" onclick="loadAll()">Refresh</button>
</div>
</div>
<div class="tabs">
<div class="tab active" data-tab="overview">Overview</div>
<div class="tab" data-tab="tickets">Tickets</div>
<div class="tab" data-tab="cost">Cost Analysis</div>
<div class="tab" data-tab="providers">Providers</div>
<div class="tab" data-tab="rtk">RTK Savings</div>
<div class="tab" data-tab="settings">⚙ Settings</div>
</div>
<div class="content">
<!-- Overview Tab -->
<div id="tab-overview">
<div class="cards" id="stats-cards">
<div class="loading"><div class="spin"></div> Loading...</div>
</div>
<!-- RTK savings banner — always visible since RTK data exists independently of LLM routing -->
<div id="rtk-overview-banner" style="display:none;margin:16px 0;background:linear-gradient(135deg,#f0fdf4,#dcfce7);border:1px solid #86efac;border-radius:12px;padding:20px 24px">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
<span style="font-size:20px"></span>
<strong style="color:#16a34a;font-size:15px">RTK Token Savings (all-time)</strong>
<span style="margin-left:auto;font-size:12px;color:#15803d" id="rtk-banner-host"></span>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px" id="rtk-banner-cards"></div>
</div>
<div class="chart-row">
<div class="chart-placeholder" id="cost-timeline">Cost timeline will appear here after requests are tracked</div>
<div id="provider-split" style="background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:24px;flex:1;min-height:180px">
<div style="font-weight:600;margin-bottom:12px;color:var(--text-muted)">Savings per AI / Pro KI gespart</div>
<div id="provider-split-inner" style="color:var(--text-muted);font-size:13px;text-align:center;margin-top:40px">Provider split will appear here after requests are tracked</div>
</div>
</div>
</div>
<!-- Tickets Tab -->
<div id="tab-tickets" style="display:none">
<div class="section">
<h2>Recent Tickets</h2>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Ticket</th>
<th>Provider</th>
<th>Model</th>
<th>Status</th>
<th>Tokens In</th>
<th>Tokens Out</th>
<th>Cached</th>
<th>Saved</th>
<th>Cost</th>
<th>Latency</th>
<th>Time</th>
</tr>
</thead>
<tbody id="ticket-rows">
<tr><td colspan="11" class="loading"><div class="spin"></div></td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Cost Tab -->
<div id="tab-cost" style="display:none">
<div class="cards" id="cost-cards"></div>
<div class="section">
<h2>Cost Breakdown by Provider</h2>
<div class="table-wrap">
<table>
<thead><tr><th>Provider</th><th>Requests</th><th>Tokens In</th><th>Tokens Out</th><th>Cost</th><th>Saved</th></tr></thead>
<tbody id="breakdown-rows"></tbody>
</table>
</div>
</div>
</div>
<!-- Providers Tab -->
<div id="tab-providers" style="display:none">
<div class="section">
<h2>Configured Providers</h2>
<div id="provider-list" class="cards"></div>
</div>
</div>
<!-- RTK Savings Tab -->
<div id="tab-rtk" style="display:none">
<div class="cards" id="rtk-stats-cards">
<div class="loading"><div class="spin"></div> Loading RTK stats...</div>
</div>
<div class="chart-row">
<div class="section" style="margin:0">
<h2>Top Commands by Savings</h2>
<div class="table-wrap">
<table>
<thead><tr><th>Command</th><th>Count</th><th>Saved</th><th>Avg %</th></tr></thead>
<tbody id="rtk-commands-rows"></tbody>
</table>
</div>
</div>
<div class="section" style="margin:0">
<h2>Hosts</h2>
<div class="table-wrap">
<table>
<thead><tr><th>Host</th><th>Commands</th><th>Saved</th><th>Last Seen</th></tr></thead>
<tbody id="rtk-hosts-rows"></tbody>
</table>
</div>
</div>
</div>
<div class="section">
<h2>About RTK Integration</h2>
<div class="card">
<p style="font-size:13px;line-height:1.6;color:var(--text-muted)">
<strong style="color:var(--text)">RTK (Rust Token Killer)</strong> — MIT-licensed open-source tool that reduces LLM token consumption by 60-90% through CLI output compression.
TokenVault ingests RTK's local SQLite stats from every machine you run the <code>tokenvault-rtk-sync</code> agent on, providing cross-machine aggregation, team/project attribution, and long-term cost trend analysis.
</p>
<p style="font-size:12px;margin-top:12px">
<strong>Setup on a new machine:</strong><br>
<code style="background:#f1f5f9;padding:2px 6px;border-radius:4px">brew install rtk</code> ·
<code style="background:#f1f5f9;padding:2px 6px;border-radius:4px">npm i -g @tokenvault/rtk-bridge</code> ·
<code style="background:#f1f5f9;padding:2px 6px;border-radius:4px">tokenvault-rtk-sync</code>
</p>
</div>
</div>
</div>
<!-- Settings Tab -->
<div id="tab-settings" style="display:none">
<div class="settings-grid">
<div class="settings-nav">
<div class="settings-nav-item active" data-settings="providers" onclick="showSettings('providers')">LLM Providers</div>
<div class="settings-nav-item" data-settings="budgets" onclick="showSettings('budgets')">Budgets & Alerts</div>
<div class="settings-nav-item" data-settings="cache" onclick="showSettings('cache')">Semantic Cache</div>
<div class="settings-nav-item" data-settings="compression" onclick="showSettings('compression')">Compression</div>
<div class="settings-nav-item" data-settings="routing" onclick="showSettings('routing')">Model Routing</div>
<div class="settings-nav-item" data-settings="governance" onclick="showSettings('governance')">Governance & RBAC</div>
<div class="settings-nav-item" data-settings="notifications" onclick="showSettings('notifications')">Notifications</div>
<div class="settings-nav-item" data-settings="database" onclick="showSettings('database')">Database</div>
<div class="settings-nav-item" data-settings="general" onclick="showSettings('general')">General</div>
<div class="settings-nav-item" data-settings="about" onclick="showSettings('about')">About</div>
</div>
<div class="settings-panel" id="settings-content">
<!-- Default: Providers -->
</div>
</div>
</div>
</div>
<script>
const API = '/api';
let lang = 'en';
const t = {
en: { totalCost:'Total Cost', totalSaved:'Total Saved', requests:'Requests', cacheRate:'Cache Hit Rate', tokensIn:'Tokens In', tokensOut:'Tokens Out' },
de: { totalCost:'Gesamtkosten', totalSaved:'Gespart', requests:'Anfragen', cacheRate:'Cache Hit Rate', tokensIn:'Tokens Ein', tokensOut:'Tokens Aus' },
};
function setLang(l) {
lang = l;
document.querySelectorAll('.lang button').forEach(b => b.classList.toggle('active', b.textContent === l.toUpperCase()));
loadAll();
}
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
document.querySelectorAll('[id^="tab-"]').forEach(el => el.style.display = 'none');
document.getElementById('tab-' + tab.dataset.tab).style.display = 'block';
});
});
function fmt(n) { return typeof n === 'number' ? (n < 0.01 ? n.toFixed(6) : n.toFixed(2)) : '0'; }
function fmtK(n) { return n >= 1000000 ? (n/1000000).toFixed(1)+'M' : n >= 1000 ? (n/1000).toFixed(1)+'K' : String(n); }
function fmtTime(d) { return new Date(d).toLocaleString(lang === 'de' ? 'de-DE' : 'en-US', { month:'short', day:'numeric', hour:'2-digit', minute:'2-digit' }); }
async function loadStats() {
try {
const [data, rtkStats] = await Promise.all([
(await fetch(API + '/tickets/stats?period=month')).json(),
fetch(API + '/rtk/stats?period=all').then(r => r.ok ? r.json() : null).catch(() => null),
]);
const labels = t[lang];
document.getElementById('stats-cards').innerHTML = `
<div class="card"><div class="label">${labels.totalCost}</div><div class="value primary">$${fmt(data.total_cost_usd)}</div><div class="sub">Last 30 days</div></div>
<div class="card"><div class="label">${labels.totalSaved}</div><div class="value green">$${fmt(data.total_saved_usd)}</div><div class="sub">Via compression + caching</div></div>
<div class="card"><div class="label">${labels.requests}</div><div class="value">${fmtK(data.total_requests)}</div><div class="sub">Tickets tracked</div></div>
<div class="card"><div class="label">${labels.cacheRate}</div><div class="value amber">${(data.cache_hit_rate * 100).toFixed(0)}%</div><div class="sub">Semantic cache hits</div></div>
<div class="card"><div class="label">${labels.tokensIn}</div><div class="value">${fmtK(data.total_tokens_in)}</div></div>
<div class="card"><div class="label">${labels.tokensOut}</div><div class="value">${fmtK(data.total_tokens_out)}</div></div>
`;
// ── RTK savings banner ──────────────────────────────────────────────────
if (rtkStats && rtkStats.total_commands > 0) {
const banner = document.getElementById('rtk-overview-banner');
banner.style.display = 'block';
document.getElementById('rtk-banner-host').textContent =
`${rtkStats.unique_hosts} host${rtkStats.unique_hosts !== 1 ? 's' : ''} · ${rtkStats.unique_projects} project${rtkStats.unique_projects !== 1 ? 's' : ''}`;
document.getElementById('rtk-banner-cards').innerHTML = `
<div style="background:#fff;border-radius:8px;padding:12px;text-align:center">
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">TOKENS SAVED</div>
<div style="font-size:22px;font-weight:700;color:#16a34a">${fmtK(rtkStats.total_saved_tokens)}</div>
</div>
<div style="background:#fff;border-radius:8px;padding:12px;text-align:center">
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">COMMANDS</div>
<div style="font-size:22px;font-weight:700;color:#6366f1">${fmtK(rtkStats.total_commands)}</div>
</div>
<div style="background:#fff;border-radius:8px;padding:12px;text-align:center">
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">AVG SAVINGS</div>
<div style="font-size:22px;font-weight:700;color:#f59e0b">${rtkStats.avg_savings_pct.toFixed(1)}%</div>
</div>
<div style="background:#fff;border-radius:8px;padding:12px;text-align:center">
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">INPUT TOKENS</div>
<div style="font-size:22px;font-weight:700">${fmtK(rtkStats.total_input_tokens)}</div>
</div>
`;
}
// ── Savings per AI (provider breakdown) ────────────────────────────────
try {
const breakdown = await (await fetch(API + '/cost/breakdown?group_by=provider')).json();
const totalTokens = breakdown.reduce((s, b) => s + b.tokens_in + b.tokens_out, 0);
const AI_LABELS = { anthropic:'Claude (Anthropic)', openai:'ChatGPT (OpenAI)', ollama:'Ollama (Lokal)', 'ai-bridge':'AI-Bridge' };
const AI_EMOJI = { anthropic:'🟣', openai:'🟢', ollama:'🟡', 'ai-bridge':'🔵' };
const colors = { anthropic:'#6366f1', openai:'#10b981', ollama:'#f59e0b', 'ai-bridge':'#3b82f6' };
if (breakdown.length > 0) {
document.getElementById('provider-split-inner').innerHTML = breakdown.map(b => {
const tok = b.tokens_in + b.tokens_out;
const pct = totalTokens > 0 ? ((tok / totalTokens) * 100).toFixed(0) : 0;
const col = colors[b.group_value] || '#94a3b8';
const label = AI_LABELS[b.group_value] || b.group_value;
const emoji = AI_EMOJI[b.group_value] || '⚫';
return `<div style="margin-bottom:14px">
<div style="display:flex;justify-content:space-between;align-items:center;font-size:12px;margin-bottom:4px">
<span style="font-weight:600">${emoji} ${label}</span>
<span style="color:var(--text-muted)">${b.request_count} req · $${fmt(b.cost_usd)}</span>
</div>
<div style="display:flex;gap:8px;font-size:11px;color:var(--text-muted);margin-bottom:4px">
<span>↑ ${fmtK(b.tokens_in)} in</span><span>↓ ${fmtK(b.tokens_out)} out</span><span style="color:#16a34a">💰 $${fmt(b.saved_usd)} saved</span>
</div>
<div style="background:var(--border);border-radius:4px;height:5px">
<div style="background:${col};width:${pct}%;height:5px;border-radius:4px"></div>
</div>
</div>`;
}).join('');
}
} catch {}
} catch { document.getElementById('stats-cards').innerHTML = '<div class="card"><div class="label">Status</div><div class="value">Offline</div><div class="sub">TokenVault core not reachable</div></div>'; }
}
async function loadTickets() {
try {
const data = await (await fetch(API + '/tickets?limit=50')).json();
document.getElementById('ticket-rows').innerHTML = data.tickets.map(t => `
<tr>
<td><strong>${t.ticket_display}</strong></td>
<td><span class="provider ${t.provider}">${t.provider}</span></td>
<td style="font-size:12px">${t.model}</td>
<td><span class="badge ${t.status}">${t.status}</span></td>
<td>${fmtK(t.tokens_in)}</td>
<td>${fmtK(t.tokens_out)}</td>
<td>${fmtK(t.tokens_cached)}</td>
<td style="color:var(--green)">${fmtK(t.tokens_saved)}</td>
<td>$${fmt(t.cost_usd)}</td>
<td>${t.latency_ms}ms</td>
<td style="font-size:11px;color:var(--text-muted)">${fmtTime(t.created_at)}</td>
</tr>
`).join('') || '<tr><td colspan="11" style="text-align:center;color:var(--text-muted)">No tickets yet — send your first request through the proxy</td></tr>';
} catch { document.getElementById('ticket-rows').innerHTML = '<tr><td colspan="11" class="loading">Could not load tickets</td></tr>'; }
}
async function loadBreakdown() {
try {
const [stats, breakdown] = await Promise.all([
(await fetch(API + '/cost?period=month')).json(),
(await fetch(API + '/cost/breakdown?group_by=provider')).json(),
]);
const labels = t[lang];
document.getElementById('cost-cards').innerHTML = `
<div class="card"><div class="label">${labels.totalCost} (30d)</div><div class="value primary">$${fmt(stats.total_cost_usd)}</div></div>
<div class="card"><div class="label">${labels.totalSaved}</div><div class="value green">$${fmt(stats.total_saved_usd)}</div></div>
<div class="card"><div class="label">Avg Compression</div><div class="value">${(stats.avg_compression_ratio * 100).toFixed(0)}%</div></div>
`;
document.getElementById('breakdown-rows').innerHTML = breakdown.map(b => `
<tr>
<td><span class="provider ${b.group_value}">${b.group_value}</span></td>
<td>${b.request_count}</td>
<td>${fmtK(b.tokens_in)}</td>
<td>${fmtK(b.tokens_out)}</td>
<td>$${fmt(b.cost_usd)}</td>
<td style="color:var(--green)">$${fmt(b.saved_usd)}</td>
</tr>
`).join('') || '<tr><td colspan="6">No data yet</td></tr>';
} catch {}
}
async function loadProviders() {
try {
const data = await (await fetch(API + '/health')).json();
document.getElementById('provider-list').innerHTML = data.providers.map(p => `
<div class="card">
<div class="label">${p.name.toUpperCase()}</div>
<div class="value" style="font-size:18px">${p.configured ? '✓ Active' : '✗ Not configured'}</div>
<div class="sub">${p.models} model${p.models !== 1 ? 's' : ''} available</div>
</div>
`).join('');
} catch {}
}
async function loadRtk() {
try {
const [stats, commands, hosts] = await Promise.all([
(await fetch(API + '/rtk/stats?period=all')).json(),
(await fetch(API + '/rtk/commands')).json(),
(await fetch(API + '/rtk/hosts')).json(),
]);
document.getElementById('rtk-stats-cards').innerHTML = `
<div class="card"><div class="label">Total Saved</div><div class="value green">${fmtK(stats.total_saved_tokens)}</div><div class="sub">tokens across all hosts</div></div>
<div class="card"><div class="label">Commands</div><div class="value primary">${fmtK(stats.total_commands)}</div><div class="sub">via RTK</div></div>
<div class="card"><div class="label">Avg Savings</div><div class="value amber">${stats.avg_savings_pct.toFixed(1)}%</div><div class="sub">per command</div></div>
<div class="card"><div class="label">Input Tokens</div><div class="value">${fmtK(stats.total_input_tokens)}</div><div class="sub">before RTK</div></div>
<div class="card"><div class="label">Output Tokens</div><div class="value">${fmtK(stats.total_output_tokens)}</div><div class="sub">after RTK</div></div>
<div class="card"><div class="label">Hosts</div><div class="value">${stats.unique_hosts}</div><div class="sub">machines tracked</div></div>
`;
document.getElementById('rtk-commands-rows').innerHTML = commands.commands.map(c => `
<tr>
<td><code style="font-size:12px">${c.cmd}</code></td>
<td>${c.count}</td>
<td style="color:var(--green)">${fmtK(c.saved_tokens)}</td>
<td>${c.avg_savings_pct.toFixed(1)}%</td>
</tr>
`).join('') || '<tr><td colspan="4" style="text-align:center;color:var(--text-muted)">No RTK data yet — run <code>tokenvault-rtk-sync</code> to ingest</td></tr>';
document.getElementById('rtk-hosts-rows').innerHTML = hosts.hosts.map(h => `
<tr>
<td><strong>${h.host}</strong></td>
<td>${fmtK(h.commands)}</td>
<td style="color:var(--green)">${fmtK(h.saved_tokens)}</td>
<td style="font-size:11px;color:var(--text-muted)">${fmtTime(h.last_seen)}</td>
</tr>
`).join('') || '<tr><td colspan="4" style="text-align:center;color:var(--text-muted)">No hosts synced yet</td></tr>';
} catch (err) {
document.getElementById('rtk-stats-cards').innerHTML = '<div class="card"><div class="label">RTK</div><div class="value">Offline</div><div class="sub">Core endpoint unreachable</div></div>';
}
}
function loadAll() { loadStats(); loadTickets(); loadBreakdown(); loadProviders(); loadRtk(); }
loadAll();
setInterval(loadAll, 30000);
// ─── Settings ──────────────────────────────────────────────────────────────
const settingsPages = {
providers: () => `
<h3>${lang==='de'?'LLM Provider Konfiguration':'LLM Provider Configuration'}</h3>
<p class="desc">${lang==='de'?'API-Schlüssel für jeden Provider. Keys werden serverseitig gespeichert und nie im Browser angezeigt.':'API keys for each provider. Keys are stored server-side and never shown in the browser.'}</p>
<div class="provider-card">
<h4><span class="status-dot" id="dot-anthropic"></span> Anthropic Claude</h4>
<div class="form-row">
<div class="form-group">
<label>API Key</label>
<input type="password" id="key-anthropic" placeholder="sk-ant-..." />
<div class="hint">Claude Opus, Sonnet, Haiku — api.anthropic.com</div>
</div>
<div class="form-group">
<label>${lang==='de'?'Standard-Modell':'Default Model'}</label>
<select id="model-anthropic">
<option value="claude-sonnet-4-20250514">Claude Sonnet 4</option>
<option value="claude-haiku-3-20250630">Claude Haiku 3</option>
<option value="claude-opus-4-20250514">Claude Opus 4</option>
</select>
</div>
</div>
</div>
<div class="provider-card">
<h4><span class="status-dot" id="dot-openai"></span> OpenAI</h4>
<div class="form-row">
<div class="form-group">
<label>API Key</label>
<input type="password" id="key-openai" placeholder="sk-..." />
<div class="hint">GPT-4o, GPT-4o-mini, o1 — api.openai.com</div>
</div>
<div class="form-group">
<label>${lang==='de'?'Standard-Modell':'Default Model'}</label>
<select id="model-openai">
<option value="gpt-4o">GPT-4o</option>
<option value="gpt-4o-mini">GPT-4o Mini</option>
<option value="o1">o1</option>
</select>
</div>
</div>
</div>
<div class="provider-card">
<h4><span class="status-dot" id="dot-google"></span> Google Gemini</h4>
<div class="form-row">
<div class="form-group">
<label>API Key</label>
<input type="password" id="key-google" placeholder="AIza..." />
<div class="hint">Gemini 2.0 Flash, Gemini 2.5 Pro</div>
</div>
<div class="form-group">
<label>${lang==='de'?'Standard-Modell':'Default Model'}</label>
<select id="model-google">
<option value="gemini-2.0-flash">Gemini 2.0 Flash</option>
<option value="gemini-2.5-pro">Gemini 2.5 Pro</option>
</select>
</div>
</div>
</div>
<div class="provider-card">
<h4><span class="status-dot" id="dot-mistral"></span> Mistral AI</h4>
<div class="form-group">
<label>API Key</label>
<input type="password" id="key-mistral" placeholder="..." />
<div class="hint">Mistral Large, Mistral Small</div>
</div>
</div>
<div class="provider-card">
<h4><span class="status-dot" id="dot-groq"></span> Groq</h4>
<div class="form-group">
<label>API Key</label>
<input type="password" id="key-groq" placeholder="gsk_..." />
<div class="hint">${lang==='de'?'Kostenlos — Llama 3.3 70B, Gemma':'Free tier — Llama 3.3 70B, Gemma'}</div>
</div>
</div>
<div class="provider-card">
<h4><span class="status-dot green"></span> Ollama (Local)</h4>
<div class="form-group">
<label>Ollama URL</label>
<input type="text" id="url-ollama" placeholder="http://localhost:11434" />
<div class="hint">${lang==='de'?'Lokale Modelle — kostenlos, keine API-Keys nötig':'Local models — free, no API keys needed'}</div>
</div>
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="saveSettings('providers')">${lang==='de'?'Speichern':'Save Changes'}</button>
<button class="btn btn-outline" onclick="testProviders()">${lang==='de'?'Verbindung testen':'Test Connections'}</button>
</div>
`,
budgets: () => `
<h3>${lang==='de'?'Budgets & Alerts':'Budgets & Alerts'}</h3>
<p class="desc">${lang==='de'?'Setze Ausgabenlimits pro Projekt, Team oder global. Bei Überschreitung werden Alerts ausgelöst oder Requests pausiert.':'Set spending limits per project, team, or globally. Exceeding triggers alerts or pauses requests.'}</p>
<div class="form-group">
<label>${lang==='de'?'Globales Monatslimit (USD)':'Global Monthly Limit (USD)'}</label>
<input type="number" id="budget-global" placeholder="100.00" step="0.01" />
<div class="hint">${lang==='de'?'Alle Provider zusammen. 0 = kein Limit.':'All providers combined. 0 = no limit.'}</div>
</div>
<div class="form-row">
<div class="form-group">
<label>${lang==='de'?'Alert-Schwelle':'Alert Threshold'}</label>
<select id="budget-threshold">
<option value="0.5">50%</option>
<option value="0.7">70%</option>
<option value="0.8" selected>80%</option>
<option value="0.9">90%</option>
<option value="0.95">95%</option>
</select>
<div class="hint">${lang==='de'?'Alert bei diesem Prozentsatz des Limits':'Alert at this percentage of limit'}</div>
</div>
<div class="form-group">
<label>${lang==='de'?'Hard-Limit':'Hard Limit'}</label>
<div style="display:flex;align-items:center;gap:8px;padding-top:4px">
<label class="toggle"><input type="checkbox" id="budget-hard"><span class="slider"></span></label>
<span style="font-size:13px">${lang==='de'?'Requests bei 100% pausieren':'Pause requests at 100%'}</span>
</div>
</div>
</div>
<div class="divider"></div>
<h3>${lang==='de'?'Projekt-Budgets':'Project Budgets'}</h3>
<div id="project-budgets">
<div class="form-row" style="margin-bottom:8px">
<div class="form-group"><label>Projekt</label><input type="text" placeholder="eo-global-pulse" /></div>
<div class="form-group"><label>Limit (USD/Monat)</label><input type="number" placeholder="25.00" step="0.01" /></div>
</div>
<div class="form-row" style="margin-bottom:8px">
<div class="form-group"><input type="text" placeholder="tip-platform" /></div>
<div class="form-group"><input type="number" placeholder="50.00" step="0.01" /></div>
</div>
</div>
<button class="btn btn-outline" onclick="addProjectBudget()">+ ${lang==='de'?'Projekt hinzufügen':'Add Project'}</button>
<div class="divider"></div>
<h3>${lang==='de'?'Anomalie-Erkennung':'Anomaly Detection'}</h3>
<div class="form-row">
<div class="form-group">
<label>${lang==='de'?'Sigma-Schwelle':'Sigma Threshold'}</label>
<select id="anomaly-sigma">
<option value="2">2σ (${lang==='de'?'Empfindlich':'Sensitive'})</option>
<option value="3" selected>3σ (Standard)</option>
<option value="4">4σ (${lang==='de'?'Nur Extremfälle':'Extreme only'})</option>
</select>
</div>
<div class="form-group">
<label>${lang==='de'?'Anomalie-Erkennung aktiv':'Anomaly Detection Active'}</label>
<div style="padding-top:4px"><label class="toggle"><input type="checkbox" id="anomaly-enabled" checked><span class="slider"></span></label></div>
</div>
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="saveSettings('budgets')">${lang==='de'?'Speichern':'Save Changes'}</button>
</div>
`,
cache: () => `
<h3>Semantic Cache</h3>
<p class="desc">${lang==='de'?'Qdrant-basierter Cache. Ähnliche Anfragen werden aus dem Cache beantwortet statt die API erneut aufzurufen.':'Qdrant-powered cache. Similar requests are answered from cache instead of calling the API again.'}</p>
<div class="form-row">
<div class="form-group">
<label>${lang==='de'?'Cache aktiv':'Cache Enabled'}</label>
<div style="padding-top:4px"><label class="toggle"><input type="checkbox" id="cache-enabled" checked><span class="slider"></span></label></div>
</div>
<div class="form-group">
<label>${lang==='de'?'Ähnlichkeitsschwelle':'Similarity Threshold'}</label>
<input type="number" id="cache-threshold" value="0.95" min="0.80" max="0.99" step="0.01" />
<div class="hint">${lang==='de'?'0.95 = 95% Ähnlichkeit für Cache-Hit':'0.95 = 95% similarity for cache hit'}</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Cache TTL</label>
<select id="cache-ttl">
<option value="3600">1 ${lang==='de'?'Stunde':'hour'}</option>
<option value="86400" selected>24 ${lang==='de'?'Stunden':'hours'}</option>
<option value="604800">7 ${lang==='de'?'Tage':'days'}</option>
<option value="2592000">30 ${lang==='de'?'Tage':'days'}</option>
<option value="0">${lang==='de'?'Kein Ablauf':'No expiry'}</option>
</select>
</div>
<div class="form-group">
<label>Qdrant URL</label>
<input type="text" id="cache-qdrant" placeholder="http://localhost:6333" />
</div>
</div>
<div class="form-group">
<label>Qdrant Collection</label>
<input type="text" id="cache-collection" placeholder="tokenvault_cache" />
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="saveSettings('cache')">${lang==='de'?'Speichern':'Save'}</button>
<button class="btn btn-outline" onclick="flushCache()">${lang==='de'?'Cache leeren':'Flush Cache'}</button>
</div>
`,
compression: () => `
<h3>${lang==='de'?'Kompression':'Compression'}</h3>
<p class="desc">${lang==='de'?'AST-basierte Token-Kompression für Code und CLI-Output. Reduziert Token-Verbrauch um 60-90%.':'AST-based token compression for code and CLI output. Reduces token usage by 60-90%.'}</p>
<div class="form-group">
<label>${lang==='de'?'Auto-Modus':'Auto Mode'}</label>
<div style="display:flex;align-items:center;gap:8px;padding-top:4px">
<label class="toggle"><input type="checkbox" id="compress-auto" checked><span class="slider"></span></label>
<span style="font-size:13px">${lang==='de'?'Automatisch besten Modus wählen (Entropy-Analyse)':'Automatically select best mode (entropy analysis)'}</span>
</div>
</div>
<div class="form-group">
<label>${lang==='de'?'Standard-Modus (wenn Auto aus)':'Default Mode (when auto off)'}</label>
<select id="compress-mode">
<option value="full">Full (${lang==='de'?'keine Kompression':'no compression'})</option>
<option value="signatures" selected>Signatures (AST)</option>
<option value="map">Map (${lang==='de'?'Abhängigkeitsgraph':'dependency graph'})</option>
<option value="aggressive">Aggressive</option>
<option value="entropy">Entropy</option>
</select>
</div>
<div class="form-group">
<label>${lang==='de'?'Shell-Hooks aktiv':'Shell Hooks Active'}</label>
<div style="display:flex;align-items:center;gap:8px;padding-top:4px">
<label class="toggle"><input type="checkbox" id="compress-shell" checked><span class="slider"></span></label>
<span style="font-size:13px">${lang==='de'?'90+ Patterns für git, npm, docker, cargo, kubectl...':'90+ patterns for git, npm, docker, cargo, kubectl...'}</span>
</div>
</div>
<div class="form-group">
<label>${lang==='de'?'Prompt Caching':'Prompt Caching'}</label>
<div style="display:flex;align-items:center;gap:8px;padding-top:4px">
<label class="toggle"><input type="checkbox" id="compress-prompt-cache" checked><span class="slider"></span></label>
<span style="font-size:13px">${lang==='de'?'Anthropic cache_control + OpenAI automatisches Caching orchestrieren':'Orchestrate Anthropic cache_control + OpenAI automatic caching'}</span>
</div>
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="saveSettings('compression')">${lang==='de'?'Speichern':'Save'}</button>
</div>
`,
routing: () => `
<h3>${lang==='de'?'Model Routing':'Model Routing'}</h3>
<p class="desc">${lang==='de'?'Automatisches Routing zum günstigsten/schnellsten Modell basierend auf Anfragekomplexität.':'Automatic routing to cheapest/fastest model based on request complexity.'}</p>
<div class="form-group">
<label>${lang==='de'?'Routing-Strategie':'Routing Strategy'}</label>
<select id="route-strategy">
<option value="cost">${lang==='de'?'Günstigstes Modell':'Cheapest model'}</option>
<option value="quality">${lang==='de'?'Bestes Modell':'Best model'}</option>
<option value="balanced" selected>${lang==='de'?'Balanced (Kosten vs. Qualität)':'Balanced (cost vs. quality)'}</option>
<option value="latency">${lang==='de'?'Schnellstes Modell':'Fastest model'}</option>
<option value="manual">${lang==='de'?'Manuell (kein Auto-Routing)':'Manual (no auto-routing)'}</option>
</select>
</div>
<div class="form-group">
<label>${lang==='de'?'Fallback-Kette':'Fallback Chain'}</label>
<textarea id="route-fallback" rows="4" placeholder="anthropic/claude-sonnet-4-20250514&#10;openai/gpt-4o&#10;ollama/qwen2.5:14b&#10;groq/llama-3.3-70b"></textarea>
<div class="hint">${lang==='de'?'Ein Modell pro Zeile. Bei Ausfall wird das nächste verwendet.':'One model per line. Next is used on failure.'}</div>
</div>
<div class="form-row">
<div class="form-group">
<label>${lang==='de'?'Max Tokens pro Request':'Max Tokens per Request'}</label>
<input type="number" id="route-max-tokens" value="4096" />
</div>
<div class="form-group">
<label>${lang==='de'?'Standard-Temperatur':'Default Temperature'}</label>
<input type="number" id="route-temp" value="0.7" min="0" max="2" step="0.1" />
</div>
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="saveSettings('routing')">${lang==='de'?'Speichern':'Save'}</button>
</div>
`,
governance: () => `
<h3>${lang==='de'?'Governance & Zugriffskontrolle':'Governance & Access Control'}</h3>
<p class="desc">${lang==='de'?'Wer darf welches Modell nutzen? Rate Limits pro Team/User.':'Who can use which model? Rate limits per team/user.'}</p>
<div class="form-group">
<label>${lang==='de'?'RBAC aktiv':'RBAC Enabled'}</label>
<div style="display:flex;align-items:center;gap:8px;padding-top:4px">
<label class="toggle"><input type="checkbox" id="gov-rbac"><span class="slider"></span></label>
<span style="font-size:13px">${lang==='de'?'Rollenbasierte Zugriffskontrolle aktivieren':'Enable role-based access control'}</span>
</div>
</div>
<div class="form-group">
<label>${lang==='de'?'Erlaubte Modell-Tiers':'Allowed Model Tiers'}</label>
<div style="display:flex;gap:16px;padding-top:4px">
<label style="font-size:13px"><input type="checkbox" checked> Fast (Haiku, Mini)</label>
<label style="font-size:13px"><input type="checkbox" checked> Standard (Sonnet, GPT-4o)</label>
<label style="font-size:13px"><input type="checkbox"> Premium (Opus, o1)</label>
<label style="font-size:13px"><input type="checkbox"> Reasoning (o1)</label>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Rate Limit (${lang==='de'?'Requests/Minute':'Requests/Minute'})</label>
<input type="number" id="gov-rate" value="60" />
</div>
<div class="form-group">
<label>${lang==='de'?'Max Token/Request':'Max Tokens/Request'}</label>
<input type="number" id="gov-max-tokens" value="8192" />
</div>
</div>
<div class="divider"></div>
<h3>${lang==='de'?'Audit Trail':'Audit Trail'}</h3>
<div class="form-group">
<label>${lang==='de'?'Audit-Logging':'Audit Logging'}</label>
<div style="display:flex;align-items:center;gap:8px;padding-top:4px">
<label class="toggle"><input type="checkbox" id="gov-audit" checked><span class="slider"></span></label>
<span style="font-size:13px">${lang==='de'?'Alle Aktionen in immutablem Log speichern (GDPR/SOC2)':'Log all actions to immutable audit trail (GDPR/SOC2)'}</span>
</div>
</div>
<div class="form-group">
<label>${lang==='de'?'PII-Scrubbing':'PII Scrubbing'}</label>
<div style="display:flex;align-items:center;gap:8px;padding-top:4px">
<label class="toggle"><input type="checkbox" id="gov-pii" checked><span class="slider"></span></label>
<span style="font-size:13px">${lang==='de'?'Persönliche Daten aus Ticket-Logs entfernen':'Remove personal data from ticket logs'}</span>
</div>
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="saveSettings('governance')">${lang==='de'?'Speichern':'Save'}</button>
</div>
`,
notifications: () => `
<h3>${lang==='de'?'Benachrichtigungen':'Notifications'}</h3>
<p class="desc">${lang==='de'?'Werde benachrichtigt bei Budget-Überschreitungen, Anomalien und Ausfällen.':'Get notified on budget exceedances, anomalies, and outages.'}</p>
<div class="form-group">
<label>${lang==='de'?'E-Mail Benachrichtigungen':'Email Notifications'}</label>
<input type="email" id="notify-email" placeholder="rf@context-x.org" />
</div>
<div class="form-group">
<label>Webhook URL</label>
<input type="url" id="notify-webhook" placeholder="https://hooks.slack.com/..." />
<div class="hint">${lang==='de'?'Slack, Teams, Discord oder benutzerdefinierter Webhook':'Slack, Teams, Discord, or custom webhook'}</div>
</div>
<div class="divider"></div>
<h3>${lang==='de'?'Benachrichtigungs-Events':'Notification Events'}</h3>
<div style="display:flex;flex-direction:column;gap:8px">
<label style="font-size:13px;display:flex;align-items:center;gap:8px"><input type="checkbox" checked> ${lang==='de'?'Budget-Alert (Schwelle erreicht)':'Budget alert (threshold reached)'}</label>
<label style="font-size:13px;display:flex;align-items:center;gap:8px"><input type="checkbox" checked> ${lang==='de'?'Budget-Limit (100% erreicht)':'Budget limit (100% reached)'}</label>
<label style="font-size:13px;display:flex;align-items:center;gap:8px"><input type="checkbox" checked> ${lang==='de'?'Kosten-Anomalie erkannt':'Cost anomaly detected'}</label>
<label style="font-size:13px;display:flex;align-items:center;gap:8px"><input type="checkbox"> ${lang==='de'?'Provider-Ausfall':'Provider outage'}</label>
<label style="font-size:13px;display:flex;align-items:center;gap:8px"><input type="checkbox"> ${lang==='de'?'Täglicher Kostenreport':'Daily cost report'}</label>
<label style="font-size:13px;display:flex;align-items:center;gap:8px"><input type="checkbox"> ${lang==='de'?'Wöchentlicher Sparreport':'Weekly savings report'}</label>
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="saveSettings('notifications')">${lang==='de'?'Speichern':'Save'}</button>
<button class="btn btn-outline" onclick="testNotification()">${lang==='de'?'Test senden':'Send Test'}</button>
</div>
`,
database: () => `
<h3>${lang==='de'?'Datenbank':'Database'}</h3>
<p class="desc">${lang==='de'?'PostgreSQL + TimescaleDB Konfiguration für Ticket-Speicherung und Zeitreihen.':'PostgreSQL + TimescaleDB configuration for ticket storage and time-series.'}</p>
<div class="form-row">
<div class="form-group">
<label>Host</label>
<input type="text" id="db-host" placeholder="127.0.0.1" />
</div>
<div class="form-group">
<label>Port</label>
<input type="number" id="db-port" placeholder="5432" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Database</label>
<input type="text" id="db-name" placeholder="tokenvault" />
</div>
<div class="form-group">
<label>User</label>
<input type="text" id="db-user" placeholder="tokenvault" />
</div>
</div>
<div class="divider"></div>
<h3>${lang==='de'?'Datenbereinigung':'Data Retention'}</h3>
<div class="form-row">
<div class="form-group">
<label>${lang==='de'?'Tickets aufbewahren':'Retain Tickets'}</label>
<select id="db-retention">
<option value="30">30 ${lang==='de'?'Tage':'days'}</option>
<option value="90" selected>90 ${lang==='de'?'Tage':'days'}</option>
<option value="365">1 ${lang==='de'?'Jahr':'year'}</option>
<option value="0">${lang==='de'?'Unbegrenzt':'Forever'}</option>
</select>
</div>
<div class="form-group">
<label>${lang==='de'?'Audit-Logs aufbewahren':'Retain Audit Logs'}</label>
<select id="db-audit-retention">
<option value="365" selected>1 ${lang==='de'?'Jahr':'year'}</option>
<option value="730">2 ${lang==='de'?'Jahre':'years'}</option>
<option value="0">${lang==='de'?'Unbegrenzt':'Forever'}</option>
</select>
</div>
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="saveSettings('database')">${lang==='de'?'Speichern':'Save'}</button>
<button class="btn btn-outline" onclick="testDb()">${lang==='de'?'Verbindung testen':'Test Connection'}</button>
</div>
`,
general: () => `
<h3>${lang==='de'?'Allgemein':'General'}</h3>
<p class="desc">${lang==='de'?'Globale Einstellungen für den TokenVault Server.':'Global settings for the TokenVault server.'}</p>
<div class="form-row">
<div class="form-group">
<label>${lang==='de'?'Server-Port':'Server Port'}</label>
<input type="number" id="gen-port" placeholder="3350" />
</div>
<div class="form-group">
<label>${lang==='de'?'Dashboard-Port':'Dashboard Port'}</label>
<input type="number" id="gen-dash-port" placeholder="3301" />
</div>
</div>
<div class="form-group">
<label>${lang==='de'?'Sprache':'Language'}</label>
<select id="gen-lang">
<option value="en">English</option>
<option value="de">Deutsch</option>
</select>
</div>
<div class="form-group">
<label>${lang==='de'?'Auto-Refresh Intervall':'Auto-Refresh Interval'}</label>
<select id="gen-refresh">
<option value="10">10s</option>
<option value="30" selected>30s</option>
<option value="60">60s</option>
<option value="0">${lang==='de'?'Aus':'Off'}</option>
</select>
</div>
<div class="form-group">
<label>${lang==='de'?'Theme':'Theme'}</label>
<select id="gen-theme">
<option value="light" selected>Light (MAGATAMA)</option>
<option value="dark">Dark</option>
</select>
</div>
<div class="divider"></div>
<h3>${lang==='de'?'Gefährliche Zone':'Danger Zone'}</h3>
<div class="form-group">
<button class="btn btn-danger" onclick="if(confirm('${lang==='de'?'Alle Tickets löschen?':'Delete all tickets?'}')) resetData()">${lang==='de'?'Alle Daten zurücksetzen':'Reset All Data'}</button>
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="saveSettings('general')">${lang==='de'?'Speichern':'Save'}</button>
</div>
`,
about: () => `
<h3>TokenVault</h3>
<p class="desc">${lang==='de'?'Hybrid MCP + Proxy Plattform für LLM Token-Einsparung':'Hybrid MCP + Proxy platform for LLM token savings'}</p>
<div style="display:grid;grid-template-columns:120px 1fr;gap:8px;font-size:13px;margin-bottom:20px">
<strong>Version</strong><span>0.1.0 (MVP)</span>
<strong>${lang==='de'?'Lizenz':'License'}</strong><span>Apache-2.0</span>
<strong>${lang==='de'?'Autor':'Author'}</strong><span>Context X (context-x.org)</span>
<strong>Gitea</strong><span><a href="https://gitea.context-x.org/rene/tokenvault" target="_blank" style="color:var(--primary)">rene/tokenvault</a></span>
<strong>Stack</strong><span>Fastify 5.x, PostgreSQL 17, Qdrant, TypeScript</span>
<strong>Packages</strong><span>@tokenvault/core, /mcp, /client, /dashboard</span>
</div>
<div class="divider"></div>
<h3>${lang==='de'?'Provider':'Providers'}</h3>
<div style="font-size:13px;line-height:2">
Anthropic (Claude) · OpenAI (GPT) · Google (Gemini) · Mistral · Groq · Cerebras · Ollama
</div>
<div class="divider"></div>
<h3>${lang==='de'?'Funktionen':'Features'}</h3>
<div style="font-size:13px;line-height:2">
✓ OpenAI-${lang==='de'?'kompatibler Proxy':'compatible proxy'} ·
✓ Ticket-System (TV-00001) ·
${lang==='de'?'Kosten-Tracking':'Cost tracking'} ·
${lang==='de'?'Multi-Provider Routing':'Multi-provider routing'} ·
✓ MCP Server ·
✓ TypeScript SDK<br>
◻ AST-Compression (Phase 2) ·
◻ Semantic Cache (Phase 2) ·
◻ Prompt Caching (Phase 2) ·
◻ Budget Enforcement (Phase 3) ·
◻ Anomaly Detection (Phase 3)
</div>
`,
};
function showSettings(page) {
document.querySelectorAll('.settings-nav-item').forEach(i => i.classList.toggle('active', i.dataset.settings === page));
const panel = document.getElementById('settings-content');
panel.innerHTML = settingsPages[page]?.() ?? '<p>Not found</p>';
if (page === 'providers') updateProviderDots();
}
async function updateProviderDots() {
try {
const data = await (await fetch(API + '/health')).json();
data.providers.forEach(p => {
const dot = document.getElementById('dot-' + p.name);
if (dot) { dot.className = 'status-dot ' + (p.configured ? 'green' : 'red'); }
});
} catch {}
}
function saveSettings(section) {
// MVP: show toast, Phase 2 will persist to API
const toast = document.createElement('div');
toast.style.cssText = 'position:fixed;bottom:24px;right:24px;background:var(--green);color:#fff;padding:12px 20px;border-radius:8px;font-size:14px;font-weight:500;z-index:999;animation:fadeIn 0.2s';
toast.textContent = lang === 'de' ? 'Einstellungen gespeichert' : 'Settings saved';
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
function addProjectBudget() {
const container = document.getElementById('project-budgets');
const row = document.createElement('div');
row.className = 'form-row';
row.style.marginBottom = '8px';
row.innerHTML = '<div class="form-group"><input type="text" placeholder="project-name" /></div><div class="form-group"><input type="number" placeholder="0.00" step="0.01" /></div>';
container.appendChild(row);
}
function testProviders() { saveSettings('providers'); }
function testNotification() { saveSettings('notifications'); }
function testDb() { saveSettings('database'); }
function flushCache() { if(confirm(lang==='de'?'Cache wirklich leeren?':'Flush cache?')) saveSettings('cache'); }
function resetData() { saveSettings('general'); }
// Init settings when tab shown
showSettings('providers');
</script>
</body>
</html>