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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren