Zum Hauptinhalt springen

Kostenkontrolle & Kosten-Dashboard — Architektur-Design (OP-COST-1)

Status: in Umsetzung — Architektur-Entwurf festgezurrt (#187, erfasst als OP-COST-1 in #184); Sicht B gebaut (Slice B1 #192, 06-25, v0.5.0/Build 7 + OP-COST-2 Slices 2–3, s. §11 Stand des Baus), Sicht A vorgemerkt · Bezug: OP-COST-1 (Owning-OP) · OP-COST-2 · OP-R7-6 · OP-R10-1 · OP-BILLING-1/OP-PAY-1 (Abgrenzung) · Zielgruppe: Business/Management · IT-Dev/Architektur · True North: nur indirekt — schärft Frage 1 (Auslastung) um die Wirtschaftlichkeit (lohnt die Auslastung, wo entsteht Marge?), beantwortet keine Leitfrage selbst.

Kernaussage. Ein Kosten-Dashboard mit zwei getrennten SichtenA betriebswirtschaftlich (Kosten/Marge je Auftrag) und B Infra-/Cloud-Kosten — macht Wirtschaftlichkeit sichtbar, rollen-gegated (Scope-Entscheidung 06-25: zwei Sichten, kein Misch-KPI; Tiefe: Design + inkrementeller Bau in Slices). Sicht B ist gebaut, Sicht A vorgemerkt. Dieses Dokument bleibt der verbindliche Entwurf; Bau-Abweichungen sind in §11 nachgezogen.

1. Zweck & Abgrenzung

Kosten-Transparenz in der App als operative Entscheidungsgrundlage. True-North-Bezug: schärft Frage 1 („Wie hoch ist die Auslastung?") um die Wirtschaftlichkeit — lohnt die Auslastung, wo entsteht Marge, wo Verlust? Ergänzt OP-R9-6 (margenstärkste Kapazitäts-Angebote).

Abgrenzung — was OP-COST-1 NICHT ist:

  • ≠ Rechnungsstellung/Mahnwesen → das ist SevDesk (OP-BILLING-1, System-of-Record für Umsatz + E-Rechnung). OP-COST-1 liest Umsatz/Auftragswert für die Marge, erzeugt keine Rechnungen.
  • ≠ SaaS-Paygate (Vermarktung/Abrechnung von Taktano selbst) → OP-PAY-1 (Lastenheft §11.8).
  • ≠ Betriebs-Logs (OP-LOG-1) und Audit-Log (OP-AUDIT-1).
  • Keine Finanzbuchhaltung — die Werte sind eine sprechende betriebliche Näherung für die operative Steuerung, nicht buchhalterisch maßgeblich.

2. Scope-Entscheidung: zwei getrennte Sichten (06-25)

Bewusst getrennt gehalten — unterschiedliche Datenquellen, Aggregationsebenen, Zielgruppen und Sichtbarkeits-Rollen → zwei Bereiche/Tabs, kein Misch-KPI.

SichtInhaltZielgruppeDatenquellen
A — BetriebswirtschaftlichArbeits- + Materialkosten + Marge je Auftrag/Leistung, Soll-IstWerkstatt-ManagementLeistungsnachweise (R1-2) · Materialerfassung (R10-1) · Preismodell (R7-6)/SevDesk
B — Infra/CloudCloudflare/Fly/D1/LLM-API-Spend (Ops)Betreiber/Ownermanuelle Posten (interim) → Provider-Billing-APIs (später)

Primärer Mehrwert liegt in A (baut auf vorhandenen Fakten auf); B ist die Ops-Sicht.

3. Sicht A — Betriebswirtschaftliche Kosten & Marge

3.1 Datenmodell — gespeicherte Fakten (neu) vs. abgeleitet (D-2)

  • Stundensatz-Stammdaten (neu) — Nutzer-Entscheidung 06-26: ein €/h-Satz je Abteilung (Mitarbeiter.abteilungIdTaxonomyValue('abteilung'), strukturiert + admin-editierbar, G-1; der Freitext Mitarbeiter.role ist als Schlüssel ungeeignet) mit optionalem Override je einzelnem Mitarbeiter (Mitarbeiter-Satz gewinnt vor dem Abteilungs-Satz).
  • Zeitliche Gültigkeit (von–bis) — Nutzer-Entscheidung 06-26: jeder Satz (Abteilung und Override) trägt eine Gültigkeitsspanne (gueltigVon/gueltigBis, halb-offen [von, bis); bis offen = aktuell gültig). Sätze werden historisiert, nie überschrieben — Lohnerhöhungen/rückwirkende Korrekturen legen einen neuen Zeitabschnitt an. Für eine Berechnung wird der zum Leistungs-Zeitpunkt gültige Satz gewählt: Mitarbeiter-Override (falls am Stichtag gültig) → sonst Abteilungs-Satz am Stichtag.
  • Berechnung nur im Report (D-2) — Nutzer-Entscheidung 06-26: Sätze sind gespeicherte Fakten; die tatsächliche Kostenberechnung (istMin × Satz) ist abgeleitet und entsteht ausschließlich im Report, wird nie persistiert. Skill-basierte Sätze bleiben spätere Verfeinerung.
  • Materialpreise + Material-Verbrauch: kommen aus OP-R10-1 (strukturierte Materialerfassung — Menge/Charge/Verschnitt je Auftrag/Teilschritt) × Material-Preis-Stammdaten. OP-COST-1 konsumiert OP-R10-1, dupliziert es nicht.
  • Auftragswert/Umsatz: kommt aus OP-R7-6 (Preismodell auf Leistungen / manueller Auftragswert) bzw. SevDesk (OP-BILLING-1, Ist-Umsatz). Nötig für die Marge.
  • Abgeleitet (nie gespeichert, D-2): Arbeitskosten, Materialkosten, Gesamtkosten, Marge, Soll-Ist — analog zur bestehenden Statistik/XP-Ableitung in server/src/operativ/leistung.ts (dort gilt schon ausdrücklich: „Leistungsnachweise = gespeicherte Fakten, Statistik IMMER abgeleitet, nie persistiert").

3.2 Berechnungslogik (abgeleitet, pure Funktion + Self-Test)

  • Arbeitskosten je Auftrag = Σ über Leistungsnachweise n: (n.istMin / 60) × stundensatz(n.mitarbeiterId, zeitpunkt(n))stundensatz(...) wählt den zum Leistungs-Zeitpunkt gültigen Satz (Mitarbeiter-Override → sonst Abteilung; je Stichtag, §3.1). Basis bereits vorhanden (Leistungsnachweis.istMin + Ist-Zeitstempel, R1-2). Helfer (alsHelfer) zählen mit ihren realen Lohnkosten (unabhängig vom Lern-lernanteil, der nur die Erfahrung gewichtet).
  • Materialkosten je Auftrag = Σ (Verbrauchsmenge × Materialpreis) — gated auf OP-R10-1.
  • Gesamtkosten = Arbeits- + Materialkosten (+ optionaler Gemeinkosten-Zuschlag, später).
  • Marge je Auftrag/Leistung = Umsatz/Auftragswert − Gesamtkosten — gated auf OP-R7-6/SevDesk.
  • Soll-Ist = kalkuliert (vorgabeMin × Satz) vs. tatsächlich (istMin × Satz); die Vorgabe-Ist-Spanne steckt bereits je Nachweis in vorgabeMin/istMin.
  • Analyse-Dimensionen: Güteklasse (1–3), Anspruch (1–3), Leistung, Abteilung, Zeitraum.

3.3 Was jetzt baubar ist

Die Arbeitskosten-Sicht (Slice A1) ist ohne neue Abhängigkeit baubar — nur Stundensatz-Stammdaten

  • Ableitung aus vorhandenen Leistungsnachweisen. Material (A2) und Marge (A3) brauchen OP-R10-1 bzw. OP-R7-6/SevDesk.

4. Sicht B — Infra-/Cloud-Kosten (Ops)

  • Quellen: Cloudflare (Worker/DO/D1/R2/Pages), Fly.io (Solver-Service), LLM-API-Spend (Anthropic; Bezug OP-AI-1/OP-AI-4), Domains/Sonstiges.
  • Erfassung — VERBINDLICHE RICHTUNG (Nutzer 06-26): automatischer Pull über die jeweiligen Provider-APIs. Die Infra-Kosten werden je Dienst direkt aus dessen API gezogen (Cloudflare GraphQL Analytics/Billing, Fly.io Machines-/Billing-API, Anthropic Usage/Cost-API, Domain-Registrar …) — periodisch (Cron-Worker), je Dienst × Monat. Manuelle Erfassung (Slice B1) bleibt Fallback/Bootstrap (Dienste ohne API, Korrekturen, „Sonstiges"). API-Keys/Tokens je Provider als Secrets (kein Repo-PII). Datenmodell-Erweiterung: quelle: 'api' | 'manuell' + Provider-Referenz/Idempotenz-Key (kein Doppel-Pull). Bezug OP-OBS-1 (Usage-Metriken) · OP-AI-1/OP-AI-4 (LLM-Spend). Als OP-COST-2 geführt.
  • Stand — OP-COST-2 Slice 1 (GitHub) GEBAUT (#202, v0.12.0/Build 15): GitHub als erster Provider mit projekt-bezogenem Auto-Pull. InfraKostenPosten um quelle:'api'|'manuell' + quelleRef (Idempotenz, Upsert ersetzt by quelleRef); pure server/src/kosten/github-billing.ts mappt die Enhanced-Billing- Usage-API (/organizations/<org>/settings/billing/usage) je Monat auf einen „GitHub"-Posten, gefiltert auf das Repo (GITHUB_BILLING_REPO); Self-Test test:kosten. DO pullGithubKosten() ist dormant/deploy- sicher (No-op ohne Secret GITHUB_BILLING_TOKEN + GITHUB_BILLING_ORG-[var], wie der D1-/R2-Pfad). Trigger interim on-demand (Admin-Action infraKosten.pullGithub + Button „↻ GitHub ziehen" + API-Badge); Pull = OP-AUDIT-1-Event (infraKosten.pull). Aktivierung = Pre-Prod/Go-Live-Requirement OP-COST-4: wrangler secret put GITHUB_BILLING_TOKEN (Billing/Plan-Read-Scope; Org/Repo bereits als [vars]). Folge-Slices: Cron-Automatik (verbindliche Richtung) + weitere Provider (Cloudflare/Fly/Anthropic). Hinweis: GitHub rechnet in USD ab → Mehrwährung (FX = OP-I18N-1).
  • Stand — OP-COST-2 Slice 2 (LLM-API-Spend) GEBAUT (#TBD, v0.43.0): Zweiter Provider mit Auto-Pull. Pure server/src/kosten/llm-billing.ts mappt die Anthropic Usage-/Cost-API (/v1/organizations/cost_report, Admin-Key) je Zeitbucket (starting_at+results[].amount/currency) auf einen Monatsposten (Dienst-Label „Anthropic API (LLM)", per LLM_BILLING_DIENST überschreibbar), quelle:'api' + quelleRef=llm:YYYY-MM (Idempotenz-Upsert). DO pullLlmKosten() ist dormant/deploy-sicher (No-op ohne Secret LLM_BILLING_KEY; Endpoint per LLM_BILLING_URL [var] provider-flexibel). Trigger interim on-demand (Admin-Action infraKosten.pullLlm + Button „↻ LLM ziehen"); Pull = OP-AUDIT-1-Event (infraKosten.pull). Self-Test test:kosten deckt Monats-Aggregation, Idempotenz, Label/Währungs-Konfiguration und Betrag-Robustheit ab. Zeitstempel des letzten erfolgreichen Abholens (Nutzer-Wunsch): je Auto-Pull-Komponente wird der Zeitpunkt des letzten erfolgreichen Pulls persistiert (infraKostenPull: Dienst→epoch-ms, schemaVersion 17→18, Merge-Default) und dezent angezeigt — als Caption unter dem jeweiligen „↻ …ziehen"-Button, als Inline-Stempel je API-Posten-Zeile und im API-Badge-Tooltip (relativ „vor 3 Min", ab 1 Tag absolut). Aktivierung = Pre-Prod-Requirement OP-COST-4: GitHub-Actions-Secret LLM_BILLING_KEY setzen — der CI-deploy-Job spiegelt es (wie ASSISTENT_LLM_KEY) vor wrangler deploy via wrangler secret put nach Cloudflare (Everything-as-Code; dormant-sicher übersprungen, wenn ungesetzt).
  • Stand — OP-COST-2 Slice 3 (Mistral = realer Anbieter, ECHTE Kosten) GEBAUT (#TBD, v0.44.0): Da der In-App-Assistent tatsächlich über Mistral (EU) läuft (nicht Anthropic), zieht Taktano die tatsächlichen LLM-Kosten aus Mistrals Admin-Usage-API (GET https://console.mistral.ai/api/admin/usage?month=M&year=Y, Admin-API-Key ≠ ASSISTENT_LLM_KEY) — echte Beträge je Modell/Monat, nicht Token×Listenpreis. Pure server/src/kosten/mistral-billing.ts (mistralUsageZuPosten) mappt die Antwort auf einen Monatsposten (Dienst „Mistral (LLM)", quelle:'api', quelleRef=mistral:YYYY-MM, idempotenter Upsert). DO pullMistralKosten() zieht laufenden + Vormonat (Monatswechsel-sicher), dormant ohne Secret MISTRAL_ADMIN_KEY; Aktion infraKosten.pullMistral + Button „↻ Mistral ziehen". Schema-Unsicherheit (Beta-Console-API, öffentlich nicht dokumentiert): der Kosten-Extraktor ist defensiv (bevorzugt ein explizites Gesamt-Kostenfeld, sonst Summe der Posten-Liste; Feldnamen/Einheit an EINER Stelle justierbar, MISTRAL_BETRAG_IN_CENT für Cent-Antworten) — nach dem ersten echten Pull Feld/Einheit bestätigen. Key ebenfalls via GitHub-Secret MISTRAL_ADMIN_KEY (CI-Sync). Hinweis: öffentliche Listen-Preise stehen nur auf mistral.ai/pricing; die API liefert die realen Kosten. Der Anthropic-Pfad (Slice 2) bleibt parallel dormant nutzbar, falls der Betrieb auf Anthropic wechselt.
  • Aggregation: je Dienst × Zeitraum + Trend; optionale Umlage auf Mandanten = Future (Multi-Tenant).
  • Zielgruppe/Rolle: Betreiber/Owner — strenger gated als Sicht A.

5. Datenmodell-Konventionen

  • Fakten gespeichert, Sichten abgeleitet (D-2) — wie leistung.ts. Kosten/Margen NIE persistieren.
  • Stammdaten admin-editierbar (G-1): Stundensätze, Materialpreise, Infra-Posten = stammdaten-artig → editierbar; Zuschläge/Schwellen dokumentiert.
  • Versioniert + idempotente Migration (schemaVersion-Muster in server/party/leitstand.ts) für die neuen Stammdaten; Persistenz zunächst DO-Blob (wie übrige Stammdaten), wandert mit OP-DATA-1 (Drizzle/D1) mit.

6. Rollen & Sichtbarkeit (sensibel!)

  • Kostendaten = vertraulich (betriebswirtschaftlich; abteilungs-/mitarbeiterbezogene Sätze potenziell personensensibel). Default: nur Management/Owner sieht Sicht A; Sicht B nur Betreiber/Owner.
  • Abhängigkeit OP-AUTH-1: ein granulares Rollenmodell existiert noch nicht (Cloudflare Access interim; Better Auth + Organizations = future). Interim: server-seitiges Gate hinter einem Admin/Manager-Flag; keine Kostendaten im Broadcast-State an Nicht-Berechtigte (kein Leak, vgl. OP-SEC-1 #6).
  • Audit (OP-AUDIT-1): Anlegen/Ändern von Stundensätzen/Materialpreisen ist margen-relevant → als Audit-Event (Stammdaten-Änderung); optional Zugriff auf Kosten-Sichten protokollieren.
  • PII/DSGVO: mitarbeiterbezogene Sätze datensparsam; in Logs/Audit keine Klartext-Sätze/-Gehälter (PII-Scrubbing, OP-LOG-1).

7. UI / Dashboard

  • Eigener Workspace „Kosten" (Sidebar, eigene Signaturfarbe --tk-ws-costs; nur --tk-*-Tokens, kein Hardcode-Hex; Mono-Font für alle Zahlen — Design-Vertrag design/CLAUDE.md).
  • Zwei Tabs: „Betrieb" (Sicht A) · „Infra" (Sicht B) — getrennt, kein Misch-KPI.
  • Sicht A: KPI-Kacheln (Kosten/Umsatz/Marge gesamt + je Auftrag/Leistung), Top-/Flop-Margen, Soll-Ist-Bänder, Trends, Filter (Zeitraum/Leistung/Abteilung/Güteklasse). Read-only, abgeleitet.
  • Sicht B: Spend je Dienst × Monat, Trend, größte Posten.
  • True-North-Platzierung: verdichtete Marge-/Kosten-Kennzahl optional im Überblick-Raum (rollen-gegated).

8. Build-Slices (Vorschlag)

  1. Slice A1 — Arbeitskosten je Auftrag (baubar JETZT): Stundensatz-Stammdaten je Abteilung (CRUD, G-1)
    • abgeleitete Arbeitskosten aus vorhandenen Leistungsnachweisen (istMin × Satz) → read-only KPI/Panel
    • Soll-Ist (Vorgabe vs. Ist). Pure Ableitungs-Funktion + Self-Test (test:kosten, CI). Keine neue Abhängigkeit. → entsperrt sofort einen Teil von True-North-„Wirtschaftlichkeit".
  2. Slice A2 — Materialkosten: gated auf OP-R10-1 (strukturierte Materialerfassung) + Materialpreis-Stammdaten → Gesamtkosten je Auftrag/Leistung.
  3. Slice A3 — Marge & Dashboard: gated auf OP-R7-6/OP-BILLING-1 (Auftragswert/Umsatz) → Marge je Auftrag/Leistung + volles Dashboard (Trends/Filter/Top-Flop).
  4. Slice B1 — Infra-Kosten (Ops): manuelle Erfassung je Posten/Monat + Ops-Dashboard; später Provider-API-Pull (Bezug OP-OBS-1).
  • Rollen-Gate zieht sich durch alle Slices; harte server-seitige Durchsetzung sobald OP-AUTH-1 steht (interim Admin-Flag).

9. Compliance & Risiko

  • OP-COMPLIANCE-1: Kostendaten-Vertraulichkeit + Rollen-Gate = Beleg fürs Zugriffs-/Rollenmodell (ISO 27001) → in docs/Compliance.md nachziehen, sobald gebaut.
  • Risiko (→ docs/betrieb/Risikoregister.md): (a) Garbage-in — falsche Stundensätze/Materialpreise → irreführende Margen/Fehlentscheidungen (Maßnahme: sprechende Stammdaten-Pflege, Soll-Ist-Plausibilisierung, Audit der Änderungen); (b) Sichtbarkeit ohne Rollenmodell — sensible Kosten vor OP-AUTH-1 (Maßnahme: interim server-seitiges Admin-Gate, kein Broadcast-Leak).

10. Bezüge

OP-R1-2 (Leistungsnachweise/Ist-Zeit = Arbeitskosten-Basis) · OP-R10-1 (Materialerfassung = Materialkosten) · OP-R7-6 (Preismodell/Auftragswert = Umsatz/Marge) · OP-BILLING-1 (SevDesk = Ist-Umsatz) · OP-AUTH-1 (Rollen/Sichtbarkeit) · OP-AUDIT-1 (Stammdaten-/Zugriffs-Audit) · OP-OBS-1 (Infra-Usage-Metriken) · OP-AI-1/OP-AI-4 (LLM-Spend) · OP-R9-6 (margenstärkste Kapazitäts-Angebote) · OP-PAY-1 (SaaS-Paygate, abgegrenzt) · G-1 (editierbare Stammdaten) · D-2 (derived state) · True North (Wirtschaftlichkeit/Auslastung).

11. Stand des Baus

Slice B1 — Infra-/Cloud-Kosten (Sicht B) — gebaut (#192, 06-25, Version 0.5.0/Build 7)

  • Datenmodell (Fakten): InfraKostenPosten (server/src/kosten/infra.ts) — { dienst, monat (YYYY-MM), betragCent, waehrung, notiz?, erfasstAm }, Beträge ganzzahlig in Cent (kein Float-Drift). Persistiert im DO-Blob (infraKostenPosten, schemaVersion 12→13, idempotent leer initialisiert; wandert mit OP-DATA-1 nach D1).
  • Ableitung (D-2, nie gespeichert): pure infraKostenAuswertung() — Summe gesamt · je Dienst (+ Anteil) · je Monat (Verlauf) · Monats-Trend (kalendarischer Vormonat; Lücke = kein Trend) · größte Posten · Mehrwährungs-Flag. Self-Test npm run test:kosten (+ CI) — deckt Aggregation, Trend, Top-Posten, Mehrwährung, Jahreswechsel, leeren Fall ab.
  • Aktionen (admin-gegated): infraKosten.hinzufuegen|aktualisieren|entfernen (Server validiert Monat/Betrag, speichert Cent). UI: Dienst frei + Vorschlagsliste (G-1), Betrag inline editierbar, Zeile entfernbar.
  • Rollen-Gate (kein Leak) — interim, bis OP-AUTH-1 A-3: Der DO bestimmt je Verbindung die Fähigkeit kosten aus der im Menü gewählten Rolle (?rolle=-Query beim Verbinden → capsAusRolle; Buchhaltung/Inhaber-Chef = kosten) und filtert die Infra-Kosten je Verbindung aus dem Broadcast — Nicht-Berechtigte erhalten das Feld infraKosten nie, auch Schreib-Aktionen (infraKosten.*) sind gegated. → erfüllt §6 („kein Broadcast-Leak"). ⚠ Interim self-declared (A-1.5): die Rolle ist selbst gewählt (kein Nachweis) — der Zugang ist am Edge durch Cloudflare Access geschützt (Golden Rule), die verifizierte Durchsetzung (Rolle aus Better-Auth-JWT statt Selbstauskunft) und Sicht A folgen mit OP-AUTH-1 A-3. Die frühere KOSTEN_ADMIN_EMAILS-E-Mail-Allowlist wurde entfernt (v0.156.0). Der Audit-Actor („wer erfasste?") kommt weiterhin aus der Access-verifizierten X-Taktano-User-E-Mail (client-Kopie gestrippt, Anti-Spoofing).
  • UI: eigener Workspace „Kosten" (Sidebar, nur für kostenAdmin sichtbar; Signaturfarbe --tk-ws-costs = #84cc16 / Light #4d7c0fprovisorisch, distinkte Hue in der vollen Palette). Tab Infra (KPI-Kacheln · Spend je Dienst · Monats-Verlauf · größte Posten · Erfassungs-Tabelle) gebaut; Tab Betrieb (Sicht A) als Platzhalter vorgemerkt. Mono-Zahlen, nur --tk-*-Tokens (No-Hex-Gate grün).
  • Bewusste Vereinfachungen / offen: Mehrwährungs-Umrechnung (FX) noch nicht → ungewichtete Summe + sichtbarer Hinweis (OP-I18N-1); dienst als freier String + Vorschlagsliste statt eigener Taxonomie-Persistenz; Provider-Billing-/Usage-API-Pull statt manueller Erfassung = später (OP-OBS-1). Audit der Posten-Änderungen GEBAUT (#199): jede infraKosten.*-Aktion → Ops-Log-Zeile (interim console) und dauerhaftes D1-Audit-Event (OP-AUDIT-1, event_type=infraKosten.*); „wer hat erfasst/geändert" (Actor) wird vollständig, sobald das Access-Gate scharf ist (OP-COST-3) — sonst unbekannt.

OP-COST-2 Slice 2 — LLM-API-Spend-Auto-Pull + „zuletzt gezogen"-Stempel — gebaut (v0.43.0)

  • Zweiter Auto-Pull-Provider: pure server/src/kosten/llm-billing.ts (llmCostZuPosten) bildet die Anthropic Usage-/Cost-API (data[]-Zeitbuckets) je Monat auf einen InfraKostenPosten ab (Dienst „Anthropic API (LLM)"), quelle:'api' + quelleRef=llm:YYYY-MM (idempotenter Upsert, kein Doppel-Pull). Beträge ganzzahlig in Cent; Betrag/Datum defensiv geparst (kaputt ⇒ 0/übersprungen statt Crash). DO pullLlmKosten() dormant ohne LLM_BILLING_KEY (Secret); Endpoint/Label per LLM_BILLING_URL/LLM_BILLING_DIENST ([vars]) überschreibbar. UI: Button „↻ LLM ziehen" neben „↻ GitHub ziehen" (admin-gegated), Aktion infraKosten.pullLlm, Audit-Event infraKosten.pull (Actor llm-pull).
  • Zeitstempel des letzten erfolgreichen Abholens (je Auto-Pull-Komponente, Nutzer-Wunsch): persistiert als infraKostenPull: Record<Dienst, epoch-ms> (State, schemaVersion 17→18, Merge-Default; gesetzt bei jedem erfolgreichen HTTP-Abruf — auch ohne inhaltliche Änderung). Im DTO als letzterPull zum Client. Dezent angezeigt: Caption unter dem jeweiligen Pull-Button (zuletzt: vor 3 Min bzw. noch nie gezogen), Inline-Stempel je API-Posten-Zeile und im API-Badge-Tooltip (relativ, ab 1 Tag absolut dd.mm.yyyy, hh:mm). Nur --tk-*-Tokens, Mono-Zahlen (No-Hex-Gate grün). Self-Test npm run test:kosten erweitert (LLM-Mapper) + test:do-migrations (v→18).

OP-COST-2 Slice 3 — Mistral (realer Anbieter): ECHTE LLM-Kosten via Admin-Usage-API — gebaut (v0.44.0)

  • Warum: Der Assistent läuft real über Mistral (EU), nicht Anthropic → die tatsächlichen LLM-Kosten kommen aus Mistrals Admin-Usage-API (GET console.mistral.ai/api/admin/usage?month=M&year=Y, Bearer Admin-Key). Wir nehmen die echten Beträge der API (je Modell/Monat), nicht Token×Listenpreis.
  • Umsetzung: pure server/src/kosten/mistral-billing.ts (mistralUsageZuPosten, mistralUsageUrl) → ein Monatsposten „Mistral (LLM)", quelle:'api', quelleRef=mistral:YYYY-MM (idempotent). DO pullMistralKosten() zieht laufenden + Vormonat, dormant ohne MISTRAL_ADMIN_KEY; Aktion infraKosten.pullMistral + Button „↻ Mistral ziehen"; Audit infraKosten.pull (Actor mistral-pull); „zuletzt gezogen"-Stempel wie GitHub/Anthropic. Key via GitHub-Secret MISTRAL_ADMIN_KEY (CI-Sync).
  • Schema-Unsicherheit (bewusst): Die Beta-Console-API ist öffentlich nicht dokumentiert (docs.mistral.ai bot-blockiert; das offizielle SDK exponiert die Route nicht). Der Kosten-Extraktor ist deshalb defensiv (Gesamt-Kostenfeld bevorzugt, sonst Summe der Posten-Liste; Feldnamen/Einheit an einer Stelle justierbar, MISTRAL_BETRAG_IN_CENT) und best-effort — nach dem ersten echten Pull Feld/Einheit einmalig verifizieren. Self-Test test:kosten deckt beide Antwort-Formen, Cent-Einheit, URL-Bau und den Leer-/Kaputt-Fall ab.

Sicht A (betriebswirtschaftlich) — offen

Slice A1 (Arbeitskosten je Auftrag) ist laut §3.3/§8 ohne neue Abhängigkeit baubar (Stundensatz-Stammdaten je Abteilung + Ableitung aus vorhandenen Leistungsnachweisen). A2 (Material, gated OP-R10-1) und A3 (Marge + volles Dashboard, gated OP-R7-6/SevDesk) folgen. Der „Betrieb"-Tab im Kosten-Workspace ist bereits angelegt. Slice-A1-Datenmodell (Nutzer 06-26, §3.1): Stundensatz je Abteilung + Mitarbeiter-Override, je mit Von-Bis-Gültigkeit (historisiert); Berechnung nur im Report (D-2).

12. Kosten-Gate — Rolle statt E-Mail-Allowlist (seit v0.156.0)

Umstellung 2026-07-26 (OP-AUTH-1 A-1.5): Das Kosten-Gate hängt nicht mehr an einer E-Mail-Allowlist (KOSTEN_ADMIN_EMAILS entfernt), sondern an der im ⚙-Menü gewählten Rolle: die Fähigkeit kosten haben Buchhaltung und Inhaber/Chef. Der DO filtert die Infra-Kosten je Verbindung aus der Rolle (?rolle=-Query → capsAusRolle) — Nicht-Berechtigte erhalten das Feld infraKosten nie, Schreib-Aktionen ebenso gegated (kein Broadcast-Leak, §6). Das frühere allowlist-basierte Owner-only-Runbook entfällt.

⚠ Interim self-declared (A-1.5): die Rolle ist selbst gewählt (kein Nachweis). Der Zugang ist am Edge durch Cloudflare Access geschützt (Golden Rule „Zero Trust vor Public" — WER erreicht die App überhaupt); die verifizierte Kosten-Durchsetzung (Rolle aus dem Better-Auth-JWT-Claim statt Selbstauskunft) folgt mit OP-AUTH-1 A-3. Bis dahin geführt als RISK-12 (Risikoregister).

Access produktiv aktivieren (weiterhin sinnvoll als Netz-Gate, Golden Rule) — ACCESS_TEAM_DOMAIN + ACCESS_AUD in server/wrangler.toml [vars] setzen + mergen (CD redeployt). Bereitliegende Werte (drkv-com, kein PII): ACCESS_TEAM_DOMAIN = "drkv-com.cloudflareaccess.com" · ACCESS_AUD = "ffafb06fcf4d3da1b46a700ca146d488a3a930c647fca7106e8dd71ee5ac9ba6". ⚠ Lockout: falsche Werte sperren alle Requests (401) → vor dem Merge prüfen; Rollback = Werte wieder leeren + redeploy. Access liefert zudem die verifizierte X-Taktano-User-E-Mail als Audit-Actor (OP-AUDIT-1).

Verifikation: im ⚙-Menü Rolle Buchhaltung/Inhaber-Chef wählen → Workspace „Kosten" + Posten sichtbar; Rolle Werker/Werkstattleitung → keine Kosten-Daten im State, Workspace fehlt.

13. Reports & Export (OP-EXPORT-1, Nutzer 06-26)

Verbindlich für ALLE Reports (Kosten und darüber hinaus): jeder Report kommt mit Diagramm und ist exportierbar als (a) PDF (formatierte Sicht inkl. Diagramm) und (b) Rohdaten — CSV und Excel (.xlsx). Gilt für die Kosten-Sichten A/B ebenso wie für künftige Reports (Auslastung, Leistung, Management-Summary OP-REPORT-1). Cross-cutting als OP-EXPORT-1 geführt; Architektur folgt (Diagramm-Lib · serverseitiges PDF/XLSX-Rendering vs. Client · Rohdaten direkt aus den abgeleiteten Aggregaten, D-2). Bezug: OP-REPORT-1, OP-COST-1.


↩ Zurück zur Doku-Landkarte · Lesepfade · Register (alle OPs)