Dateien
AegisSight-Monitor-Verwaltung/src/static/js/statistik.js
claude-dev 25ec80c8e4 feat(statistik): Hauptreiter Statistik mit kompletter Betriebsstatistik
Neuer Reiter zwischen Audit-Log und Verbrauchsrechner, Anlass war die
Dashboard-Kachel API-Kosten gesamt, die nirgendwo hinführte (sie springt
jetzt hierher). Kennzahlen-Set nach Betreiber-Best-Practices für
nutzungsbasierte KI-Produkte (Kostenverlauf, Marge je Kunde, Betrieb).

- GET /api/statistik?months=N liefert alles in einem JSON. Kosten und
  Läufe je Monat und Lagentyp (führende Quelle refresh_log mit
  lückenlosem incidents-Join), Quellen-Split aus token_usage_monthly,
  Erfolgsquote, normalisiertes Fehler-Ranking, Dauer je Typ, Wachstum,
  MAU/DAU aus user_activity_days und je Organisation Ist-Kosten plus
  Margen-Schätzung zu heutigen Sätzen (Recherche-Anlage zählt dreifach,
  weil der Multi-Pass nur eine Log-Zeile erzeugt). Aggregation als pure,
  unit-getestete Funktionen, Monatsachse lückenlos gefüllt, Mediane nur
  über Läufe mit Kosten (Erfassung begann erst 03/2026).
- statistik.js injiziert sich selbst nach dem rechner.js-Muster,
  Platzierung doppelt gesichert (Ladereihenfolge mit async=false plus
  insertBefore). Charts als Inline-SVG ohne Bibliothek, eine Achse je
  Chart, Hover-Titel mit großen Trefferflächen, Legenden, Serienfarben
  je Theme mit dem dataviz-Palette-Validator geprüft (alle Checks PASS).
  Zeitraum 6/12/Alles, Datenstand-Hinweis, ehrliche Fußnote unter der
  Margen-Tabelle, Leerzustand für die junge Aktivitäts-Erfassung.
- Migration user_activity_days für die Portal-Staging-DB (auf Live legt
  sie der Monitor an, dort läuft die Erfassung in get_current_user).
- 10 neue Unit-Tests plus Auth-Smoke für den Endpoint, 96 Tests grün
  (die zwei bekannten shared-Import-Fehlschläge bestehen unverändert).

Gegenstück im Monitor: Commit d367c60 (Aktivitäts-Erfassung).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 15:09:34 +00:00

441 Zeilen
21 KiB
JavaScript

"use strict";
// Statistik-Tab für das Verwaltungsportal. Selbst-injizierend nach dem
// Muster von rechner.js, damit dashboard.html unverändert bleibt.
// Charts sind handgerendertes Inline-SVG ohne Bibliothek. Die Serienfarben
// sind mit dem dataviz-Palette-Validator gegen beide Themes geprüft
// (Light #96791A/#2a78d6/#199e70, Dark #a8863a/#3987e5/#199e70, alle Checks PASS).
(function () {
var geladen = false;
var aktMonate = 12;
// ---------- Format-Helfer ----------
function fmt(n) { return Number(n || 0).toLocaleString("de-DE"); }
function fmtUsd(n) {
return "$" + Number(n || 0).toLocaleString("de-DE", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function fmtMonat(ym) {
if (!ym) return "";
var namen = ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"];
return namen[parseInt(ym.slice(5, 7), 10) - 1] + " " + ym.slice(2, 4);
}
function fmtDatum(s) {
if (!s) return "-";
var d = String(s).slice(0, 10).split("-");
return d.length === 3 ? d[2] + "." + d[1] + "." + d[0] : s;
}
function esc(s) {
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
});
}
// ---------- SVG-Renderer ----------
// Eine Achse pro Chart, rezessives Grid, 2px-Lücken zwischen Stapel-
// Segmenten, Hover über grosszügige unsichtbare Trefferflächen mit <title>.
var W = 720, H = 230, ML = 52, MR = 8, MT = 10, MB = 26;
function niceMax(v) {
if (v <= 0) return 1;
var mag = Math.pow(10, Math.floor(Math.log10(v)));
var norm = v / mag;
var step = norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 5 ? 5 : 10;
return step * mag;
}
function achsen(maxY, einheit) {
var plotH = H - MT - MB;
var out = "";
for (var i = 0; i <= 3; i++) {
var y = MT + plotH - (plotH * i) / 3;
var wert = (maxY * i) / 3;
out += '<line x1="' + ML + '" y1="' + y + '" x2="' + (W - MR) + '" y2="' + y +
'" stroke="var(--border)" stroke-width="1"/>';
out += '<text x="' + (ML - 6) + '" y="' + (y + 3) + '" text-anchor="end" font-size="10" fill="var(--text-muted)">' +
esc(kurz(wert, einheit)) + "</text>";
}
return out;
}
function kurz(v, einheit) {
var s;
if (v >= 1000000) s = (v / 1000000).toLocaleString("de-DE", { maximumFractionDigits: 1 }) + " Mio";
else if (v >= 10000) s = Math.round(v / 1000).toLocaleString("de-DE") + "k";
else s = v.toLocaleString("de-DE", { maximumFractionDigits: v < 10 ? 1 : 0 });
return einheit === "usd" ? s + " $" : s;
}
function xLabels(labels) {
var n = labels.length;
if (!n) return "";
var plotW = W - ML - MR;
var schritt = n > 8 ? 2 : 1;
var out = "";
for (var i = 0; i < n; i++) {
if (i % schritt !== 0) continue;
var x = ML + (plotW * (i + 0.5)) / n;
out += '<text x="' + x + '" y="' + (H - 8) + '" text-anchor="middle" font-size="10" fill="var(--text-muted)">' +
esc(labels[i]) + "</text>";
}
return out;
}
// serien: [{name, farbe, werte[]}], gestapelt. einheit: 'usd' | ''
function chartBalken(labels, serien, einheit) {
var n = labels.length;
if (!n) return '<div class="st-leer">Keine Daten im Zeitraum</div>';
var summen = labels.map(function (_, i) {
return serien.reduce(function (a, s) { return a + (s.werte[i] || 0); }, 0);
});
var maxY = niceMax(Math.max.apply(null, summen.concat([0])));
var plotW = W - ML - MR, plotH = H - MT - MB;
var breite = Math.min(40, (plotW / n) * 0.62);
var svg = achsen(maxY, einheit);
for (var i = 0; i < n; i++) {
var x = ML + (plotW * (i + 0.5)) / n - breite / 2;
var yCursor = MT + plotH;
var titel = [fmtMonat(labels[i]) || labels[i]];
for (var s = 0; s < serien.length; s++) {
var wert = serien[s].werte[i] || 0;
titel.push(serien[s].name + ": " + (einheit === "usd" ? fmtUsd(wert) : fmt(wert)));
if (wert <= 0) continue;
var hoehe = (wert / maxY) * plotH;
yCursor -= hoehe;
// 2px-Lücke zwischen Segmenten, oberstes Segment leicht gerundet
var segH = Math.max(0, hoehe - 2);
if (segH > 0) {
svg += '<rect x="' + x + '" y="' + yCursor + '" width="' + breite + '" height="' + segH +
'" rx="2" fill="' + serien[s].farbe + '"/>';
}
}
if (summen[i] <= 0) titel.push("Keine Daten");
svg += '<rect x="' + (ML + (plotW * i) / n) + '" y="' + MT + '" width="' + (plotW / n) +
'" height="' + plotH + '" fill="transparent"><title>' + esc(titel.join("\n")) + "</title></rect>";
}
svg += xLabels(labels.map(fmtMonat));
return '<svg viewBox="0 0 ' + W + " " + H + '" style="width:100%;height:auto;display:block;" role="img">' + svg + "</svg>";
}
// serien: [{name, farbe, werte[]}] mit null-Lücken. xIstTag = Tageslabels
function chartLinie(labels, serien, einheit, xIstTag) {
var n = labels.length;
if (!n) return '<div class="st-leer">Keine Daten im Zeitraum</div>';
var alle = [];
serien.forEach(function (s) { s.werte.forEach(function (v) { if (v != null) alle.push(v); }); });
if (!alle.length) return '<div class="st-leer">Keine Daten im Zeitraum</div>';
var maxY = niceMax(Math.max.apply(null, alle));
var plotW = W - ML - MR, plotH = H - MT - MB;
function px(i) { return ML + (plotW * (i + 0.5)) / n; }
function py(v) { return MT + plotH - (v / maxY) * plotH; }
var svg = achsen(maxY, einheit);
serien.forEach(function (s) {
var punkte = [];
for (var i = 0; i < n; i++) {
if (s.werte[i] == null) continue;
punkte.push(px(i).toFixed(1) + "," + py(s.werte[i]).toFixed(1));
}
if (punkte.length > 1) {
svg += '<polyline points="' + punkte.join(" ") + '" fill="none" stroke="' + s.farbe +
'" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>';
}
for (var j = 0; j < n; j++) {
if (s.werte[j] == null) continue;
svg += '<circle cx="' + px(j) + '" cy="' + py(s.werte[j]) + '" r="2.5" fill="' + s.farbe + '"/>';
svg += '<circle cx="' + px(j) + '" cy="' + py(s.werte[j]) + '" r="9" fill="transparent"><title>' +
esc((xIstTag ? fmtDatum(labels[j]) : fmtMonat(labels[j])) + "\n" + s.name + ": " +
(einheit === "usd" ? fmtUsd(s.werte[j]) : fmt(s.werte[j]))) + "</title></circle>";
}
});
svg += xLabels(xIstTag ? labels.map(function (t, i) { return i % Math.ceil(n / 8) === 0 ? String(t).slice(8, 10) + "." + String(t).slice(5, 7) + "." : ""; }) : labels.map(fmtMonat));
return '<svg viewBox="0 0 ' + W + " " + H + '" style="width:100%;height:auto;display:block;" role="img">' + svg + "</svg>";
}
function legende(eintraege) {
return '<div class="st-legende">' + eintraege.map(function (e) {
return '<span><span class="st-dot" style="background:' + e.farbe + '"></span>' + esc(e.name) + "</span>";
}).join("") + "</div>";
}
function kpiKachel(label, wert, sub) {
return '<div class="stat-card"><div class="stat-label">' + esc(label) + '</div>' +
'<div class="stat-value">' + wert + "</div>" +
(sub ? '<div class="stat-sub">' + sub + "</div>" : "") + "</div>";
}
function chartCard(titel, inhalt, legendeHtml) {
return '<div class="card"><div class="card-header"><h2>' + esc(titel) + "</h2>" +
(legendeHtml || "") + '</div><div class="card-body">' + inhalt + "</div></div>";
}
// ---------- Rendering ----------
function C1() { return "var(--st-c1)"; }
function C2() { return "var(--st-c2)"; }
function C3() { return "var(--st-c3)"; }
function render(d) {
var el = document.getElementById("st-inhalt");
if (!el) return;
var monate = d.window.monate || [];
var mt = {};
(d.monat_typ || []).forEach(function (r) { mt[r.monat + "|" + r.typ] = r; });
function reihe(typ, feld) {
return monate.map(function (m) { var r = mt[m + "|" + typ]; return r ? r[feld] : 0; });
}
function reiheNull(typ, feld) {
return monate.map(function (m) { var r = mt[m + "|" + typ]; return r && r[feld] != null ? r[feld] : null; });
}
// Übrige Quellen (globe/chat/enhance) je Monat zusammenfassen
var nebenKosten = {};
(d.monat_quelle || []).forEach(function (r) {
if (r.quelle === "monitor") return;
nebenKosten[r.monat] = (nebenKosten[r.monat] || 0) + (r.kosten || 0);
});
var kpi = d.kpi || {};
var html = "";
// --- Kosten & Marge ---
html += '<h3 class="st-gruppe">Kosten &amp; Marge</h3>';
html += '<div class="stats-grid">' +
kpiKachel("API-Kosten im Zeitraum", fmtUsd(kpi.kosten_usd), fmt(kpi.laeufe_gesamt) + " Läufe gesamt") +
kpiKachel("Erfolgreiche Läufe", fmt(kpi.laeufe_ok), kpi.erfolgsquote != null ? kpi.erfolgsquote.toLocaleString("de-DE") + "% Erfolgsquote" : "") +
kpiKachel("Median je Lauf", kpi.median_kosten_lauf != null ? fmtUsd(kpi.median_kosten_lauf) : "-", "nur Läufe mit Kosten") +
kpiKachel("Artikel gefunden", fmt(kpi.artikel), fmt(kpi.tokens_in) + " Tokens ein / " + fmt(kpi.tokens_out) + " aus") +
"</div>";
html += chartCard("API-Kosten je Monat (USD)",
chartBalken(monate, [
{ name: "Live-Monitoring", farbe: C1(), werte: reihe("adhoc", "kosten") },
{ name: "Recherche", farbe: C2(), werte: reihe("research", "kosten") },
{ name: "Übrige Quellen", farbe: C3(), werte: monate.map(function (m) { return Math.round((nebenKosten[m] || 0) * 100) / 100; }) },
], "usd"),
legende([{ name: "Live-Monitoring", farbe: C1() }, { name: "Recherche", farbe: C2() }, { name: "Übrige Quellen", farbe: C3() }]));
html += chartCard("Median-Kosten je Lauf (USD), der Frühwarner für Kosten-Anstieg",
chartLinie(monate, [
{ name: "Live-Monitoring", farbe: C1(), werte: reiheNull("adhoc", "kosten_median") },
{ name: "Recherche", farbe: C2(), werte: reiheNull("research", "kosten_median") },
], "usd"),
legende([{ name: "Live-Monitoring", farbe: C1() }, { name: "Recherche", farbe: C2() }]));
// Orgs-Tabelle
var orgZeilen = (d.orgs || []).map(function (o) {
var margeCls = o.marge_usd >= 0 ? "st-pos" : "st-neg";
return "<tr><td>" + esc(o.name) + "</td><td>" + fmt(o.laeufe_ok) + " / " + fmt(o.laeufe_gesamt) + "</td>" +
"<td>" + fmtUsd(o.kosten_usd) + "</td><td>" + fmt(o.credits_geschaetzt) + "</td>" +
"<td>" + fmtUsd(o.gegenwert_usd) + '</td><td class="' + margeCls + '">' + fmtUsd(o.marge_usd) + "</td></tr>";
}).join("");
html += '<div class="card"><div class="card-header"><h2>Je Organisation (Zeitraum)</h2></div>' +
'<div class="table-wrap"><table><thead><tr><th>Organisation</th><th>Läufe ok/gesamt</th>' +
"<th>Ist-Kosten</th><th>Credits (geschätzt)</th><th>Gegenwert</th><th>Marge</th></tr></thead>" +
"<tbody>" + (orgZeilen || '<tr><td colspan="6" class="text-muted">Keine Daten</td></tr>') + "</tbody></table></div>" +
'<div class="card-body st-fussnote">Schätzung zu den heutigen Sätzen (billing_tariff), es gibt keine historisierten Buchungen. ' +
"Recherche-Anlage zählt als 3 Durchläufe (erster abgeschlossener Lauf je Recherche-Lage). " +
"Studio-Bausteine (analysis/factcheck) fehlen mangels Nutzungsdaten, der Gegenwert ist eher zu niedrig. " +
"Ist-Kosten enthalten auch Fehlläufe. 1 Credit = " + fmtUsd(d.tarif.usd_per_credit) + ".</div></div>";
// --- Nutzung ---
html += '<h3 class="st-gruppe">Nutzung</h3>';
var mb = {};
(d.monat_betrieb || []).forEach(function (r) { mb[r.monat] = r; });
function bReihe(feld) { return monate.map(function (m) { return mb[m] ? mb[m][feld] : 0; }); }
html += chartCard("Erfolgreiche Läufe je Monat",
chartBalken(monate, [
{ name: "Automatisch", farbe: C1(), werte: bReihe("auto") },
{ name: "Manuell", farbe: C2(), werte: bReihe("manual") },
], ""),
legende([{ name: "Automatisch", farbe: C1() }, { name: "Manuell", farbe: C2() }]));
html += chartCard("Gefundene Artikel je Monat",
chartBalken(monate, [
{ name: "Live-Monitoring", farbe: C1(), werte: reihe("adhoc", "artikel") },
{ name: "Recherche", farbe: C2(), werte: reihe("research", "artikel") },
], ""),
legende([{ name: "Live-Monitoring", farbe: C1() }, { name: "Recherche", farbe: C2() }]));
// --- Betrieb ---
html += '<h3 class="st-gruppe">Betrieb</h3>';
var dauerA = d.dauer_typ && d.dauer_typ.adhoc != null ? Math.round(d.dauer_typ.adhoc / 60) + " Min" : "-";
var dauerR = d.dauer_typ && d.dauer_typ.research != null ? Math.round(d.dauer_typ.research / 60) + " Min" : "-";
html += '<div class="stats-grid">' +
kpiKachel("Erfolgsquote", kpi.erfolgsquote != null ? kpi.erfolgsquote.toLocaleString("de-DE") + "%" : "-",
fmt(kpi.laeufe_ok) + " von " + fmt(kpi.laeufe_gesamt) + " Läufen") +
kpiKachel("Dauer Live-Lauf (ø)", dauerA, "") +
kpiKachel("Dauer Recherche-Durchlauf (ø)", dauerR, "") +
"</div>";
html += chartCard("Läufe nach Ausgang je Monat",
chartBalken(monate, [
{ name: "Erfolgreich", farbe: "var(--success)", werte: bReihe("completed") },
{ name: "Fehler", farbe: "var(--danger)", werte: bReihe("error") },
{ name: "Abgebrochen", farbe: "var(--text-muted)", werte: bReihe("cancelled") },
], ""),
legende([{ name: "Erfolgreich", farbe: "var(--success)" }, { name: "Fehler", farbe: "var(--danger)" }, { name: "Abgebrochen", farbe: "var(--text-muted)" }]));
var fehlerZeilen = (d.fehler_top || []).map(function (f) {
return "<tr><td>" + esc(f.grund) + "</td><td>" + fmt(f.n) + "</td></tr>";
}).join("");
html += '<div class="card"><div class="card-header"><h2>Häufigste Fehlerursachen</h2></div>' +
'<div class="table-wrap"><table><thead><tr><th>Ursache</th><th>Anzahl</th></tr></thead><tbody>' +
(fehlerZeilen || '<tr><td colspan="2" class="text-muted">Keine Fehler im Zeitraum</td></tr>') +
"</tbody></table></div></div>";
// --- Aktivität ---
html += '<h3 class="st-gruppe">Aktivität</h3>';
var akt = d.aktivitaet || {};
html += '<div class="stats-grid">' +
kpiKachel("Nutzer mit Login (30 Tage)", fmt(akt.nutzer_30d), "Momentaufnahme aus dem letzten Login") +
kpiKachel("Aktivitäts-Erfassung", akt.erfassung_seit ? "seit " + fmtDatum(akt.erfassung_seit) : "startet jetzt",
akt.erfassung_seit ? "je Nutzer und Tag ein Eintrag" : "MAU/DAU wachsen ab diesem Update") +
"</div>";
var mauWerte = (akt.mau || []).map(function (r) { return r.nutzer; });
var mauMax = Math.max.apply(null, mauWerte.concat([0]));
if (mauMax > 0) {
html += chartCard("Aktive Nutzer je Monat (MAU)",
chartBalken(monate, [{ name: "Aktive Nutzer", farbe: C1(), werte: mauWerte }], ""), "");
var dauLabels = (akt.dau || []).map(function (r) { return r.tag; });
var dauWerte = (akt.dau || []).map(function (r) { return r.nutzer; });
html += chartCard("Aktive Nutzer je Tag (DAU, letzte 30 Tage)",
dauLabels.length
? chartLinie(dauLabels, [{ name: "Aktive Nutzer", farbe: C1(), werte: dauWerte }], "", true)
: '<div class="st-leer">Noch keine Tageswerte</div>', "");
} else {
html += '<div class="card"><div class="card-body st-leer">Noch keine Aktivitätsdaten. ' +
"Die Erfassung (ein Eintrag je Nutzer und Tag) beginnt mit diesem Update, " +
"die MAU/DAU-Kurven erscheinen hier, sobald sich Nutzer im Monitor bewegen.</div></div>";
}
// --- Wachstum ---
html += '<h3 class="st-gruppe">Wachstum</h3>';
var w = {};
(d.wachstum || []).forEach(function (r) { w[r.monat] = r; });
function wReihe(feld) { return monate.map(function (m) { return w[m] ? w[m][feld] : 0; }); }
html += chartCard("Neuzugänge je Monat",
chartBalken(monate, [
{ name: "Organisationen", farbe: C1(), werte: wReihe("orgs") },
{ name: "Nutzer", farbe: C2(), werte: wReihe("users") },
{ name: "Lagen", farbe: C3(), werte: wReihe("lagen") },
], ""),
legende([{ name: "Organisationen", farbe: C1() }, { name: "Nutzer", farbe: C2() }, { name: "Lagen", farbe: C3() }]));
el.innerHTML = html;
// Datenstand
var ds = document.getElementById("st-datenstand");
if (ds) {
var teile = [];
if (d.datenstand.letzter_lauf) teile.push("Letzter Lauf " + fmtDatum(d.datenstand.letzter_lauf));
teile.push("Kostenerfassung ab 03/2026");
if (d.tarif && d.tarif.quelle === "defaults") teile.push("Sätze aus Rückfallwerten");
ds.textContent = "Datenstand: " + teile.join(" · ");
}
}
// ---------- Laden ----------
function lade() {
var el = document.getElementById("st-inhalt");
if (el) el.innerHTML = '<div class="st-leer">Lade Statistik …</div>';
API.get("/api/statistik?months=" + aktMonate).then(function (d) {
render(d);
}).catch(function (err) {
if (el) el.innerHTML = '<div class="st-leer">Statistik konnte nicht geladen werden: ' + esc(err.message) + "</div>";
});
}
function setzeZeitraum(monate, btn) {
aktMonate = monate;
document.querySelectorAll("#st-zeitraum button").forEach(function (b) { b.classList.remove("btn-primary"); b.classList.add("btn-secondary"); });
btn.classList.remove("btn-secondary");
btn.classList.add("btn-primary");
lade();
}
// ---------- Injektion (Muster rechner.js) ----------
var STYLES = "\n" +
"#sec-statistik { --st-c1:#a8863a; --st-c2:#3987e5; --st-c3:#199e70; }\n" +
'[data-theme="light"] #sec-statistik { --st-c1:#96791A; --st-c2:#2a78d6; --st-c3:#199e70; }\n' +
"#sec-statistik .st-gruppe { margin:26px 0 12px; font-size:16px; border-bottom:1px solid var(--border); padding-bottom:6px; }\n" +
"#sec-statistik .st-kopf { display:flex; align-items:center; gap:12px; flex-wrap:wrap; }\n" +
"#sec-statistik .st-kopf h2 { font-size:20px; margin-right:auto; }\n" +
"#sec-statistik .st-datenstand { font-size:12px; color:var(--text-muted); margin-top:6px; }\n" +
"#sec-statistik .st-legende { display:flex; gap:14px; font-size:12px; color:var(--text-secondary); flex-wrap:wrap; }\n" +
"#sec-statistik .st-dot { display:inline-block; width:10px; height:10px; border-radius:3px; margin-right:5px; vertical-align:-1px; }\n" +
"#sec-statistik .st-leer { padding:22px; text-align:center; color:var(--text-muted); font-size:13px; }\n" +
"#sec-statistik .st-fussnote { font-size:12px; color:var(--text-muted); border-top:1px solid var(--border); }\n" +
"#sec-statistik .st-pos { color:var(--success); }\n" +
"#sec-statistik .st-neg { color:var(--danger); }\n" +
"#sec-statistik .card { margin-bottom:16px; }\n";
function init() {
var nav = document.querySelector("nav.nav-tabs");
var content = document.querySelector(".app-content");
if (!nav || !content || document.getElementById("sec-statistik")) return;
var style = document.createElement("style");
style.textContent = STYLES;
document.head.appendChild(style);
var sektion = document.createElement("div");
sektion.className = "section";
sektion.id = "sec-statistik";
sektion.innerHTML =
'<div class="st-kopf">' +
"<h2>Statistik</h2>" +
'<div id="st-zeitraum" style="display:flex; gap:6px;">' +
'<button type="button" class="btn btn-secondary btn-small" data-monate="6">6 Monate</button>' +
'<button type="button" class="btn btn-primary btn-small" data-monate="12">12 Monate</button>' +
'<button type="button" class="btn btn-secondary btn-small" data-monate="0">Alles</button>' +
"</div>" +
"</div>" +
'<div class="st-datenstand" id="st-datenstand"></div>' +
'<div id="st-inhalt" style="margin-top:14px;"></div>';
content.appendChild(sektion);
var btn = document.createElement("button");
btn.className = "nav-tab";
btn.setAttribute("data-section", "statistik");
btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3v18h18"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg>Statistik';
// Zwischen Audit-Log und Verbrauchsrechner, egal in welcher Ladereihenfolge
var rechnerTab = nav.querySelector('.nav-tab[data-section="rechner"]');
nav.insertBefore(btn, rechnerTab || null);
btn.addEventListener("click", function () {
document.querySelectorAll("nav.nav-tabs .nav-tab").forEach(function (t) { t.classList.remove("active"); });
btn.classList.add("active");
document.querySelectorAll(".app-content > .section").forEach(function (s) { s.classList.remove("active"); });
var detail = document.getElementById("orgDetail");
if (detail) detail.classList.remove("active");
sektion.classList.add("active");
if (!geladen) { geladen = true; lade(); }
});
nav.addEventListener("click", function (e) {
var ziel = e.target.closest(".nav-tab");
if (!ziel || ziel === btn) return;
btn.classList.remove("active");
sektion.classList.remove("active");
});
sektion.querySelectorAll("#st-zeitraum button").forEach(function (b) {
b.addEventListener("click", function () { setzeZeitraum(parseInt(b.getAttribute("data-monate"), 10), b); });
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();