"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 { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
// ---------- SVG-Renderer ----------
// Eine Achse pro Chart, rezessives Grid, 2px-Lücken zwischen Stapel-
// Segmenten, Hover über grosszügige unsichtbare Trefferflächen mit
.
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 += '';
out += '' +
esc(kurz(wert, einheit)) + "";
}
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 += '' +
esc(labels[i]) + "";
}
return out;
}
// serien: [{name, farbe, werte[]}], gestapelt. einheit: 'usd' | ''
function chartBalken(labels, serien, einheit) {
var n = labels.length;
if (!n) return '
Keine Daten im Zeitraum
';
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 += '';
}
}
if (summen[i] <= 0) titel.push("Keine Daten");
svg += '' + esc(titel.join("\n")) + "";
}
svg += xLabels(labels.map(fmtMonat));
return '";
}
// serien: [{name, farbe, werte[]}] mit null-Lücken. xIstTag = Tageslabels
function chartLinie(labels, serien, einheit, xIstTag) {
var n = labels.length;
if (!n) return '
Keine Daten im Zeitraum
';
var alle = [];
serien.forEach(function (s) { s.werte.forEach(function (v) { if (v != null) alle.push(v); }); });
if (!alle.length) return '
Keine Daten im Zeitraum
';
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 += '';
}
for (var j = 0; j < n; j++) {
if (s.werte[j] == null) continue;
svg += '';
svg += '' +
esc((xIstTag ? fmtDatum(labels[j]) : fmtMonat(labels[j])) + "\n" + s.name + ": " +
(einheit === "usd" ? fmtUsd(s.werte[j]) : fmt(s.werte[j]))) + "";
}
});
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 '";
}
function legende(eintraege) {
return '
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) + ".
' +
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") +
"
";
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)
: '
Noch keine Tageswerte
', "");
} else {
html += '
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.
";
}
// --- Wachstum ---
html += '
Wachstum
';
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 = '