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¶
- Score NIEMALS synchron im API-Request berechnen — immer async via Queue oder Background Job
- Score NIEMALS manuell überschreibbar — er ist berechnet, nicht gesetzt. Kein Admin-Override
- Jede Score-Berechnung wird in property_score_history gespeichert — kein Silent Update
- Fehlende Daten senken NICHT den Score, sondern die Konfidenz — Score und Konfidenz sind zwei unabhängige Dimensionen
- i18n auf allen neuen Komponenten und API-Responses — keine hartcodierten deutschen Strings
- Keine Emojis — nur Lucide React Icons
- Dark Theme durchgehend: #0f172a / #1e293b / #334155 / #6366f1
- TypeScript strict — keine any-Types
- 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 ErstellungPATCH /api/v1/tickets/:id→ bei Status-Änderung zu RESOLVED oder CLOSEDPOST /api/v1/payments→ nach ZahlungseingangPATCH /api/v1/payments/:id→ bei Markierung als überfälligPOST /api/v1/tenancies→ bei neuem MietverhältnisPATCH /api/v1/tenancies/:id→ bei BeendigungPATCH /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:
npx prisma migrate devausführen und prüfen ob alle Migrationen sauber durchlaufennpm run build— keine TypeScript-Fehlernpm run test— alle neuen Tests grün- CLAUDE.md aktualisieren: neue Modelle, neue Endpunkte, neue Komponenten dokumentieren
- MEMORY.md aktualisieren: Score-Feature als implementiert markieren, offene TODOs (Techem-Integration, Energie-API, V2-Sentiment via GPT) dokumentieren
- 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.