Zum Inhalt

XOLIB SCORE — VOLLSTÄNDIGE IMPLEMENTIERUNG

Claude Code Prompt · v1 + v2 + Hash-Chaining + UI


KONTEXT

Du arbeitest an Xolib, einer KI-nativen B2B SaaS-Plattform für deutsche Hausverwaltungen. Stack: Next.js, TypeScript, Prisma, PostgreSQL, OpenAI GPT-4o-mini, Tailwind CSS, Lucide React Icons.

Lies zuerst CLAUDE.md und MEMORY.md vollständig. Mach dich mit der bestehenden Codebase vertraut, insbesondere: - Bestehende Prisma-Modelle: Property, Unit, Tenancy, Payment, Ticket, ServiceProvider, Document, OperatingCost - Bestehende API-Struktur unter /api/v1/ - Bestehende UI-Komponenten, insbesondere KpiDonutGrid - i18n-Setup (8 Sprachen, Pflicht auf jedem neuen Feature) - Bestehende Background-Job / Cron-Infrastruktur


ZIEL

Implementiere den Xolib Score — einen objektiven, datengetriebenen Gebäude-Bewertungsindex von 0 bis 100. Er aggregiert Echtzeit-Daten aus der gesamten Plattform und berechnet täglich einen aktualisierten Score pro Objekt (Property).

Der Score ist das strategisch wichtigste Feature der Plattform. Er wird zukünftig als externe Bank-API vermarktet. Jede Implementierungsentscheidung muss diese Frage bestehen: "Würde eine Bank oder ein Gericht diesem Datenpunkt vertrauen?"


NICHT-VERHANDELBARE REGELN

  1. Score NIEMALS synchron im API-Request berechnen — immer async via Queue oder Background Job
  2. Score NIEMALS manuell überschreibbar — er ist berechnet, nicht gesetzt. Kein Admin-Override
  3. Jede Score-Berechnung wird in property_score_history gespeichert — kein Silent Update
  4. Fehlende Daten senken NICHT den Score, sondern die Konfidenz — Score und Konfidenz sind zwei unabhängige Dimensionen
  5. i18n auf allen neuen Komponenten und API-Responses — keine hartcodierten deutschen Strings
  6. Keine Emojis — nur Lucide React Icons
  7. Dark Theme durchgehend: #0f172a / #1e293b / #334155 / #6366f1
  8. TypeScript strict — keine any-Types
  9. Nach Fertigstellung: CLAUDE.md und MEMORY.md aktualisieren

SCHRITT 1 — PRISMA SCHEMA ERWEITERUNGEN

Füge folgende neue Modelle und Felder zur schema.prisma hinzu:

Neues Modell: PropertyScore

model PropertyScore {
  id                  String   @id @default(cuid())
  tenantId            String
  propertyId          String   @unique

  // Gesamt-Score
  totalScore          Float
  scoreGrade          String   // 'A+' | 'A' | 'B' | 'C' | 'D' | 'F'
  percentile          Float?

  // Kategorie-Scores (je 0-100)
  substanzScore       Float
  technologieScore    Float
  ertragScore         Float
  complianceScore     Float
  instandhaltungScore Float

  // Potenzial
  potentialScore      Float
  potentialGap        Float

  // Trend
  scoreDelta30d       Float?
  scoreDelta90d       Float?
  trendDirection      String   // 'improving' | 'stable' | 'declining'

  // Risiko
  riskFlags           Json     // [{flag, severity, category, detectedAt, titleKey, descriptionKey}]
  criticalIssues      Int      @default(0)

  // Metadaten
  calculatedAt        DateTime @default(now())
  dataCompleteness    Float
  confidenceScore     Float
  version             Int      @default(1)

  tenant              Tenant   @relation(fields: [tenantId], references: [id])
  property            Property @relation(fields: [propertyId], references: [id])
  history             PropertyScoreHistory[]
  factors             ScoreFactor[]

  @@index([tenantId, totalScore(sort: Desc)])
  @@index([tenantId, scoreGrade])
  @@index([propertyId])
}

Neues Modell: PropertyScoreHistory

model PropertyScoreHistory {
  id                  String   @id @default(cuid())
  tenantId            String
  propertyId          String
  scoreId             String

  totalScore          Float
  scoreGrade          String
  substanzScore       Float
  technologieScore    Float
  ertragScore         Float
  complianceScore     Float
  instandhaltungScore Float
  potentialScore      Float
  confidenceScore     Float

  changedFactors      Json
  changeReason        String   // 'scheduled_recalc' | 'data_update' | 'manual_trigger'
  triggeredBy         String?

  // Hash-Chaining (Manipulationsschutz)
  scoreHash           String
  previousHash        String?
  chainValid          Boolean  @default(true)
  chainValidatedAt    DateTime?
  externalAnchorTx    String?
  externalAnchorAt    DateTime?

  calculatedAt        DateTime @default(now())

  score               PropertyScore @relation(fields: [scoreId], references: [id])

  @@index([propertyId, calculatedAt(sort: Desc)])
  @@index([tenantId, calculatedAt(sort: Desc)])
  @@index([scoreHash])
}

Neues Modell: ScoreFactor

model ScoreFactor {
  id              String   @id @default(cuid())
  scoreId         String
  propertyId      String
  category        String   // 'substanz' | 'technologie' | 'ertrag' | 'compliance' | 'instandhaltung'
  factorKey       String
  factorLabelKey  String   // i18n-Key
  rawValue        Float
  normalizedValue Float
  weight          Float
  contribution    Float
  dataSource      String
  dataVersion     String   // 'v1_manual' | 'v2_auto' | 'v3_iot'
  dataFreshness   String   // 'current' | 'stale' | 'missing'
  measuredAt      DateTime

  score           PropertyScore @relation(fields: [scoreId], references: [id])

  @@index([scoreId, category])
  @@index([propertyId, factorKey])
}

Neues Modell: EnergyReading

model EnergyReading {
  id            String   @id @default(cuid())
  tenantId      String
  propertyId    String
  unitId        String?
  period        String   // Format: 'YYYY-MM'
  heatKwh       Float?
  coldWaterM3   Float?
  hotWaterM3    Float?
  electricityKwh Float?
  source        String   // 'techem' | 'ista' | 'brunata' | 'manual'
  createdAt     DateTime @default(now())

  property      Property @relation(fields: [propertyId], references: [id])

  @@index([propertyId, period])
  @@unique([propertyId, unitId, period, source])
}

Neues Modell: WorkerRating

model WorkerRating {
  id                String   @id @default(cuid())
  tenantId          String
  ticketId          String   @unique
  serviceProviderId String
  propertyId        String
  stars             Int      // 1-5
  comment           String?
  reworkRequired    Boolean  @default(false)
  quotedAmount      Float?
  finalAmount       Float?
  sentimentScore    Float?   // 0-100, berechnet via NLP
  ratedAt           DateTime @default(now())
  ratedByUserId     String

  ticket            Ticket          @relation(fields: [ticketId], references: [id])
  serviceProvider   ServiceProvider @relation(fields: [serviceProviderId], references: [id])

  @@index([propertyId])
  @@index([serviceProviderId])
}

Neues Modell: GlobalChainAnchor

model GlobalChainAnchor {
  id          String   @id @default(cuid())
  anchorDate  DateTime @unique
  chainTip    String
  totalScores Int
  externalRef String?
  createdAt   DateTime @default(now())
}

Erweiterungen bestehender Modelle

Property — neue Felder:

// Substanz
constructionYear       Int?
lastRenovationYear     Int?
roofRenovationYear     Int?
windowsRenovationYear  Int?
heatingYear            Int?

// Technologie
energyCertificate      String?  // 'A+' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G'
heatingType            String?  // 'gas' | 'oil' | 'heat_pump' | 'district' | 'pellet' | 'electric'
hasFiber               Boolean  @default(false)
hasElevator            Boolean  @default(false)
hasSmartMeter          Boolean  @default(false)
hasEvCharging          Boolean  @default(false)
isBarrierFree          String?  // 'full' | 'partial' | 'none'

// Score-Relation
score                  PropertyScore?

Ticket — neue Felder:

workerRating    WorkerRating?
sentimentScore  Float?   // 0-100, NLP-Analyse des Ticket-Textes

ServiceProvider — neue Felder:

workerRatings   WorkerRating[]
avgRating       Float?   // denormalisiert, täglich aktualisiert

Nach Schema-Änderungen: npx prisma migrate dev --name xolib-score-v1-v2 ausführen.


SCHRITT 2 — SCORE-BERECHNUNGS-SERVICE

Erstelle src/lib/score/ als neues Verzeichnis mit folgenden Dateien:

src/lib/score/types.ts

Definiere alle TypeScript-Interfaces: ScoreData, ScoreFactorData, ScoreCalculationResult, RiskFlag, ScoreGrade. Keine any-Types.

src/lib/score/integrity.ts

Implementiere kryptografisches Hash-Chaining:

import crypto from 'crypto';

// Deterministisches JSON — Feldreihenfolge ist UNVERÄNDERLICH
export function calculateScoreHash(score: ScoreData, previousHash: string | null): string {
  const payload = JSON.stringify({
    propertyId:           score.propertyId,
    totalScore:           score.totalScore,
    substanzScore:        score.substanzScore,
    technologieScore:     score.technologieScore,
    ertragScore:          score.ertragScore,
    complianceScore:      score.complianceScore,
    instandhaltungScore:  score.instandhaltungScore,
    calculatedAt:         score.calculatedAt.toISOString(),
    previousHash:         previousHash ?? 'GENESIS',
  });
  return crypto.createHash('sha256').update(payload, 'utf8').digest('hex');
}

export async function verifyPropertyChain(propertyId: string): Promise<{
  valid: boolean;
  brokenAt?: string;
  totalEntries: number;
}>

export async function verifyAllChains(): Promise<{
  valid: boolean;
  brokenChains: string[];
  totalProperties: number;
}>

src/lib/score/normalization.ts

Alle Normalisierungsfunktionen (roher Wert → 0-100):

ERTRAG-Faktoren: - normalizeBelegungsquote(rate: number): number - 100% = 100, 95% = 85, 90% = 65, 85% = 40, <80% = 0 - normalizeZahlungsverspaetung(avgDays: number): number - 0 Tage = 100, <3 = 85, <7 = 65, <14 = 35, >14 = 0 - normalizeLeerstandstage(days: number): number - 0 = 100, <7 = 85, <30 = 60, <90 = 30, >90 = 0 - normalizeZahlungstrend(trendPercent: number): number - >20% Verbesserung = 100, stabil = 60, Verschlechterung = 20 - normalizeChronischeZahler(count: number, totalUnits: number): number - 0 = 100, 1 = 70, 2 = 40, 3+ = 0

INSTANDHALTUNG-Faktoren: - normalizeTicketLoesungsrate(rate: number): number - >95% = 100, >85% = 80, >70% = 55, >50% = 25, <50% = 0 - normalizeLoesungszeit(avgDays: number): number - <1 = 100, <3 = 85, <7 = 65, <14 = 35, >14 = 0 - normalizeOffeneKritischeTickets(count: number): number - 0 = 100, 1 = 60, 2 = 30, 3+ = 0. Abzug: -10 je Ticket >30 Tage offen - normalizePraevention(praeventivRate: number): number - >30% = 100, >20% = 75, >10% = 50, <10% = 25, 0% = 0 - normalizeHandwerkerBewertung(avgStars: number, count: number): number - Ø5,0 = 100, Ø4,5 = 85, Ø4,0 = 70, Ø3,5 = 50, <3,0 = 0. Unter 5 Bewertungen: 50 (neutral) - normalizeNachbearbeitungsquote(rate: number): number - 0% = 100, <5% = 80, <10% = 55, <20% = 25, >20% = 0 - normalizeSentiment(avgSentiment: number): number - Direkte Übernahme des NLP-Scores (0-100)

COMPLIANCE-Faktoren: - normalizeFreistellungsbescheinigungen(expired: number, total: number): number - 0 abgelaufen = 100, 1 = 60, 2 = 20, fehlend bei aktivem Dienstleister = 0 - normalizeVersicherungsstatus(hasAll: boolean, hasPartial: boolean): number - Alle vorhanden = 100, teilweise = 50, keine = 0 - normalizeAbrechnungspuenktlichkeit(dayslate: number): number - Fristgerecht = 100, <30 Tage = 60, <90 Tage = 20, >90 Tage = 0 - normalizeDokumentVollstaendigkeit(vorhandenCount: number, pflichtCount: number): number - 100% = 100, <90% = 70, <70% = 40, <50% = 10

SUBSTANZ-Faktoren: - normalizeGebaeudalter(constructionYear: number, lastRenovationYear?: number): number - normalizeSanierung(lastRenovationYear?: number): number - <5 J. = 100, <10 = 85, <20 = 60, <30 = 35, >30 oder unbekannt = 15 - normalizeDach(roofRenovationYear?: number): number - normalizeFenster(windowsRenovationYear?: number): number - normalizeHeizungsalter(heatingYear?: number): number

TECHNOLOGIE-Faktoren: - normalizeEnergiezertifikat(grade?: string): number - A+ = 100, A = 90, B = 75, C = 55, D = 35, E = 20, F = 10, G = 0, fehlt = 25 - normalizeHeizungstyp(type?: string): number - heat_pump = 100, district = 85, pellet = 75, gas (neu <10J.) = 55, gas (alt) = 35, oil = 10 - normalizeSmartInfrastructure(hasFiber: boolean, hasSmartMeter: boolean, hasElevator: boolean, hasEvCharging: boolean, floors: number): number - Je Feature +25 Pkt. Aufzug zählt nur bei >3 Etagen. Max 100. - normalizeEnergieVerbrauch(kwhPerSqm?: number): number - <50 = 100, <80 = 85, <120 = 65, <160 = 40, >160 = 15, fehlt = 50 (neutral)

src/lib/score/calculator.ts

Hauptberechnungs-Service:

export async function calculateScore(propertyId: string): Promise<ScoreCalculationResult> {
  // 1. Alle relevanten Daten laden
  // 2. Jeden Faktor berechnen und normalisieren
  // 3. Kategorie-Scores als gewichtete Summe
  // 4. Gesamt-Score berechnen: substanz*0.25 + technologie*0.25 + ertrag*0.25 + compliance*0.15 + instandhaltung*0.15
  // 5. Score-Klasse bestimmen: A+(90-100) A(80-89) B(65-79) C(50-64) D(35-49) F(0-34)
  // 6. Konfidenz berechnen: Σ(Faktor-Gewicht × Datenvollständigkeit)
  // 7. Potenzial-Score berechnen: Score bei vollständigen Daten
  // 8. Risiko-Flags identifizieren
  // 9. Trend berechnen (Vergleich mit vorherigem Score)
  // 10. ScoreFactors für jeden Datenpunkt erstellen
}

export async function upsertPropertyScore(propertyId: string, result: ScoreCalculationResult): Promise<void> {
  // Transaktion:
  // 1. Vorherigen Score-Hash laden
  // 2. Neuen Hash berechnen (calculateScoreHash)
  // 3. PropertyScore upserten
  // 4. PropertyScoreHistory-Eintrag erstellen mit Hash
  // 5. ScoreFactors erstellen
}

Risiko-Flag Logik — folgende Flags implementieren: - heating_age_critical: Heizung >20 Jahre → severity: 'high' - long_term_vacancy: Leerstand >30 Tage → severity: 'high' - open_critical_tickets: Kritische Tickets >7 Tage offen → severity: 'critical' - payment_default_pattern: >2 chronische Zahler → severity: 'high' - tax_exemption_expired: Freistellungsbescheinigung abgelaufen → severity: 'critical' - missing_insurance: Versicherung fehlt → severity: 'critical' - energy_cert_expired: Energieausweis abgelaufen → severity: 'high' - escalating_communication: Sentiment-Score <30 → severity: 'medium' - high_bk_deviation: BK-Abweichung >200 EUR/Einheit → severity: 'medium' - low_worker_rating: Handwerker-Ø <3 Sterne → severity: 'medium'

src/lib/score/sentiment.ts

Keyword-basierte Sentiment-Klassifikation (V1 — kein API-Call):

const NEGATIVE_KEYWORDS = ['mangel', 'defekt', 'kaputt', 'problem', 'fehler', 'nicht funktioniert', 'schimmel', 'feuchtigkeit', 'kalt', 'dunkel'];
const ESCALATION_KEYWORDS = ['anwalt', 'mietminderung', 'klage', 'fristlos', 'verbraucherzentrale', 'gericht', 'rechtlich', 'schadensersatz', 'untragbar'];
const POSITIVE_KEYWORDS = ['danke', 'erledigt', 'super', 'schnell', 'freundlich', 'zufrieden', 'top', 'perfekt'];

export function analyzeSentiment(text: string): number {
  // 0 = eskalierend, 100 = konstruktiv
  // Eskalations-Keywords: direkt 0-10
  // Negative Keywords: 30-50
  // Neutral: 60-70
  // Positive Keywords: 80-100
}

SCHRITT 3 — BACKGROUND JOBS

src/jobs/scoreRecalculation.ts

Nächtlicher Job (02:00 Uhr):

export async function runNightlyScoreRecalculation(): Promise<void> {
  // Alle Properties aktiver Mandanten laden
  // Für jede Property: calculateScore() + upsertPropertyScore()
  // Fehler je Property loggen ohne andere zu blockieren
  // Am Ende: GlobalChainAnchor für den Tag erstellen
  // Job-Dauer und Ergebnisse in strukturiertem Log ausgeben
}

Wichtig: Diesen Job in die bestehende Cron-Infrastruktur einbinden. Prüfe wie andere Jobs registriert sind und folge demselben Muster.

Trigger-basierte Neuberechnung

Füge in folgende bestehende API-Routes einen asynchronen Score-Neuberechnungs-Trigger ein (NICHT await — feuern und vergessen):

  • POST /api/v1/tickets → nach Erstellung
  • PATCH /api/v1/tickets/:id → bei Status-Änderung zu RESOLVED oder CLOSED
  • POST /api/v1/payments → nach Zahlungseingang
  • PATCH /api/v1/payments/:id → bei Markierung als überfällig
  • POST /api/v1/tenancies → bei neuem Mietverhältnis
  • PATCH /api/v1/tenancies/:id → bei Beendigung
  • PATCH /api/v1/properties/:id → bei Änderung der Score-relevanten Felder

Trigger-Funktion:

async function triggerScoreRecalculation(propertyId: string, reason: string): Promise<void> {
  // Async, nie awaiten im Request-Pfad
  // calculateScore + upsertPropertyScore
  // Bei Fehler: nur loggen, nie werfen
}


SCHRITT 4 — API ENDPUNKTE

Erstelle folgende neue API-Routes unter /api/v1/:

GET /api/v1/properties/[id]/score

Response:

{
  score: PropertyScore,
  factors: ScoreFactor[],
  riskFlags: RiskFlag[],
  trend: { direction, delta30d, delta90d },
  confidence: number,
  dataCompleteness: number,
  potentialScore: number,
  potentialGap: number,
  integrityStatus: { valid: boolean, lastVerified: string, hash: string }
}

GET /api/v1/properties/[id]/score/history

Query-Params: ?from=&to=&limit=50 Response: PropertyScoreHistory[] (für Chart-Darstellung)

POST /api/v1/properties/[id]/score/recalculate

Nur OWNER_ADMIN. Asynchron. Response: { jobId: string, status: 'queued' }

GET /api/v1/score/portfolio

Response: Alle Properties des Mandanten mit Score, Klasse, Trend, Top-Risiko. Sortierbar.

GET /api/v1/score/portfolio/summary

Response: Portfolio-Kennzahlen — Ø Score, Verteilung A/B/C/D/F, Anzahl kritische Flags.

GET /api/v1/properties/[id]/score/verify

Response: Chain-Verifikation für diese Property — valid boolean, Anzahl Einträge, letzter Hash.

Alle Endpunkte: Tenant-Isolation via tenantId, Authentifizierung via bestehende Auth-Middleware.


SCHRITT 5 — WORKER RATING FLOW

API: POST /api/v1/tickets/[id]/rating

Nur aufrufbar wenn Ticket-Status RESOLVED oder CLOSED. Body: { stars, comment?, reworkRequired, quotedAmount?, finalAmount? }.

Nach Speicherung: 1. Sentiment auf comment berechnen (analyzeSentiment) 2. avgRating auf ServiceProvider denormalisiert aktualisieren 3. Score-Neuberechnung für Property triggern

UI-Hook

In der bestehenden Ticket-Detail-Ansicht: Nach Status-Wechsel zu RESOLVED → Rating-Drawer automatisch öffnen. Drawer zeigt: Sterne-Auswahl (1-5), Kommentarfeld, Toggle "Nacharbeit erforderlich", optionale Beträge. Submit-Button: "Bewertung speichern". Schließen ohne Rating: möglich, aber Reminder-Badge auf Ticket-Karte.


SCHRITT 6 — ENERGY READING ENDPUNKTE

POST /api/v1/properties/[id]/energy-readings

Body: { period, heatKwh?, coldWaterM3?, hotWaterM3?, electricityKwh?, source } Für manuelle Eingabe (V1). Upsert-Logik: gleiche Property + Period + Source wird überschrieben.

GET /api/v1/properties/[id]/energy-readings

Query: ?from=&to= — Zeitreihe für Charts.

Score-Trigger nach jeder neuen Energiemessung.


SCHRITT 7 — UI KOMPONENTEN

Erstelle unter src/components/score/ folgende Komponenten. Alle TypeScript, alle i18n, alle Dark Theme, keine Emojis.

ScoreHero.tsx

Volle Seitenbreite, sichtbar ohne Scrollen. Drei Bereiche:

Links: - Große Score-Zahl (Font-Size: sehr groß, Farbe nach Klasse: Grün/Gelb/Rot) - Klasse-Badge (z.B. "B — Gut") - Trend-Indikator: Pfeil-Icon (TrendingUp/TrendingDown/Minus von Lucide) + Delta "+3.2 in 30 Tagen"

Mitte: - 5 Kategorie-Circles (KpiDonutGrid-Stil): Name + Score + Klasse - Farbe: Grün (≥75) / Gelb (50-74) / Rot (<50) - Klick auf Circle: öffnet ScoreFactorDrawer für diese Kategorie

Rechts: - Konfidenz-Anzeige: Prozent + Status-Badge ("Verifiziert" grün / "Unvollständig" gelb / "Schätzung" rot) - Berechnungsdatum - Integritäts-Badge: Shield-Icon (Lucide) + "Hash-Kette gültig"

Unten (volle Breite): - Potenzial-Badge: "Maximal erreichbar: 91 (+13 Pkt)" - Benchmark-Badge: "Besser als 62% ähnlicher Objekte" - Risiko-Badge: "2 aktive Risikoflags" (rot wenn >0 kritisch) - KI-Insight: Automatisch generierter Satz (von /api/v1/properties/[id]/score, Feld: aiInsight)

ScoreTrendChart.tsx

Linechart (Recharts) über 12 Monate: - Gesamt-Score als Hauptlinie - Toggle: je Kategorie als gestrichelte Linie ein-/ausblendbar - Ereignis-Marker: vertikale Linien bei signifikanten Score-Änderungen, Tooltip zeigt Grund - Benchmark-Linie: Mandanten-Durchschnitt gestrichelt grau - Zeitraum-Selector: 3M / 6M / 12M / Gesamt - Export-Button: CSV-Download

ScoreCategoryGrid.tsx

5 Karten in Grid-Layout:

Je Karte: - Header: Kategorie-Name + Score + Gewicht + Klasse - Faktor-Liste: je Faktor eine Zeile mit Name, gemessenem Wert, normalisierten Score-Balken (0-100), Datenquelle, Messdatum - Datenstatus-Icons: CheckCircle (aktuell) / Clock (veraltet) / XCircle (fehlend) / Edit (manuell) — alle Lucide - Fehlende-Daten-Hinweis: "Energieverbrauch fehlt — bei Eingabe +6 Pkt erreichbar" mit Button "Jetzt ergänzen" - KI-Empfehlung: eine konkrete Handlungsempfehlung je Kategorie - Klick auf Karte: öffnet ScoreFactorDrawer

ScoreFactorDrawer.tsx

Drawer von rechts, 480px: - Header: Kategorie + aktueller Score - Faktor-Tabelle: vollständige Details aller Faktoren - Manuelle Felder: Inline-Eingabe direkt im Drawer mit Live-Vorschau ("Score würde auf XX steigen") - Sparklines: Mini-Trendlinie je Faktor (letzte 6 Monate, Recharts) - Quellen-Transparenz: exakte Datenbasis ("47 Zahlungs-Einträge, 01.03.2025 — 01.03.2026") - Hash-Badge: "Score-Integrität verifiziert: [Datum]"

RiskFlagPanel.tsx

Unterhalb ScoreCategoryGrid: - Aktive Flags sortiert nach Schwere: KRITISCH (rot) → HOCH (orange) → MITTEL (gelb) - Je Flag: Titel, Beschreibung, betroffene Kategorie, erkannt seit, empfohlene Maßnahme, geschätzte Score-Verbesserung - CTA-Buttons je Flag-Typ: "Ticket erstellen" / "Handwerker beauftragen" / "Dokument hochladen" - Behobene Flags (letzte 30 Tage): graue Liste mit "Score-Verbesserung durch Behebung: +X Pkt"

ScoreIntegrityPanel.tsx

Ausklappbarer Abschnitt (standardmäßig zugeklappt, ChevronDown-Icon): - Score-ID: kopierbar - SHA-256 Hash: vollständig angezeigt + Copy-Button - Chain-Status: Anzahl Versionen, letzter Verifikations-Timestamp, Status - Externe Verankerung: Link wenn vorhanden - Verifikations-Anleitung: ausklappbar, Schritt-für-Schritt auch für Nicht-Techniker - Download: Score-Zertifikat PDF (GET /api/v1/properties/[id]/score/certificate) - API-Endpunkt-Anzeige für Bank-Abfrage

PortfolioScoreMatrix.tsx

Für Haupt-Dashboard: - Ersetzt oder ergänzt bestehendes Portfolio-Widget - Tabelle: alle Objekte mit Score, Klasse, Trend-Pfeil, Konfidenz, Top-Risiko - Sortierbar nach allen Spalten - Filter: Klasse-Filter, Trend-Filter, Risiko-Filter - Portfolio-Kennzahlen oben: Ø Score, Verteilung als Stacked Bar - Export: Portfolio-Score-Report PDF


SCHRITT 8 — SCORE-SEITE

Erstelle neue Route: /properties/[id]/score/page.tsx

Layout von oben nach unten: 1. ScoreHero (volle Breite) 2. ScoreTrendChart (volle Breite) 3. ScoreCategoryGrid (volle Breite, 2-3 Karten pro Zeile) 4. RiskFlagPanel (volle Breite) 5. ScoreIntegrityPanel (volle Breite, zugeklappt)

Navigation: Score-Tab oder Score-Badge auf Property-Detail-Seite hinzufügen. Score-Badge auch auf Property-Karte in der Listenansicht (kleine Zahl + Klasse + Trend-Pfeil).


SCHRITT 9 — I18N

Füge alle neuen Strings zur i18n-Konfiguration hinzu (alle 8 Sprachen). Mindestens:

score.title, score.grade.aplus, score.grade.a, score.grade.b, score.grade.c, score.grade.d, score.grade.f,
score.category.substanz, score.category.technologie, score.category.ertrag, score.category.compliance, score.category.instandhaltung,
score.confidence.verified, score.confidence.incomplete, score.confidence.estimate,
score.trend.improving, score.trend.stable, score.trend.declining,
score.integrity.valid, score.integrity.checking, score.integrity.title,
score.risk.critical, score.risk.high, score.risk.medium,
score.factor.missing, score.factor.stale, score.factor.manual, score.factor.current,
score.potential.label, score.benchmark.label,
score.rating.title, score.rating.stars, score.rating.rework,
score.energy.period, score.energy.heat, score.energy.electricity

SCHRITT 10 — TESTS

Schreibe Tests für: - normalization.ts: je Normalisierungsfunktion mindestens 5 Testfälle (Grenzwerte + Mittelwerte) - integrity.ts: Hash-Berechnung deterministisch, Chain-Verifikation funktioniert, Manipulation wird erkannt - calculator.ts: Vollständige Score-Berechnung mit Mock-Daten — Ergebnis in erwartetem Bereich - API-Routes: Score-Endpunkt gibt korrektes Format zurück, Tenant-Isolation funktioniert


ABSCHLUSS

Nach vollständiger Implementierung:

  1. npx prisma migrate dev ausführen und prüfen ob alle Migrationen sauber durchlaufen
  2. npm run build — keine TypeScript-Fehler
  3. npm run test — alle neuen Tests grün
  4. CLAUDE.md aktualisieren: neue Modelle, neue Endpunkte, neue Komponenten dokumentieren
  5. MEMORY.md aktualisieren: Score-Feature als implementiert markieren, offene TODOs (Techem-Integration, Energie-API, V2-Sentiment via GPT) dokumentieren
  6. Notion-Session-Eintrag erstellen mit: was wurde implementiert, welche Entscheidungen wurden getroffen, was ist noch offen

PRIORITÄTSREIHENFOLGE falls Zeit fehlt

Wenn du nicht alles in einem Durchgang schaffst, priorisiere so:

Muss in diesem Sprint: - Prisma-Migrationen (alle Modelle) - calculator.ts + normalization.ts (Ertrag + Instandhaltung zuerst — vollständig aus bestehenden Daten) - Nächtlicher BG-Job - Hash-Chaining (integrity.ts) - GET /api/v1/properties/[id]/score - ScoreHero + ScoreCategoryGrid (minimale UI zum Anzeigen)

Kann in nächstem Sprint: - Worker Rating Flow - Energy Reading Endpunkte - ScoreTrendChart - ScoreFactorDrawer (vollständig) - ScoreIntegrityPanel - PortfolioScoreMatrix - Score-Zertifikat PDF


Xolib Score — Der Bloomberg des deutschen Immobilienmarkts. Jede Zeile Code bringt uns näher.