[MSP] Verbrauchsreport je Mandant + Metrik-Rollup und Retention (Abrechnungsgrundlage) #218

Open
opened 2026-07-20 23:15:00 +00:00 by chinux · 1 comment
Owner

Monatlicher Verbrauchsnachweis je Mandant als Grundlage für nutzungsbasierte Abrechnung. Die Daten liegen vollständig vor — es fehlt die Aggregationsschicht.

Datenlage

Vorhanden: Tenant.customer_number, TenantNode + TenantVMRange, NodeMetric/VMMetric (Zeitreihen), maxmem/maxdisk aus Proxmox, Storage-Belegung in Node.last_data, BackupLog mit size und Zeitstempel.

Zwei Befunde vorab:

  1. NodeMetric und VMMetric haben keinerlei Retention — kein Pruning, kein Rollup, keine Aufbewahrungsgrenze. Die Tabellen wachsen unbegrenzt. Für Monatsmittel ist das einerseits gut (Historie ist da), andererseits ist die Rohauflösung die falsche Granularität für Reports und die Tabellen werden mit der Zeit unbenutzbar.
  2. Die Mandantenzuordnung über TenantVMRange fehlt an mehreren Stellen (siehe #212/#209). Ein Verbrauchsreport, der Guests dem falschen Mandanten zurechnet, ist schlimmer als keiner — er landet auf einer Rechnung.

Kennzahlen

Pro Mandant und Monat:

Kennzahl Quelle Aggregat
Anzahl Guests (VM/CT) Bestand Mittel + Höchststand
vCPU zugewiesen cpus Mittel + Höchststand
RAM zugewiesen maxmem Mittel + Höchststand
RAM belegt VMMetric.ram_used_mb Mittel + 95. Perzentil
Storage zugewiesen maxdisk Mittel + Höchststand
Storage belegt (Gast) vm_scans[].disk Mittel
Backup-Volumen BackupLog.size Summe + Anzahl Läufe
Nodes TenantNode Höchststand

Höchststand ist die abrechnungsrelevante Größe, nicht der Mittelwert — wer 20 Tage 8 VMs und 10 Tage 12 VMs betreibt, hat 12 bereitgestellt bekommen. Beides ausweisen, damit die Grundlage nachvollziehbar bleibt.


Prompt für Claude Code

Baue einen monatlichen Verbrauchsreport je Mandant in theProx.
SETZT #212 VORAUS (korrekte Mandantenzuordnung inkl. TenantVMRange).

--- 1. MIGRATION: ROLLUP + RETENTION ---
Neue Tabelle usage_daily (Tagesaggregat, die eigentliche Reportgrundlage):
    id, tenant_id, node_id, vmid NULL, day (Date)
    guests, vcpu, ram_assigned_mb, ram_used_avg_mb, ram_used_p95_mb,
    disk_assigned_mb, disk_used_mb, backup_bytes, backup_runs,
    samples (int)   -- wie viele Rohwerte eingeflossen sind
    UNIQUE (tenant_id, node_id, vmid, day)
    Index auf (tenant_id, day)

"samples" ist wichtig: ein Tag mit 3 statt 288 Messwerten ist nicht
belastbar und muss im Report als solcher erkennbar sein.

Retention (eigener Settings-Key "metrics.retention", konfigurierbar):
    node_metrics / vm_metrics roh:  35 Tage (Default)
    usage_daily:                    unbegrenzt (klein, Abrechnungsnachweis)
Pruning im Scheduler, batchweise mit LIMIT, damit kein Lock-Sturm entsteht.
ACHTUNG: das ist die erste Löschroutine auf diesen Tabellen — vor dem
scharf Schalten die Zeilenzahl loggen und einen Trockenlauf-Modus
("metrics.retention.dry_run") anbieten.

--- 2. ROLLUP-JOB ---
server/services/usage.py:
    async def rollup_day(day: date) -> dict
    async def maybe_rollup()   -- Scheduler-Tick, taeglich, rechnet den
                                  VORTAG (nie den laufenden Tag)

Idempotent: erneuter Lauf fuer denselben Tag ueberschreibt, addiert nicht.
Ein Tag ohne Rohdaten erzeugt KEINE Zeile (nicht 0 schreiben — fehlend und
null sind verschiedene Aussagen).

Mandantenzuordnung ueber die Funktion aus #212 (TenantVMRange vor
TenantNode). Guests ohne Mandant laufen unter einem festen Pseudo-Tenant,
damit sie nicht still aus der Summe fallen.

--- 3. REPORT-SERVICE ---
    async def usage_report(tenant_id, year, month) -> dict

Liefert die Kennzahlentabelle aus dem Issue-Text, je Guest aufgeschluesselt
plus Mandantensumme. Zusaetzlich:
    "coverage": <anteil tage mit daten>,  -- 0.0–1.0
    "incomplete_days": [...]
Ein Report mit Luecken muss das SAGEN, nicht glaetten.

--- 4. API ---
    GET /api/usage/{tenant_id}?year=&month=        -> JSON
    GET /api/usage/{tenant_id}/export?format=csv   -> CSV
    GET /api/usage?year=&month=                    -> alle Mandanten, Summen
RBAC: Superadmin sieht alle; Mandanten-Nutzer nur den eigenen (bestehende
UserTenantRole-Logik nutzen, nicht neu bauen).

--- 5. FRONTEND ---
Neuer Settings-Tab "Verbrauch" (Muster: TenantsTab.svelte):
Mandantenauswahl, Monatsauswahl, Kennzahlentabelle, CSV-Download,
Hinweisbanner bei coverage < 0.95 mit den fehlenden Tagen.

--- 6. TESTS ---
  - rollup_day idempotent (zweimal laufen = gleiches Ergebnis)
  - Guest wechselt mitten im Monat den Mandanten -> tagegenau zugeordnet
  - Tag ohne Rohdaten -> keine Zeile, coverage sinkt
  - Hoechststand != Mittelwert bei schwankender Guest-Zahl
  - Retention loescht Rohdaten, laesst usage_daily unberuehrt
  - RBAC: Mandanten-Nutzer sieht fremde Mandanten nicht

Definition of Done

  • usage_daily mit idempotentem Rollup
  • Retention für Rohmetriken, mit Trockenlauf
  • Mittel und Höchststand je Kennzahl
  • Datenlücken werden ausgewiesen, nicht geglättet
  • CSV-Export, RBAC-korrekt
Monatlicher Verbrauchsnachweis je Mandant als Grundlage für nutzungsbasierte Abrechnung. Die Daten liegen vollständig vor — es fehlt die Aggregationsschicht. ## Datenlage Vorhanden: `Tenant.customer_number`, `TenantNode` + `TenantVMRange`, `NodeMetric`/`VMMetric` (Zeitreihen), `maxmem`/`maxdisk` aus Proxmox, Storage-Belegung in `Node.last_data`, `BackupLog` mit `size` und Zeitstempel. **Zwei Befunde vorab:** 1. `NodeMetric` und `VMMetric` haben **keinerlei Retention** — kein Pruning, kein Rollup, keine Aufbewahrungsgrenze. Die Tabellen wachsen unbegrenzt. Für Monatsmittel ist das einerseits gut (Historie ist da), andererseits ist die Rohauflösung die falsche Granularität für Reports und die Tabellen werden mit der Zeit unbenutzbar. 2. Die Mandantenzuordnung über `TenantVMRange` fehlt an mehreren Stellen (siehe #212/#209). Ein Verbrauchsreport, der Guests dem falschen Mandanten zurechnet, ist schlimmer als keiner — er landet auf einer Rechnung. ## Kennzahlen Pro Mandant und Monat: | Kennzahl | Quelle | Aggregat | |---|---|---| | Anzahl Guests (VM/CT) | Bestand | Mittel + Höchststand | | vCPU zugewiesen | `cpus` | Mittel + Höchststand | | RAM zugewiesen | `maxmem` | Mittel + Höchststand | | RAM belegt | `VMMetric.ram_used_mb` | Mittel + 95. Perzentil | | Storage zugewiesen | `maxdisk` | Mittel + Höchststand | | Storage belegt (Gast) | `vm_scans[].disk` | Mittel | | Backup-Volumen | `BackupLog.size` | Summe + Anzahl Läufe | | Nodes | `TenantNode` | Höchststand | **Höchststand ist die abrechnungsrelevante Größe**, nicht der Mittelwert — wer 20 Tage 8 VMs und 10 Tage 12 VMs betreibt, hat 12 bereitgestellt bekommen. Beides ausweisen, damit die Grundlage nachvollziehbar bleibt. --- ## Prompt für Claude Code ``` Baue einen monatlichen Verbrauchsreport je Mandant in theProx. SETZT #212 VORAUS (korrekte Mandantenzuordnung inkl. TenantVMRange). --- 1. MIGRATION: ROLLUP + RETENTION --- Neue Tabelle usage_daily (Tagesaggregat, die eigentliche Reportgrundlage): id, tenant_id, node_id, vmid NULL, day (Date) guests, vcpu, ram_assigned_mb, ram_used_avg_mb, ram_used_p95_mb, disk_assigned_mb, disk_used_mb, backup_bytes, backup_runs, samples (int) -- wie viele Rohwerte eingeflossen sind UNIQUE (tenant_id, node_id, vmid, day) Index auf (tenant_id, day) "samples" ist wichtig: ein Tag mit 3 statt 288 Messwerten ist nicht belastbar und muss im Report als solcher erkennbar sein. Retention (eigener Settings-Key "metrics.retention", konfigurierbar): node_metrics / vm_metrics roh: 35 Tage (Default) usage_daily: unbegrenzt (klein, Abrechnungsnachweis) Pruning im Scheduler, batchweise mit LIMIT, damit kein Lock-Sturm entsteht. ACHTUNG: das ist die erste Löschroutine auf diesen Tabellen — vor dem scharf Schalten die Zeilenzahl loggen und einen Trockenlauf-Modus ("metrics.retention.dry_run") anbieten. --- 2. ROLLUP-JOB --- server/services/usage.py: async def rollup_day(day: date) -> dict async def maybe_rollup() -- Scheduler-Tick, taeglich, rechnet den VORTAG (nie den laufenden Tag) Idempotent: erneuter Lauf fuer denselben Tag ueberschreibt, addiert nicht. Ein Tag ohne Rohdaten erzeugt KEINE Zeile (nicht 0 schreiben — fehlend und null sind verschiedene Aussagen). Mandantenzuordnung ueber die Funktion aus #212 (TenantVMRange vor TenantNode). Guests ohne Mandant laufen unter einem festen Pseudo-Tenant, damit sie nicht still aus der Summe fallen. --- 3. REPORT-SERVICE --- async def usage_report(tenant_id, year, month) -> dict Liefert die Kennzahlentabelle aus dem Issue-Text, je Guest aufgeschluesselt plus Mandantensumme. Zusaetzlich: "coverage": <anteil tage mit daten>, -- 0.0–1.0 "incomplete_days": [...] Ein Report mit Luecken muss das SAGEN, nicht glaetten. --- 4. API --- GET /api/usage/{tenant_id}?year=&month= -> JSON GET /api/usage/{tenant_id}/export?format=csv -> CSV GET /api/usage?year=&month= -> alle Mandanten, Summen RBAC: Superadmin sieht alle; Mandanten-Nutzer nur den eigenen (bestehende UserTenantRole-Logik nutzen, nicht neu bauen). --- 5. FRONTEND --- Neuer Settings-Tab "Verbrauch" (Muster: TenantsTab.svelte): Mandantenauswahl, Monatsauswahl, Kennzahlentabelle, CSV-Download, Hinweisbanner bei coverage < 0.95 mit den fehlenden Tagen. --- 6. TESTS --- - rollup_day idempotent (zweimal laufen = gleiches Ergebnis) - Guest wechselt mitten im Monat den Mandanten -> tagegenau zugeordnet - Tag ohne Rohdaten -> keine Zeile, coverage sinkt - Hoechststand != Mittelwert bei schwankender Guest-Zahl - Retention loescht Rohdaten, laesst usage_daily unberuehrt - RBAC: Mandanten-Nutzer sieht fremde Mandanten nicht ``` ## Definition of Done - [ ] `usage_daily` mit idempotentem Rollup - [ ] Retention für Rohmetriken, mit Trockenlauf - [ ] Mittel **und** Höchststand je Kennzahl - [ ] Datenlücken werden ausgewiesen, nicht geglättet - [ ] CSV-Export, RBAC-korrekt
Author
Owner

Teil des MSP-Themenblocks: #218, #219, #220, #221, #222, #223, #224, #225. Reihenfolge-Vorschlag: #218 (Datengrundlage) → #223 (Vertragsstatus) → #220 (Wartungsfenster) → #219 (Kundenbericht) → Rest.

Teil des MSP-Themenblocks: #218, #219, #220, #221, #222, #223, #224, #225. Reihenfolge-Vorschlag: #218 (Datengrundlage) → #223 (Vertragsstatus) → #220 (Wartungsfenster) → #219 (Kundenbericht) → Rest.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
chinux/theProx#218
No description provided.