feat: Credits-System — Token-Usage-Router, Budget-Verwaltung, Frontend-Übersicht

- Neuer Router /api/token-usage mit Overview, Org-Detail, Monatsstatistik
- Budget-Felder (credits_total, cost_per_credit, token_budget_usd) bei Lizenz-Erstellung
- Token-Nutzung Sub-Tab in Org-Detail mit Verbrauchsbalken und Monatstabelle
- Dashboard Stat-Card für API-Kosten gesamt
- CSS Dark-Theme Styling für Token-Komponenten
Dieser Commit ist enthalten in:
Claude Dev
2026-03-17 23:59:49 +01:00
Ursprung 3828d7c8bf
Commit 7cd36959b0
7 geänderte Dateien mit 333 neuen und 4 gelöschten Zeilen

Datei anzeigen

@@ -43,6 +43,7 @@ document.addEventListener("DOMContentLoaded", () => {
setupOrgDetailTabs();
setupForms();
loadDashboard();
loadDashboardTokenStats();
loadOrgs();
});
@@ -75,6 +76,7 @@ function setupOrgDetailTabs() {
tab.classList.add("active");
document.querySelectorAll("#orgDetail > .section").forEach(s => s.classList.remove("active"));
document.getElementById(`sub-${subtab}`).classList.add("active");
if (subtab === 'tokens' && currentOrgId) loadOrgTokenUsage(currentOrgId);
});
});
@@ -465,6 +467,12 @@ function setupForms() {
if (licType !== "permanent") {
body.duration_days = parseInt(document.getElementById("newLicDuration").value);
}
const creditsTotal = document.getElementById('newLicCreditsTotal');
const costPerCredit = document.getElementById('newLicCostPerCredit');
const budgetUsd = document.getElementById('newLicBudgetUsd');
if (creditsTotal && creditsTotal.value) body.credits_total = parseInt(creditsTotal.value);
if (costPerCredit && costPerCredit.value) body.cost_per_credit = parseFloat(costPerCredit.value);
if (budgetUsd && budgetUsd.value) body.token_budget_usd = parseFloat(budgetUsd.value);
try {
await API.post("/api/licenses", body);
closeModal("modalNewLicense");
@@ -557,3 +565,74 @@ function formatDate(iso) {
return iso;
}
}
// ===== Token-Nutzung =====
async function loadOrgTokenUsage(orgId) {
try {
const [current, monthly] = await Promise.all([
API.get('/api/token-usage/' + orgId + '/current'),
API.get('/api/token-usage/' + orgId),
]);
const budget = current.budget || {};
const usage = current.usage || {};
const el = (id, val) => { const e = document.getElementById(id); if (e) e.textContent = val; };
el('tokenCreditsUsed', budget.credits_used != null ? Math.round(budget.credits_used).toLocaleString('de-DE') : '-');
el('tokenCreditsRemaining', budget.credits_remaining != null ? budget.credits_remaining.toLocaleString('de-DE') : '-');
el('tokenBudgetUsd', budget.token_budget_usd != null ? '$' + Number(budget.token_budget_usd).toFixed(2) : '-');
el('tokenCostUsd', usage.total_cost_usd != null ? '$' + Number(usage.total_cost_usd).toFixed(2) : '-');
const bar = document.getElementById('tokenBudgetBar');
const percentEl = document.getElementById('tokenBudgetPercent');
const percent = budget.credits_percent_used || 0;
if (bar) {
bar.style.width = Math.min(100, percent) + '%';
bar.classList.remove('warning', 'critical');
if (percent > 80) bar.classList.add('critical');
else if (percent > 50) bar.classList.add('warning');
}
if (percentEl) percentEl.textContent = percent.toFixed(1) + '%';
const tbody = document.getElementById('tokenMonthlyTable');
if (tbody) {
tbody.innerHTML = monthly.map(function(m) {
return '<tr>' +
'<td>' + esc(m.year_month) + '</td>' +
'<td>' + m.refresh_count + '</td>' +
'<td>' + m.api_calls.toLocaleString('de-DE') + '</td>' +
'<td>' + m.input_tokens.toLocaleString('de-DE') + '</td>' +
'<td>' + m.output_tokens.toLocaleString('de-DE') + '</td>' +
'<td>$' + Number(m.total_cost_usd).toFixed(2) + '</td>' +
'</tr>';
}).join('');
}
} catch (err) {
console.error('Token-Usage laden fehlgeschlagen:', err);
}
}
async function loadDashboardTokenStats() {
try {
const data = await API.get('/api/token-usage/overview');
const grid = document.getElementById('statsGrid');
if (grid && data.length > 0) {
const totals = data.reduce(function(acc, org) {
return {
cost: acc.cost + (org.total_cost_usd || 0),
refreshes: acc.refreshes + (org.total_refreshes || 0),
};
}, { cost: 0, refreshes: 0 });
const card = document.createElement('div');
card.className = 'stat-card';
card.innerHTML = '<div class="stat-label">API-Kosten gesamt</div>' +
'<div class="stat-value">$' + totals.cost.toFixed(2) + '</div>' +
'<div class="stat-sub">' + totals.refreshes + ' Refreshes</div>';
grid.appendChild(card);
}
} catch (err) {
console.error('Token-Overview laden fehlgeschlagen:', err);
}
}