diff --git a/Endpoints/ApiKeysEndpoint.cs b/Endpoints/ApiKeysEndpoint.cs
index c2dde47..3b79f06 100644
--- a/Endpoints/ApiKeysEndpoint.cs
+++ b/Endpoints/ApiKeysEndpoint.cs
@@ -1,5 +1,8 @@
using System;
using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Gott.Extensions;
@@ -20,6 +23,7 @@ public static class ApiKeysEndpoint
app.MapPut("/api/admin/api-keys/{keyId}/activate", HandleActivate);
app.MapPut("/api/admin/api-keys/{keyId}/deactivate", HandleDeactivate);
app.MapDelete("/api/admin/api-keys/{keyId}", HandleDelete);
+ app.MapPost("/api/admin/api-keys/{keyId}/check", HandleCheck);
}
private static async Task HandleGetAll(HttpContext ctx, DatabaseService db)
@@ -170,5 +174,125 @@ public static class ApiKeysEndpoint
await ctx.Response.WriteAsJsonAsync(new { ok = true });
}
+ private static async Task HandleCheck(HttpContext ctx, IHttpClientFactory httpFactory, DatabaseService db, string keyId)
+ {
+ if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
+ var existing = await db.GetApiKey(keyId);
+ if (existing == null)
+ {
+ ctx.Response.StatusCode = 404;
+ await ctx.Response.WriteAsJsonAsync(new { error = "Key nicht gefunden" });
+ return;
+ }
+
+ if (existing.Provider != "anthropic")
+ {
+ ctx.Response.StatusCode = 400;
+ await ctx.Response.WriteAsJsonAsync(new { error = "Check nur für Anthropic-Keys verfügbar" });
+ return;
+ }
+
+ try
+ {
+ var client = httpFactory.CreateClient();
+ client.Timeout = TimeSpan.FromSeconds(30);
+
+ var payload = JsonSerializer.Serialize(new
+ {
+ model = "claude-sonnet-4-20250514",
+ max_tokens = 1,
+ messages = new[] { new { role = "user", content = "hi" } }
+ });
+
+ var request = new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages");
+ request.Headers.Add("x-api-key", existing.ApiKey);
+ request.Headers.Add("anthropic-version", "2023-06-01");
+ request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
+
+ var resp = await client.SendAsync(request);
+ var headers = resp.Headers;
+
+ string GetHeader(string name)
+ {
+ if (headers.TryGetValues("anthropic-ratelimit-" + name, out var v1))
+ return string.Join("", v1);
+ if (headers.TryGetValues(name, out var v2))
+ return string.Join("", v2);
+ return null;
+ }
+
+ if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
+ {
+ await ctx.Response.WriteAsJsonAsync(new { status = "error", message = "Ungültig oder abgelaufen (401)" });
+ return;
+ }
+ if (resp.StatusCode == (System.Net.HttpStatusCode)429)
+ {
+ await ctx.Response.WriteAsJsonAsync(new { status = "error", message = "Rate Limited (429)" });
+ return;
+ }
+ if (resp.StatusCode == System.Net.HttpStatusCode.Forbidden)
+ {
+ await ctx.Response.WriteAsJsonAsync(new { status = "error", message = "Insufficient Quota (403)" });
+ return;
+ }
+ if ((int)resp.StatusCode >= 400)
+ {
+ await ctx.Response.WriteAsJsonAsync(new { status = "error", message = "API-Fehler (" + (int)resp.StatusCode + ")" });
+ return;
+ }
+
+ // Key ist gültig – Header auslesen
+ var d7Util = GetHeader("unified-7d-utilization");
+ var d5hUtil = GetHeader("unified-5h-utilization");
+ var overageUtil = GetHeader("unified-overage-utilization");
+ var d7Status = GetHeader("unified-7d-status");
+ var d5hStatus = GetHeader("unified-5h-status");
+ var overageStatus = GetHeader("unified-overage-status");
+
+ // Subscription-Keys haben unified-* Headers, API-Keys nicht
+ var isSubscription = d7Util != null || d5hUtil != null;
+ var fallbackPct = GetHeader("unified-fallback-percentage");
+
+ // Plan-Erkennung:
+ // - overage > 0 → definitiv MAX (Extra Usage aktiv, nur bei Max sinnvoll nutzbar)
+ // - kein fallback-percentage Header → PRO
+ // - sonst → Pro/Max (nicht unterscheidbar)
+ string keyType;
+ if (!isSubscription)
+ keyType = "API Console";
+ else if (overageUtil != null && double.TryParse(overageUtil, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var ovCheck) && ovCheck > 0)
+ keyType = "Max";
+ else if (fallbackPct == null)
+ keyType = "Pro";
+ else
+ keyType = "Pro/Max";
+
+ double? usage7d = null, usage5h = null, overage = null;
+ if (d7Util != null && double.TryParse(d7Util, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var d7))
+ usage7d = Math.Round(d7 * 100, 1);
+ if (d5hUtil != null && double.TryParse(d5hUtil, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var d5))
+ usage5h = Math.Round(d5 * 100, 1);
+ if (overageUtil != null && double.TryParse(overageUtil, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var ov))
+ overage = Math.Round(ov * 100, 1);
+
+ await ctx.Response.WriteAsJsonAsync(new
+ {
+ status = "ok",
+ keyType,
+ isSubscription,
+ usage7d,
+ usage5h,
+ overage,
+ status7d = d7Status,
+ status5h = d5hStatus,
+ statusOverage = overageStatus
+ });
+ }
+ catch (Exception ex)
+ {
+ await ctx.Response.WriteAsJsonAsync(new { status = "error", message = ex.Message });
+ }
+ }
}
diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js
index 16697c9..9263b06 100644
--- a/wwwroot/js/api.js
+++ b/wwwroot/js/api.js
@@ -645,6 +645,15 @@ async function apiDeleteApiKey(keyId) {
return resp.json();
}
+async function apiCheckApiKey(keyId) {
+ const resp = await fetch('/api/admin/api-keys/' + keyId + '/check', { method: 'POST' });
+ if (!resp.ok) {
+ const err = await resp.json().catch(() => ({}));
+ throw new Error(err.error || 'Fehler beim Prüfen');
+ }
+ return resp.json();
+}
+
// ── Krea Image API ──
async function apiKreaStatus() {
diff --git a/wwwroot/js/pages/api-keys-page.js b/wwwroot/js/pages/api-keys-page.js
index 81b489d..c7ded68 100644
--- a/wwwroot/js/pages/api-keys-page.js
+++ b/wwwroot/js/pages/api-keys-page.js
@@ -32,6 +32,8 @@ registerPage('api-keys', {
}
keys.forEach(function(key) {
+ var wrapper = document.createElement('div');
+
var row = document.createElement('div');
row.className = 'admin-user-row';
@@ -54,6 +56,21 @@ registerPage('api-keys', {
var actions = document.createElement('div');
actions.className = 'admin-user-actions';
+ // Status-Zeile (für Check-Ergebnis)
+ var statusLine = document.createElement('div');
+ statusLine.className = 'admin-user-meta';
+ statusLine.style.cssText = 'padding:0 var(--box-padding) .5rem;display:none;font-size:.85em';
+
+ // Check (nur Anthropic)
+ if (key.provider === 'anthropic') {
+ var checkBtn = document.createElement('button');
+ checkBtn.className = 'icon-btn';
+ checkBtn.title = 'Key prüfen';
+ checkBtn.innerHTML = '';
+ checkBtn.addEventListener('click', function() { checkKey(key, checkBtn, statusLine); });
+ actions.appendChild(checkBtn);
+ }
+
// Bearbeiten
var editBtn = document.createElement('button');
editBtn.className = 'icon-btn';
@@ -89,7 +106,9 @@ registerPage('api-keys', {
row.appendChild(info);
row.appendChild(actions);
- list.appendChild(row);
+ wrapper.appendChild(row);
+ wrapper.appendChild(statusLine);
+ list.appendChild(wrapper);
});
}
@@ -102,6 +121,39 @@ registerPage('api-keys', {
}
}
+ async function checkKey(key, btn, statusLine) {
+ var origHtml = btn.innerHTML;
+ btn.innerHTML = '';
+ btn.disabled = true;
+ statusLine.style.display = 'block';
+ statusLine.textContent = 'Prüfe…';
+ statusLine.style.opacity = '0.5';
+ try {
+ var result = await apiCheckApiKey(key.keyId);
+ statusLine.style.opacity = '1';
+ if (result.status === 'ok') {
+ var parts = ['✅ Gültig', result.keyType];
+ if (result.usage5h != null) parts.push('5h: ' + result.usage5h + '%');
+ if (result.usage7d != null) {
+ var s7d = '7d: ' + result.usage7d + '%';
+ if (result.status7d === 'allowed_warning') s7d += ' ⚠️';
+ else if (result.status7d === 'exceeded') s7d += ' 🛑';
+ parts.push(s7d);
+ }
+ if (result.overage != null && result.overage > 0) parts.push('Overage: ' + result.overage + '%');
+ statusLine.textContent = parts.join(' · ');
+ } else {
+ statusLine.textContent = '❌ ' + (result.message || 'Unbekannter Fehler');
+ }
+ } catch (err) {
+ statusLine.style.opacity = '1';
+ statusLine.textContent = '❌ ' + err.message;
+ } finally {
+ btn.innerHTML = origHtml;
+ btn.disabled = false;
+ }
+ }
+
function startEdit(key) {
editingKeyId = key.keyId;
formTitle.textContent = 'API-Key bearbeiten';