Admin-Dashboard (Modell / Budget / Usage)
Steuer- & Sichtfläche für die LLM-Kostengovernance aus
ADR-0017.
Entscheidung:
ADR-0024.
Gleichwertig als UI + API + MCP (AI-Parität, ADR-0018) — die
Logik liegt einmalig in lib/llm/* (Single Source).
Was man kann
- Modell je Aufgabe wählen (
classification,embedding,standardReport,premiumReport,pdfExtraction,toolUse,bulkParse) — Override schlägt ENV/Default, sofort wirksam (Cache-Invalidierung). - Budget-Cap (USD Tag/Monat) setzen — die Engine bremst fail-open bei Überschreitung (ADR-0017).
- Ist-Kosten sehen: heute / Monat + je Task+Modell (Calls/Tokens/$).
Benutzerverwaltung & Impersonation
Eigener Admin-Bereich (ab Dashboard-Nav verlinkt), proxy.ts-gated
(platform_admin + 2FA) und zusätzlich server-seitig geprüft.
/admin/users— Nutzerliste mit Suche (E-Mail/Name) + Rollen-Filter; pro Zeile Auge-Button (Session-Takeover, ADR-0026 v2 — wechselt in die Sicht des Nutzers mit dessen Rolle/Rechten; orange Leiste oben zeigt den Modus, dort auch „Beenden“), Kontext (read-only Inspektion via?inspect=, auditiert) und Detail. Unten: Admin-Audit + Impersonation-Audit./admin/users/[id]— Plattform-Rolle ändern (user/support/platform_admin) und Konto DSGVO-konform löschen (gleiche Erasure-Kaskade wie die Selbst-Löschung)./admin/impersonate— nur noch Redirect auf/admin/users(Konsole dort integriert); die Headless-APIGET/POST /api/v1/admin/impersonatebleibt unverändert.
Sicherheit (fail-closed): nur verifizierte platform_admin, nicht aus
einer Impersonation heraus; kein Self-Target; kein Degradieren des letzten
Admins (Lockout-Schutz); kein Löschen eines anderen Admins; Org-Owner-Guard
vor der Löschung; striktes Audit vor der Löschung. Rollen-/Lösch-Eingriffe
landen in admin_audit_log, Impersonation weiter in impersonation_log.
Rollen-Änderungen wirken für den Ziel-Nutzer erst ab dessen nächstem
Login/Token-Refresh (JWT-Sessions).
Seiten im Admin-Bereich
Alle Seiten verlangen die Plattformrolle platform_admin und verifizierte
TOTP-2FA; mutierende Aktionen sind während einer Impersonation gesperrt.
Stand: 07.10.2026, abgeglichen mit app/admin/** und components/AdminNav.tsx.
| Route | Zweck |
|---|---|
/admin | Übersicht: Modell je Aufgabe, Budget-Caps, LLM-Ist-Kosten |
/admin/users, /admin/users/[id] | Benutzerverwaltung, Rollen, Löschung, Impersonation (siehe oben) |
/admin/anfragen | Zugangs-/Pilotanfragen aus dem Formular: Status-Triage (neu, qualifiziert, nicht_qualifiziert, spam), Folgevertrag-Markierung, CSV-Export |
/admin/data-sources | Datenquellen, aktive Releases, Drift-Überwachung |
/admin/cron, /admin/cron/[name], /admin/cron/[name]/runs/[id] | Cron-Jobs, Laufhistorie, einzelner Lauf (siehe „Cron-Jobs“) |
/admin/metrics | Werkzeug-KPIs und interner Prüfmodus |
/admin/kosten | Tag-90-Gate: Kosten pro Tool-Abschluss, qualifiziertem Lead und aktivem Source Release |
/admin/impersonate | nur Redirect nach /admin/users |
Nur ein Mensch setzt in /admin/anfragen den Status qualifiziert; es gibt
keinen automatischen Übergang. Dieser Status ist der Nenner „qualifizierter
Lead“ in /admin/kosten.
Zugänge
| Zugang | Auth | Für |
|---|---|---|
UI /admin | Auth.js platform_admin + 2FA (ADR-0006, Gate in proxy.ts) | Menschen |
API /api/v1/admin/* | API-Key mit admin-Scope | Skripte/KI |
MCP get_usage/set_model/set_budget | dito (LANDNUTZEN_API_KEY) | KI-Ops |
Kein/ungültiger Key → 401, gültiger Key ohne Scope → 403.
Datenquellen und Drift-Überwachung
/admin/data-sources trennt weiterhin den letzten technischen Lauf vom
Betriebsnachweis des tatsächlich aktiven Releases. Zusätzlich zeigt die Seite
für jeden qualifizierten Active Pointer den Zustand fresh, due, stale,
failed oder unknown, seine individuellen Fristen, Coverage-/Quality-Befunde,
den letzten Zustandswechsel und die Alarmzustellung.
Der stündliche Cron monitor-data-source-drift verwendet das bestehende
CRON_SECRET und das cron_runs-Ledger. Seit 25.09.2026 verschickt er
Zustandswechsel als Tagesdigest: höchstens eine Mail pro Wiener
Kalendertag, frühestens ab 07:00, mit allen Wechseln seit dem letzten Digest,
per world4you-SMTP ausschließlich an office@ostheimer.at (ADR-0048,
Nachtrag). Alarmiert werden nur neue, geänderte, verschärfte oder behobene
Zustände; unveränderter Drift verursacht keine wiederholten E-Mails. Noch nicht
fällige Wechsel zeigt die Alarmzustellung als „Alarm ausstehend (nächster
Tagesdigest)“. Fehlgeschlagene oder nicht konfigurierte Zustellung bleibt im
Dashboard sichtbar und wird beim nächsten Lauf wiederholt. Bekannte
Teilabdeckung und bereits freigegebene Qualitätswarnungen sind keine neuen
Vorfälle. Der Monitor verändert keinen Release und keinen Active Pointer.
/admin/cron/monitor-data-source-drift zeigt die Laufhistorie mit
Beobachtungen, Übergängen, Zustellstatus, Fingerprint und – beim versendenden
Lauf – dem Digest-Tag alertDigestDay.
Cron-Jobs
/admin/cron listet alle Einträge aus vercel.json#crons zusammen mit dem
Katalog lib/cron/catalog.ts, dem letzten Lauf aus dem Ledger cron_runs und
einem 30-Tage-Aggregat; Stand 07.10.2026 sind es 28 Crons, Katalog und
vercel.json stimmen überein. Ein manueller Trigger
(POST /api/admin/cron/<name>/trigger, nur per Sitzung eines
Plattform-Admins) ruft die Cron-Route mit dem CRON_SECRET auf und schreibt
triggered_by='admin'. /admin/cron/<name> zeigt die Laufhistorie eines
Eintrags.
/admin/cron/<name>/runs/<id> zeigt einen einzelnen Lauf mit vollständigem
Ergebnis-JSON. Seite und URL sind zugleich der Nachweis (evidence_ref) der
Snapshot-Renewals auf Vercel (ADR-0080): renew-altlasten-snapshot
(täglich 03:37 UTC), renew-hochwasser-snapshot (täglich 02:07 UTC) und
renew-brownfield-snapshot (dienstags 03:19 UTC) ersetzen damit die frühere
Actions-Run-URL; ohne cron_runs-Zeile veröffentlicht kein Lauf. Bei
Reviewpflicht der Altlasten steht hier dieselbe Aktivierungsanleitung wie in
der Review-Mail. cron-prune (sonntags 02:00 UTC) löscht Läufe, die älter
als 90 Tage sind und nicht zu den letzten 30 Läufen ihres Crons gehören.
Kosten pro Tool-Abschluss (/admin/kosten)
Die Seite macht das Tag-90-Gate „Kosten sichtbar“ prüfbar (Vertrag
docs/contracts/admin-cost-overview-v1.md, Headless-Gegenstück
GET /api/v1/admin/cost-overview). Sie zeigt Kosten pro qualifiziertem
Tool-Abschluss (PV-Ertragsrechner und seit 04.10.2026 Standort-Check),
pro qualifiziertem Lead (/admin/anfragen) und pro aktivem Source Release,
getrennt nach gemessenen direkten Kosten und einem optional konfigurierten
Infrastrukturwert. Nicht erfasste oder nicht aussagekräftige Nenner stehen
als eigener Status, nicht als Null. Der Abschnitt „Eigene Prüfläufe“ weist
interne Läufe als „davon intern“ aus; sie zählen nie im Nenner (ADR-0079).
Die Infrastrukturkosten sind nie live abgerufen, sondern ein optionaler
Konfigurationswert mit Quelle und Datum (docs/API-ECONOMICS.md).
Betriebsmails
Alle automatischen Betriebsmails laufen über denselben Pfad wie die
Login-Mails (lib/mail/send.ts, world4you-SMTP über EMAIL_SERVER, Absender
EMAIL_FROM, ADR-0038) und gehen ausschließlich an office@ostheimer.at:
| Auslöser | Takt | |
|---|---|---|
| Neue Pilotanfrage | POST /api/v1/access-requests (ohne Personendaten) | sofort |
| Cron failed/stuck | runCron, auch Renewal-Crons (renew-*) | sofort |
| Altlasten-Review bzw. Entwarnung | renew-altlasten-snapshot bei Reviewpflicht (Frist, Grund, Aktivierungsanleitung, Anhänge) und wenn sie später entfällt; ersetzt das frühere GitHub-Issue (ADR-0080) | sofort |
| Datenquellen-Tagesdigest | monitor-data-source-drift | höchstens 1× pro Tag ab 07:00 Wien |
Ohne EMAIL_SERVER wird nichts versendet (nur eine Log-Warnung); einen
Resend-Fallback gibt es für Betriebsmails nicht. Zustellstatistiken wie bei
Resend fehlen – Beleg für einen Versand sind die Vercel-Logs
(Betriebsmail „…“ an office@ostheimer.at versendet) und der Posteingang.
Werkzeug-KPIs
/admin/metrics zeigt die kennungsfreien First-Party-Aggregate aus ADR-0042:
- Aktivierungsfunnel vom ersten bewussten Formularkontakt bis zum ersten Ergebnis oder ehrlichen technischen Endzustand;
- qualifizierte Serverläufe, Erfolgs- und Fehlerquoten;
- p50/p95 als obere Grenzen fester Laufzeitklassen sowie die ausdrücklich als Näherung markierte Erstlauf-/Wiederverwendungs-Sicht je Serverinstanz;
- Providerstatus, grobes Bundesland und direkt zurechenbare Providerkosten;
- Stichprobe, erster Messtag und Reife der festen 14-Tage-Baseline einschließlich Funnel-Integrität und vollständiger Kostenzuordnung;
- den separat ausgewiesenen, während D+1 bis D+14 fortgeschriebenen und ab D+15 eingefrorenen Ausgangswert mit denselben Funnel-, Fehler-, Provider-, Runtime-, Laufzeit- und Kostendimensionen wie die laufende Auswertung;
- den Schalter „Interner Prüfmodus“ und die Tabelle „Eigene Prüfläufe“ (Insgesamt gemessen, davon intern, Öffentlich), siehe unten.
Erfasst sind der PV-Ertragsrechner (pv-yield), der Standort-Check
(site-check) und das Immobilienmarkt-Signal (market-signal). Der
Standort-Check zählt seit dem Deploy vom 04.10.2026 als qualifizierter
Tool-Abschluss und misst seit Migration 0065 (#650) Laufzeit und Bundesland je
Prüfung; bis 04.10.2026 war er nicht instrumentiert. Sein Ausgangswert ist am
06.10.2026 neu verankert (Fortschreibung 07.10. bis 20.10.2026, unveränderlich
ab 21.10.2026). Das Marktsignal hat keine Eingabe und geht nicht in den
Abschluss-Nenner ein.
Interner Prüfmodus (ADR-0079)
Vor eigenen Tests in Produktion auf /admin/metrics „Prüfmodus einschalten“
(Plattform-Admin mit bestätigter 2FA, keine Impersonation). Der Browser
erhält das Cookie ln_qa (Ablaufzeit und Signatur, keine Kennung, höchstens
zwölf Stunden). Die Werkzeuge schicken dessen Wert als Header
X-LandNutzen-Internal-Run mit; ihre Läufe zählen dann nur in
tool_metrics_internal_daily (Migration 0066), nicht in der Tag-90-Kennzahl und
nicht im 14-Tage-Ausgangswert. Die Tabelle hält dieselbe 30-Tage-Löschfrist
wie die Tageswerte. Danach
„Prüfmodus ausschalten“. Gilt nur im Browser, in dem er eingeschaltet wurde.
Skripte und Playwright setzen denselben Header mit dem Wert aus
METRICS_INTERNAL_RUN_TOKEN — über scripts/lib/internal-run-header.mjs
(internalRunHeaders(url) für fetch, markInternalRunsInPlaywright(context)
für Playwright), damit das Token nur an LandNutzen-Hosts geht. Nie über
Playwrights extraHTTPHeaders: Die gingen auch an Kartendienste und
Geocoder. Unmarkierte Läufe zählen öffentlich.
Es existieren keine Rohereignisse. Adresse, Koordinate, Eingabe, Ergebnis,
URL, Referrer und Besucherkennungen sind weder in der Tabelle noch in der
Admin-Antwort enthalten. Der tägliche Cleanup behält regulär den aktuellen und
die 29 vorhergehenden Kalendertage; sein Laufstatus ist im Cron-Dashboard
sichtbar. Davon getrennt bleiben der Baseline-Anker aus Tool-ID und ersten
Funnel-/Server-Messtagen sowie die während D+1 bis D+14 fortgeschriebenen
Dimensionsaggregate dauerhaft als Vergleichsreferenz erhalten. Mit Ablauf von
D+14 ist der Snapshot vollständig und bleibt ab D+15 unverändert. Er enthält
keinen Kalendertag und keine Region. Die UI nutzt Auth.js mit
platform_admin und dem bestehenden 2FA-Gate.
Endpunkte
| Methode | Pfad | Zweck |
|---|---|---|
GET | /api/v1/admin/usage | Usage je Task+Modell + Spend + Budget |
GET | /api/v1/admin/model | effektives Modell je Task |
PUT | /api/v1/admin/model | { task, model } setzen |
GET | /api/v1/admin/budget | Budget + Ist |
PUT | /api/v1/admin/budget | { dailyUsd, monthlyUsd } (null = kein Limit) |
GET | /api/v1/admin/metrics?days=14&tool=pv-yield | aggregierte Werkzeug-KPIs; Fenster 1/7/14/30 Tage |
GET | /api/v1/admin/cost-overview?days=28 | Kosten pro Tool-Abschluss, qualifiziertem Lead und aktivem Source Release; Fenster 7/14/28 Tage; internalRuns für „davon intern“ |
GET | /api/v1/admin/data-sources | Quellenkatalog, aktiver Release, pointergebundene Drift-Policy, Befunde und Alarmzustellung |
GET/POST | /api/v1/admin/impersonate | Impersonation-Audit-Log lesen bzw. read-only Kontext-Inspektion mit Audit |
MET-01-Baseline abschließen
Der revisionsfähige Abschlussprüfer liest ausschließlich die bestehenden Aggregate und gibt weder Tageswerte noch Regionen oder Besucherkennungen aus:
npm run qa:metrics:baseline -- --markdown
Vor Ablauf von D+14 endet der Befehl bewusst mit einem Fehlerstatus. Für einen reinen Zwischenstands-Readback während der laufenden Messung muss dieser Zustand ausdrücklich zugelassen werden:
npm run qa:metrics:baseline -- --markdown --allow-pending
Das Tag-30-Gate darf erst geschlossen werden, wenn der Prüfer
abschlussbereit, 14/14 vollständig abgeschlossene Messtage, vorhandene
Funnel- und Serverdaten, Funnel-Integrität und vollständige Kostenzuordnung
meldet. Der ausgegebene SHA-256 bindet den tag- und regionsfreien Snapshot an
den Nachweis.
Admin-Key erzeugen
node scripts/create-admin-key.mjs "Label"
Speichert nur den Hash (SHA-256(key+secret)); der Klartext wird
einmalig ausgegeben (nicht wiederherstellbar). Scope
{report, admin}. Verwendung:
curl -H "Authorization: Bearer ln_…" \
https://www.landnutzen.at/api/v1/admin/usage
Sicherheit
Admin-Key ist mächtig: nur Hash gespeichert, Scope-getrennt,
Rate-Limit/Audit wie jeder Key (ADR-0019/0023), Rotation manuell
(neuen Key erzeugen, alten via revoked_at sperren). UI zusätzlich
durch 2FA-Gate geschützt.