feat: Token-Nutzung nach Quelle (Monitor/Globe) aufgeschluesselt
- Backend liefert usage_by_source im current-Endpoint - Monatliche Tabelle zeigt Quelle-Badge (Monitor/Globe) - Source-Split unter den Kosten-KPIs sichtbar Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -76,6 +76,7 @@ async def get_org_usage(org_id: int, admin=Depends(get_current_admin)):
|
|||||||
|
|
||||||
return [{
|
return [{
|
||||||
"year_month": row["year_month"],
|
"year_month": row["year_month"],
|
||||||
|
"source": row["source"] if "source" in row.keys() else "monitor",
|
||||||
"input_tokens": row["input_tokens"],
|
"input_tokens": row["input_tokens"],
|
||||||
"output_tokens": row["output_tokens"],
|
"output_tokens": row["output_tokens"],
|
||||||
"cache_creation_tokens": row["cache_creation_tokens"],
|
"cache_creation_tokens": row["cache_creation_tokens"],
|
||||||
@@ -98,7 +99,26 @@ async def get_org_current_usage(org_id: int, admin=Depends(get_current_admin)):
|
|||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
"SELECT * FROM token_usage_monthly WHERE organization_id = ? AND year_month = ?",
|
"SELECT * FROM token_usage_monthly WHERE organization_id = ? AND year_month = ?",
|
||||||
(org_id, year_month))
|
(org_id, year_month))
|
||||||
usage = await cursor.fetchone()
|
usage_rows = await cursor.fetchall()
|
||||||
|
# Summe ueber alle Sources
|
||||||
|
usage = {
|
||||||
|
"input_tokens": sum(r["input_tokens"] for r in usage_rows),
|
||||||
|
"output_tokens": sum(r["output_tokens"] for r in usage_rows),
|
||||||
|
"total_cost_usd": sum(r["total_cost_usd"] for r in usage_rows),
|
||||||
|
"api_calls": sum(r["api_calls"] for r in usage_rows),
|
||||||
|
"refresh_count": sum(r["refresh_count"] for r in usage_rows),
|
||||||
|
}
|
||||||
|
# Per-Source Aufschluesselung
|
||||||
|
usage_by_source = {}
|
||||||
|
for r in usage_rows:
|
||||||
|
src = r["source"] if "source" in r.keys() else "monitor"
|
||||||
|
usage_by_source[src] = {
|
||||||
|
"input_tokens": r["input_tokens"],
|
||||||
|
"output_tokens": r["output_tokens"],
|
||||||
|
"total_cost_usd": round(r["total_cost_usd"], 2),
|
||||||
|
"api_calls": r["api_calls"],
|
||||||
|
"refresh_count": r["refresh_count"],
|
||||||
|
}
|
||||||
|
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
"SELECT credits_total, credits_used, cost_per_credit, token_budget_usd, budget_warning_percent FROM licenses WHERE organization_id = ? AND status = 'active' ORDER BY id DESC LIMIT 1",
|
"SELECT credits_total, credits_used, cost_per_credit, token_budget_usd, budget_warning_percent FROM licenses WHERE organization_id = ? AND status = 'active' ORDER BY id DESC LIMIT 1",
|
||||||
@@ -111,12 +131,13 @@ async def get_org_current_usage(org_id: int, admin=Depends(get_current_admin)):
|
|||||||
return {
|
return {
|
||||||
"year_month": year_month,
|
"year_month": year_month,
|
||||||
"usage": {
|
"usage": {
|
||||||
"input_tokens": usage["input_tokens"] if usage else 0,
|
"input_tokens": usage["input_tokens"],
|
||||||
"output_tokens": usage["output_tokens"] if usage else 0,
|
"output_tokens": usage["output_tokens"],
|
||||||
"total_cost_usd": round(usage["total_cost_usd"], 2) if usage else 0,
|
"total_cost_usd": round(usage["total_cost_usd"], 2),
|
||||||
"api_calls": usage["api_calls"] if usage else 0,
|
"api_calls": usage["api_calls"],
|
||||||
"refresh_count": usage["refresh_count"] if usage else 0,
|
"refresh_count": usage["refresh_count"],
|
||||||
},
|
},
|
||||||
|
"usage_by_source": usage_by_source,
|
||||||
"budget": {
|
"budget": {
|
||||||
"credits_total": credits_total,
|
"credits_total": credits_total,
|
||||||
"credits_used": round(credits_used, 1) if credits_used else 0,
|
"credits_used": round(credits_used, 1) if credits_used else 0,
|
||||||
|
|||||||
@@ -7,6 +7,12 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||||
<link rel="apple-touch-icon" href="/static/favicon.svg">
|
<link rel="apple-touch-icon" href="/static/favicon.svg">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.source-badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:12px; font-weight:600; }
|
||||||
|
.badge-monitor { background:#e3f2fd; color:#1565c0; }
|
||||||
|
.badge-globe { background:#e8f5e9; color:#2e7d32; }
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
@@ -188,6 +194,7 @@
|
|||||||
<div class="token-stat-value" id="tokenCostUsd">-</div>
|
<div class="token-stat-value" id="tokenCostUsd">-</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="tokenSourceSplit" style="margin-top:12px;font-size:14px;"></div>
|
||||||
<div class="token-budget-bar-section">
|
<div class="token-budget-bar-section">
|
||||||
<div class="token-budget-label">
|
<div class="token-budget-label">
|
||||||
<span>Budget-Auslastung</span>
|
<span>Budget-Auslastung</span>
|
||||||
@@ -202,6 +209,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Monat</th>
|
<th>Monat</th>
|
||||||
|
<th>Quelle</th>
|
||||||
<th>Refreshes</th>
|
<th>Refreshes</th>
|
||||||
<th>API-Calls</th>
|
<th>API-Calls</th>
|
||||||
<th>Input-Tokens</th>
|
<th>Input-Tokens</th>
|
||||||
|
|||||||
@@ -606,6 +606,14 @@ async function loadOrgTokenUsage(orgId) {
|
|||||||
el('tokenBudgetUsd', budget.token_budget_usd != null ? '$' + Number(budget.token_budget_usd).toFixed(2) : '-');
|
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) : '-');
|
el('tokenCostUsd', usage.total_cost_usd != null ? '$' + Number(usage.total_cost_usd).toFixed(2) : '-');
|
||||||
|
|
||||||
|
// Source-Split anzeigen
|
||||||
|
var bySource = current.usage_by_source || {};
|
||||||
|
var splitHtml = '';
|
||||||
|
if (bySource.monitor) splitHtml += '<span class="source-badge badge-monitor">Monitor</span> $' + Number(bySource.monitor.total_cost_usd).toFixed(2) + ' (' + bySource.monitor.api_calls + ' Calls) ';
|
||||||
|
if (bySource.globe) splitHtml += '<span class="source-badge badge-globe">Globe</span> $' + Number(bySource.globe.total_cost_usd).toFixed(2) + ' (' + bySource.globe.api_calls + ' Calls)';
|
||||||
|
var splitEl = document.getElementById('tokenSourceSplit');
|
||||||
|
if (splitEl) splitEl.innerHTML = splitHtml;
|
||||||
|
|
||||||
const bar = document.getElementById('tokenBudgetBar');
|
const bar = document.getElementById('tokenBudgetBar');
|
||||||
const percentEl = document.getElementById('tokenBudgetPercent');
|
const percentEl = document.getElementById('tokenBudgetPercent');
|
||||||
const percent = budget.credits_percent_used || 0;
|
const percent = budget.credits_percent_used || 0;
|
||||||
@@ -622,8 +630,11 @@ async function loadOrgTokenUsage(orgId) {
|
|||||||
const tbody = document.getElementById('tokenMonthlyTable');
|
const tbody = document.getElementById('tokenMonthlyTable');
|
||||||
if (tbody) {
|
if (tbody) {
|
||||||
tbody.innerHTML = monthly.map(function(m) {
|
tbody.innerHTML = monthly.map(function(m) {
|
||||||
|
var srcLabel = (m.source || 'monitor') === 'globe' ? 'Globe' : 'Monitor';
|
||||||
|
var srcClass = (m.source || 'monitor') === 'globe' ? 'badge-globe' : 'badge-monitor';
|
||||||
return '<tr>' +
|
return '<tr>' +
|
||||||
'<td>' + esc(m.year_month) + '</td>' +
|
'<td>' + esc(m.year_month) + '</td>' +
|
||||||
|
'<td><span class="source-badge ' + srcClass + '">' + srcLabel + '</span></td>' +
|
||||||
'<td>' + m.refresh_count + '</td>' +
|
'<td>' + m.refresh_count + '</td>' +
|
||||||
'<td>' + m.api_calls.toLocaleString('de-DE') + '</td>' +
|
'<td>' + m.api_calls.toLocaleString('de-DE') + '</td>' +
|
||||||
'<td>' + m.input_tokens.toLocaleString('de-DE') + '</td>' +
|
'<td>' + m.input_tokens.toLocaleString('de-DE') + '</td>' +
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren