Dateien
AegisSight-Monitor-Verwaltung/src/static/js/app.js
claude-dev 5a87168416 Phase 3a Frontend-Hygiene: Toast statt alert/confirm
- src/static/css/style.css: Toast-Styles (.toast-container, .toast,
  Varianten info/success/warning/error, Animations)
- src/static/dashboard.html: <div id=toastContainer> vor </body>,
  Cancel-Button im Confirm-Modal bekommt id=confirmCancelBtn
- src/static/js/app.js:
  - showToast(msg, type) neu - links oben, autoclose 3.5s (error: 6s)
  - showConfirm(title, text, callback?) jetzt Promise<boolean>-fähig
    (Backwards-compat: Legacy-Callback wird bei OK weiter aufgerufen)
  - Cancel/Close-Hooks am modalConfirm setzen Promise auf false
- alle 18 alert() in app.js / source-health.js / sources.js durch
  showToast(msg, type) ersetzt (type je nach Kontext error/success/warning/info)
- 2 confirm() in source-health.js durch await showConfirm() ersetzt
2026-05-09 03:02:32 +00:00

836 Zeilen
31 KiB
JavaScript

/* Verwaltungsportal - Frontend Logic */
"use strict";
const API = {
token: localStorage.getItem("token"),
async request(path, opts = {}) {
const headers = { "Content-Type": "application/json" };
if (this.token) headers["Authorization"] = `Bearer ${this.token}`;
const res = await fetch(path, { ...opts, headers });
if (res.status === 401) {
localStorage.removeItem("token");
localStorage.removeItem("username");
window.location.href = "/";
return;
}
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.detail || `Fehler ${res.status}`);
}
if (res.status === 204) return null;
return res.json();
},
get(path) { return this.request(path); },
post(path, body) { return this.request(path, { method: "POST", body: JSON.stringify(body) }); },
put(path, body) { return this.request(path, { method: "PUT", body: body ? JSON.stringify(body) : undefined }); },
del(path) { return this.request(path, { method: "DELETE" }); },
};
// --- State ---
let currentOrgId = null;
let orgsCache = [];
// --- Init ---
document.addEventListener("DOMContentLoaded", () => {
if (!API.token) { window.location.href = "/"; return; }
document.getElementById("headerUser").textContent = localStorage.getItem("username") || "";
document.getElementById("logoutBtn").addEventListener("click", logout);
setupNavTabs();
setupOrgDetailTabs();
setupForms();
loadDashboard();
loadDashboardTokenStats();
loadOrgs();
});
function logout() {
localStorage.removeItem("token");
localStorage.removeItem("username");
window.location.href = "/";
}
// --- Navigation ---
function setupNavTabs() {
document.querySelectorAll(".nav-tabs:not(#orgDetailTabs):not(#sourceSubTabs) .nav-tab").forEach(tab => {
tab.addEventListener("click", () => {
const section = tab.dataset.section;
document.querySelectorAll(".nav-tabs:not(#orgDetailTabs):not(#sourceSubTabs) .nav-tab").forEach(t => t.classList.remove("active"));
tab.classList.add("active");
document.querySelectorAll(".app-content > .section").forEach(s => s.classList.remove("active"));
document.getElementById(`sec-${section}`).classList.add("active");
if (section === "licenses") loadExpiringLicenses();
if (section === "audit" && typeof loadAudit === "function") loadAudit();
});
});
}
function setupOrgDetailTabs() {
document.querySelectorAll("#orgDetailTabs .nav-tab").forEach(tab => {
tab.addEventListener("click", () => {
const subtab = tab.dataset.subtab;
document.querySelectorAll("#orgDetailTabs .nav-tab").forEach(t => t.classList.remove("active"));
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);
});
});
document.getElementById("orgBackBtn").addEventListener("click", () => {
document.getElementById("orgListView").style.display = "";
document.getElementById("orgDetail").classList.remove("active");
currentOrgId = null;
});
}
// --- Dashboard ---
async function loadDashboard() {
try {
const stats = await API.get("/api/dashboard/stats");
document.getElementById("statsGrid").innerHTML = `
<div class="stat-card">
<div class="stat-label">Organisationen</div>
<div class="stat-value">${stats.organizations.total}</div>
<div class="stat-sub">${stats.organizations.active} aktiv</div>
</div>
<div class="stat-card">
<div class="stat-label">Nutzer</div>
<div class="stat-value">${stats.users.total}</div>
<div class="stat-sub">${stats.users.active} aktiv</div>
</div>
<div class="stat-card">
<div class="stat-label">Aktive Lizenzen</div>
<div class="stat-value">${stats.licenses.active}</div>
</div>
<div class="stat-card">
<div class="stat-label">Vorfälle</div>
<div class="stat-value">${stats.incidents.total}</div>
<div class="stat-sub">${stats.incidents.active} aktiv</div>
</div>
`;
// Expiring licenses
const expList = document.getElementById("expiringList");
if (stats.expiring_licenses.length === 0) {
expList.innerHTML = '<li class="text-muted" style="padding: 8px 0;">Keine ablaufenden Lizenzen</li>';
} else {
expList.innerHTML = stats.expiring_licenses.map(l => `
<li class="expiring-item">
<span>${esc(l.org_name)} <span class="badge badge-${l.license_type}">${l.license_type}</span></span>
<span class="expiring-date">${formatDate(l.valid_until)}</span>
</li>
`).join("");
}
// Recent activity
const actEl = document.getElementById("recentActivity");
if (stats.recent_activity.length === 0) {
actEl.innerHTML = '<div class="text-muted">Keine Aktivität</div>';
} else {
actEl.innerHTML = stats.recent_activity.map(a => `
<div class="activity-item">
<div class="activity-icon ${a.type}">${a.type === "org" ? "O" : "U"}</div>
<div>
<div>${esc(a.label)}</div>
<div class="text-muted" style="font-size: 12px;">${formatDate(a.created_at)}</div>
</div>
</div>
`).join("");
}
} catch (err) {
console.error("Dashboard laden fehlgeschlagen:", err);
}
}
// --- Organizations ---
async function loadOrgs() {
try {
orgsCache = await API.get("/api/orgs");
renderOrgTable(orgsCache);
} catch (err) {
console.error("Orgs laden fehlgeschlagen:", err);
}
}
function renderOrgTable(orgs) {
const tbody = document.getElementById("orgTable");
if (orgs.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-muted">Keine Organisationen</td></tr>';
return;
}
tbody.innerHTML = orgs.map(o => `
<tr>
<td><a href="#" class="text-accent" style="text-decoration: none;" onclick="openOrg(${o.id}); return false;">${esc(o.name)}</a></td>
<td class="text-secondary">${esc(o.slug)}</td>
<td>${o.user_count}</td>
<td><span class="badge badge-${o.license_type || 'none'}">${o.license_type || "Keine"}</span></td>
<td><span class="badge badge-${o.is_active ? 'active' : 'inactive'}">${o.is_active ? "Aktiv" : "Inaktiv"}</span></td>
<td>
<button class="btn btn-secondary btn-small" onclick="openOrg(${o.id})">Details</button>
</td>
</tr>
`).join("");
}
// Search filter
document.addEventListener("DOMContentLoaded", () => {
const searchEl = document.getElementById("orgSearch");
if (searchEl) {
searchEl.addEventListener("input", () => {
const q = searchEl.value.toLowerCase();
const filtered = orgsCache.filter(o =>
o.name.toLowerCase().includes(q) || o.slug.toLowerCase().includes(q)
);
renderOrgTable(filtered);
});
}
});
// --- Open Org Detail ---
async function openOrg(orgId) {
currentOrgId = orgId;
document.getElementById("orgListView").style.display = "none";
document.getElementById("orgDetail").classList.add("active");
// Reset to users tab
document.querySelectorAll("#orgDetailTabs .nav-tab").forEach(t => t.classList.remove("active"));
document.querySelector('#orgDetailTabs .nav-tab[data-subtab="users"]').classList.add("active");
document.querySelectorAll("#orgDetail > .section").forEach(s => s.classList.remove("active"));
document.getElementById("sub-users").classList.add("active");
try {
const org = await API.get(`/api/orgs/${orgId}`);
document.getElementById("orgDetailHeader").innerHTML = `
<h2>${esc(org.name)} <span class="badge badge-${org.is_active ? 'active' : 'inactive'}" style="font-size: 12px; vertical-align: middle;">${org.is_active ? "Aktiv" : "Inaktiv"}</span></h2>
<div class="text-secondary mt-8">Slug: ${esc(org.slug)} | Nutzer: ${org.user_count} | Lizenz: ${org.license_type || "Keine"}</div>
`;
document.getElementById("editOrgName").value = org.name;
document.getElementById("editOrgActive").value = org.is_active ? "true" : "false";
loadOrgUsers(orgId);
loadOrgLicenses(orgId);
} catch (err) {
console.error("Org laden fehlgeschlagen:", err);
}
}
// --- Org Users ---
async function loadOrgUsers(orgId) {
try {
const users = await API.get(`/api/users?org_id=${orgId}`);
const licenses = await API.get(`/api/licenses?org_id=${orgId}`);
const activeLic = licenses.find(l => l.status === "active");
const activeUsers = users.filter(u => u.is_active).length;
document.getElementById("userLimitInfo").textContent = activeLic
? `${activeUsers} / ${activeLic.max_users} Nutzer`
: "Keine aktive Lizenz";
const tbody = document.getElementById("userTable");
if (users.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-muted">Keine Nutzer</td></tr>';
return;
}
tbody.innerHTML = users.map(u => `
<tr>
<td>${esc(u.email)}</td>
<td>
<select class="btn btn-secondary btn-small" onchange="changeRole(${u.id}, this.value)" style="padding: 4px 8px;">
<option value="member" ${u.role === "member" ? "selected" : ""}>Mitglied</option>
<option value="org_admin" ${u.role === "org_admin" ? "selected" : ""}>Org-Admin</option>
</select>
</td>
<td><span class="badge badge-${u.is_active ? 'active' : 'inactive'}">${u.is_active ? "Aktiv" : "Inaktiv"}</span></td>
<td style="text-align:center">${u.globe_access ? '<button class="btn btn-small" style="background:#00cc66;color:#fff;border:none;min-width:50px" onclick="toggleGlobeAccess(' + u.id + ')">An</button>' : '<button class="btn btn-secondary btn-small" style="min-width:50px" onclick="toggleGlobeAccess(' + u.id + ')">Aus</button>'}</td>
<td style="text-align:center">${u.network_access ? '<button class="btn btn-small" style="background:#f0b429;color:#0f172a;border:none;min-width:50px" onclick="toggleNetworkAccess(' + u.id + ')">An</button>' : '<button class="btn btn-secondary btn-small" style="min-width:50px" onclick="toggleNetworkAccess(' + u.id + ')">Aus</button>'}</td>
<td>
${u.is_active
? `<button class="btn btn-secondary btn-small" onclick="toggleUser(${u.id}, false)">Deaktivieren</button>`
: `<button class="btn btn-success btn-small" onclick="toggleUser(${u.id}, true)">Aktivieren</button>`
}
<button class="btn btn-danger btn-small" onclick="confirmDeleteUser(${u.id}, '${esc(u.email)}')">Löschen</button>
</td>
</tr>
`).join("");
} catch (err) {
console.error("Nutzer laden fehlgeschlagen:", err);
}
}
async function changeRole(userId, role) {
try {
await API.put(`/api/users/${userId}/role?role=${role}`);
} catch (err) {
showToast(err.message, "error");
if (currentOrgId) loadOrgUsers(currentOrgId);
}
}
async function toggleUser(userId, activate) {
try {
await API.put(`/api/users/${userId}/${activate ? "activate" : "deactivate"}`);
if (currentOrgId) loadOrgUsers(currentOrgId);
} catch (err) {
showToast(err.message, "error");
}
}
async function toggleGlobeAccess(userId) {
try {
await API.put("/api/users/" + userId + "/globe-access");
if (currentOrgId) loadOrgUsers(currentOrgId);
} catch (err) {
showToast(err.message, "error");
if (currentOrgId) loadOrgUsers(currentOrgId);
}
}
async function toggleNetworkAccess(userId) {
try {
await API.put("/api/users/" + userId + "/network-access");
if (currentOrgId) loadOrgUsers(currentOrgId);
} catch (err) {
showToast(err.message, "error");
if (currentOrgId) loadOrgUsers(currentOrgId);
}
}
function confirmDeleteUser(userId, email) {
showConfirm(
"Nutzer löschen",
`Soll der Nutzer "${email}" endgültig gelöscht werden?`,
async () => {
try {
await API.del(`/api/users/${userId}`);
if (currentOrgId) loadOrgUsers(currentOrgId);
} catch (err) {
showToast(err.message, "error");
}
}
);
}
// --- Org Licenses ---
async function loadOrgLicenses(orgId) {
try {
const licenses = await API.get(`/api/licenses?org_id=${orgId}`);
const tbody = document.getElementById("licenseTable");
if (licenses.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-muted">Keine Lizenzen</td></tr>';
return;
}
tbody.innerHTML = licenses.map(l => `
<tr>
<td><span class="badge badge-${l.license_type}">${l.license_type}</span></td>
<td>${l.max_users}</td>
<td>${formatDate(l.valid_from)}</td>
<td>${l.valid_until ? formatDate(l.valid_until) : "Unbegrenzt"}</td>
<td><span class="badge badge-${l.status}">${l.status}</span></td>
<td>
${l.status === "active" ? `
<button class="btn btn-secondary btn-small" onclick="extendLicense(${l.id})">Verlängern</button>
<button class="btn btn-danger btn-small" onclick="confirmRevokeLicense(${l.id})">Widerrufen</button>
` : ""}
</td>
</tr>
`).join("");
} catch (err) {
console.error("Lizenzen laden fehlgeschlagen:", err);
}
}
async function extendLicense(licId) {
const days = prompt("Um wie viele Tage verlängern?", "365");
if (!days) return;
try {
await API.put(`/api/licenses/${licId}/extend?days=${parseInt(days)}`);
if (currentOrgId) loadOrgLicenses(currentOrgId);
} catch (err) {
showToast(err.message, "error");
}
}
function confirmRevokeLicense(licId) {
showConfirm(
"Lizenz widerrufen",
"Soll die Lizenz wirklich widerrufen werden? Nutzer können dann nur noch lesen.",
async () => {
try {
await API.put(`/api/licenses/${licId}/revoke`);
if (currentOrgId) loadOrgLicenses(currentOrgId);
} catch (err) {
showToast(err.message, "error");
}
}
);
}
// --- Expiring Licenses (global view) ---
async function loadExpiringLicenses() {
const days = document.getElementById("expiringDays").value;
try {
const licenses = await API.get(`/api/licenses/expiring?days=${days}`);
const tbody = document.getElementById("expiringTable");
if (licenses.length === 0) {
tbody.innerHTML = `<tr><td colspan="5" class="text-muted">Keine ablaufenden Lizenzen in den nächsten ${days} Tagen</td></tr>`;
return;
}
tbody.innerHTML = licenses.map(l => `
<tr>
<td><a href="#" class="text-accent" style="text-decoration: none;" onclick="switchToOrg(${l.organization_id}); return false;">${esc(l.org_name)}</a></td>
<td><span class="badge badge-${l.license_type}">${l.license_type}</span></td>
<td>${l.max_users}</td>
<td class="text-warning">${formatDate(l.valid_until)}</td>
<td>
<button class="btn btn-secondary btn-small" onclick="extendLicense(${l.id})">Verlängern</button>
</td>
</tr>
`).join("");
} catch (err) {
console.error("Ablaufende Lizenzen laden fehlgeschlagen:", err);
}
}
document.addEventListener("DOMContentLoaded", () => {
const sel = document.getElementById("expiringDays");
if (sel) sel.addEventListener("change", loadExpiringLicenses);
});
function switchToOrg(orgId) {
// Switch to orgs tab and open detail
document.querySelectorAll(".nav-tabs:not(#orgDetailTabs):not(#sourceSubTabs) .nav-tab").forEach(t => t.classList.remove("active"));
document.querySelector('.nav-tab[data-section="orgs"]').classList.add("active");
document.querySelectorAll(".app-content > .section").forEach(s => s.classList.remove("active"));
document.getElementById("sec-orgs").classList.add("active");
openOrg(orgId);
}
// --- Forms ---
function setupForms() {
// New Org
document.getElementById("newOrgBtn").addEventListener("click", () => openModal("modalNewOrg"));
document.getElementById("newOrgForm").addEventListener("submit", async (e) => {
e.preventDefault();
const errEl = document.getElementById("newOrgError");
errEl.style.display = "none";
try {
await API.post("/api/orgs", {
name: document.getElementById("newOrgName").value,
slug: document.getElementById("newOrgSlug").value,
});
closeModal("modalNewOrg");
document.getElementById("newOrgForm").reset();
loadOrgs();
loadDashboard();
} catch (err) {
errEl.textContent = err.message;
errEl.style.display = "block";
}
});
// Auto-generate slug from name
document.getElementById("newOrgName").addEventListener("input", (e) => {
const slug = e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
document.getElementById("newOrgSlug").value = slug;
});
// New User
document.getElementById("newUserBtn").addEventListener("click", () => openModal("modalNewUser"));
document.getElementById("newUserForm").addEventListener("submit", async (e) => {
e.preventDefault();
const errEl = document.getElementById("newUserError");
errEl.style.display = "none";
try {
await API.post(`/api/users?org_id=${currentOrgId}`, {
email: document.getElementById("newUserEmail").value,
role: document.getElementById("newUserRole").value,
});
closeModal("modalNewUser");
document.getElementById("newUserForm").reset();
loadOrgUsers(currentOrgId);
} catch (err) {
errEl.textContent = err.message;
errEl.style.display = "block";
}
});
// New License
document.getElementById("newLicenseBtn").addEventListener("click", () => {
document.getElementById("newLicenseForm").reset();
openModal("modalNewLicense");
});
document.getElementById("newLicType").addEventListener("change", (e) => {
const durationGroup = document.getElementById("durationGroup");
if (e.target.value === "permanent") {
durationGroup.style.display = "none";
} else {
durationGroup.style.display = "";
document.getElementById("newLicDuration").value = e.target.value === "trial" ? "14" : "365";
}
});
document.getElementById("newLicenseForm").addEventListener("submit", async (e) => {
e.preventDefault();
const errEl = document.getElementById("newLicError");
errEl.style.display = "none";
const licType = document.getElementById("newLicType").value;
const body = {
organization_id: currentOrgId,
license_type: licType,
max_users: parseInt(document.getElementById("newLicMaxUsers").value),
};
if (licType !== "permanent") {
body.duration_days = parseInt(document.getElementById("newLicDuration").value);
}
const unlimitedEl = document.getElementById('newLicUnlimited');
body.unlimited_budget = !!(unlimitedEl && unlimitedEl.checked);
if (!body.unlimited_budget) {
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");
loadOrgLicenses(currentOrgId);
loadDashboard();
} catch (err) {
errEl.textContent = err.message;
errEl.style.display = "block";
}
});
// Org Edit
document.getElementById("orgEditForm").addEventListener("submit", async (e) => {
e.preventDefault();
try {
await API.put(`/api/orgs/${currentOrgId}`, {
name: document.getElementById("editOrgName").value,
is_active: document.getElementById("editOrgActive").value === "true",
});
openOrg(currentOrgId);
loadOrgs();
loadDashboard();
} catch (err) {
showToast(err.message, "error");
}
});
// Delete Org
document.getElementById("deleteOrgBtn").addEventListener("click", () => {
showConfirm(
"Organisation löschen",
"Soll die Organisation mit allen Nutzern und Lizenzen endgültig gelöscht werden? Diese Aktion kann nicht rückgängig gemacht werden.",
async () => {
try {
await API.del(`/api/orgs/${currentOrgId}`);
document.getElementById("orgListView").style.display = "";
document.getElementById("orgDetail").classList.remove("active");
currentOrgId = null;
loadOrgs();
loadDashboard();
} catch (err) {
showToast(err.message, "error");
}
}
);
});
}
// --- Modal helpers ---
function openModal(id) {
document.getElementById(id).classList.add("active");
}
function closeModal(id) {
document.getElementById(id).classList.remove("active");
}
// Confirm dialog
let confirmCallback = null;
let confirmResolver = null;
function showConfirm(title, text, callback) {
document.getElementById("confirmTitle").textContent = title;
document.getElementById("confirmText").textContent = text;
// Backward-compat: legacy Callback wird bei OK aufgerufen
confirmCallback = callback || null;
openModal("modalConfirm");
return new Promise((resolve) => {
if (confirmResolver) confirmResolver(false); // alten Resolver schliessen
confirmResolver = resolve;
});
}
function showToast(msg, type) {
type = type || "info";
const c = document.getElementById("toastContainer");
if (!c) { console.log("[toast]", type, msg); return; }
const el = document.createElement("div");
el.className = "toast toast-" + type;
el.textContent = msg;
c.appendChild(el);
setTimeout(() => {
el.classList.add("toast-out");
setTimeout(() => el.remove(), 220);
}, type === "error" ? 6000 : 3500);
}
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("confirmOkBtn").addEventListener("click", async () => {
closeModal("modalConfirm");
const cb = confirmCallback;
const rs = confirmResolver;
confirmCallback = null;
confirmResolver = null;
if (cb) {
try { await cb(); } catch (e) { showToast(e.message || String(e), "error"); }
}
if (rs) rs(true);
});
// Cancel/Close -> resolver(false)
function _confirmCancel() {
const rs = confirmResolver;
confirmCallback = null;
confirmResolver = null;
if (rs) rs(false);
}
document.getElementById("confirmCancelBtn")?.addEventListener("click", _confirmCancel);
document.querySelector("#modalConfirm .modal-close")?.addEventListener("click", _confirmCancel);
});
// --- Utilities ---
function esc(str) {
if (!str) return "";
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
function formatDate(iso) {
if (!iso) return "-";
try {
const d = new Date(iso);
return d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" });
} catch {
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 unlimited = !!budget.unlimited_budget;
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') : '-');
if (unlimited) {
el('tokenCreditsRemaining', '∞ Unbegrenzt');
el('tokenBudgetUsd', '—');
} else {
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) : '-');
// 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 percentEl = document.getElementById('tokenBudgetPercent');
const barWrap = bar ? bar.parentElement : null;
if (unlimited) {
if (bar) bar.style.width = '0%';
if (percentEl) percentEl.textContent = 'Unbegrenzt';
if (barWrap) barWrap.style.opacity = '0.4';
} else {
const percent = budget.credits_percent_used || 0;
if (bar) {
bar.style.width = Math.min(100, percent) + '%';
bar.classList.remove('warning', 'critical', 'over-limit');
if (percent > 100) bar.classList.add('over-limit');
else if (percent > 80) bar.classList.add('critical');
else if (percent > 50) bar.classList.add('warning');
}
if (percentEl) {
percentEl.textContent = percent > 100
? ('UEBER LIMIT (' + percent.toFixed(1) + '%)')
: (percent.toFixed(1) + '%');
percentEl.style.color = percent > 100 ? 'var(--danger-text, #991b1b)' : '';
percentEl.style.fontWeight = percent > 100 ? '700' : '';
}
if (barWrap) barWrap.style.opacity = '1';
}
fillBudgetForm(budget);
const tbody = document.getElementById('tokenMonthlyTable');
if (tbody) {
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>' +
'<td>' + esc(m.year_month) + '</td>' +
'<td><span class="source-badge ' + srcClass + '">' + srcLabel + '</span></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);
}
}
// Budget-Formular Felder befuellen
function fillBudgetForm(budget) {
const el = (id, val) => { const e = document.getElementById(id); if (e && val != null) e.value = val; };
el('editCreditsTotal', budget.credits_total);
el('editCostPerCredit', budget.cost_per_credit);
el('editBudgetUsd', budget.token_budget_usd);
el('editCreditsUsed', budget.credits_used ? Math.round(budget.credits_used) : 0);
const cb = document.getElementById('editUnlimitedBudget');
if (cb) {
cb.checked = !!budget.unlimited_budget;
onUnlimitedToggle();
}
}
// Unlimited-Toggle: Felder ausgrauen wenn aktiv
function onUnlimitedToggle() {
const cb = document.getElementById('editUnlimitedBudget');
const isUnlimited = cb && cb.checked;
['editCreditsTotal','editCostPerCredit','editBudgetUsd'].forEach(function(id) {
const e = document.getElementById(id);
if (e) {
e.disabled = isUnlimited;
e.style.opacity = isUnlimited ? '0.4' : '1';
}
});
}
// Unlimited-Toggle im Lizenz-Modal
function onNewLicUnlimitedToggle() {
const cb = document.getElementById('newLicUnlimited');
const isUnlimited = cb && cb.checked;
['newLicCreditsTotal','newLicCostPerCredit','newLicBudgetUsd'].forEach(function(id) {
const e = document.getElementById(id);
if (e) {
e.disabled = isUnlimited;
e.style.opacity = isUnlimited ? '0.4' : '1';
}
});
}
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);
}
}
// Budget-Formular Submit
document.addEventListener('DOMContentLoaded', function() {
var form = document.getElementById('tokenBudgetForm');
if (form) {
form.addEventListener('submit', async function(e) {
e.preventDefault();
var msgEl = document.getElementById('tokenBudgetMsg');
if (msgEl) msgEl.textContent = 'Speichern...';
// Lizenz-ID fuer die aktuelle Org ermitteln
try {
var licenses = await API.get('/api/licenses?org_id=' + currentOrgId);
var activeLic = licenses.find(function(l) { return l.status === 'active'; });
if (!activeLic) {
if (msgEl) msgEl.textContent = 'Keine aktive Lizenz gefunden';
return;
}
var body = {};
var unlimitedCb = document.getElementById('editUnlimitedBudget');
var isUnlimited = !!(unlimitedCb && unlimitedCb.checked);
body.unlimited_budget = isUnlimited;
var creditsTotal = document.getElementById('editCreditsTotal');
var costPerCredit = document.getElementById('editCostPerCredit');
var budgetUsd = document.getElementById('editBudgetUsd');
var creditsUsed = document.getElementById('editCreditsUsed');
if (!isUnlimited) {
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);
}
// credits_used immer mitsenden (auch im Unlimited-Modus, fuer Korrekturen)
if (creditsUsed && creditsUsed.value !== '') body.credits_used = parseFloat(creditsUsed.value);
var result = await API.put('/api/token-usage/budget/' + activeLic.id, body);
if (msgEl) {
if (result && result.warning) {
msgEl.textContent = result.warning;
msgEl.style.color = 'var(--danger-text, #991b1b)';
} else {
msgEl.textContent = 'Gespeichert!';
msgEl.style.color = '';
}
}
setTimeout(function() { if (msgEl) { msgEl.textContent = ''; msgEl.style.color = ''; } }, 5000);
// Daten neu laden
loadOrgTokenUsage(currentOrgId);
} catch (err) {
if (msgEl) msgEl.textContent = 'Fehler: ' + err.message;
console.error('Budget speichern fehlgeschlagen:', err);
}
});
}
});