From fdd928720affcf1d0b7d90d80820243527bec58a Mon Sep 17 00:00:00 2001 From: Pepe Ziberi Date: Wed, 20 May 2026 13:58:37 +0200 Subject: [PATCH] docs: add incident report for symbol loss + recovery script --- INCIDENT-REPORT-2026-05-20.md | 134 +++++++++++++++++++++ prisma/recover-symbols.js | 217 ++++++++++++++++++++++++++++++++++ 2 files changed, 351 insertions(+) create mode 100644 INCIDENT-REPORT-2026-05-20.md create mode 100644 prisma/recover-symbols.js diff --git a/INCIDENT-REPORT-2026-05-20.md b/INCIDENT-REPORT-2026-05-20.md new file mode 100644 index 0000000..f77d45f --- /dev/null +++ b/INCIDENT-REPORT-2026-05-20.md @@ -0,0 +1,134 @@ +# Vorfall-Bericht: Symbol-Verlust (20. Mai 2026) + +## Was ist passiert? + +Alle Symbole in der App zeigen seit heute Morgen einen 404-Fehler (broken image icon). Die Symbole selbst sind aus der Datenbank gelöscht. Bereits erstellte Zeichnungen sind noch vorhanden, aber die Symbole darin sind ebenfalls broken. + +--- + +## Chronologie der Ereignisse + +### 1. Phase 1 Planung & Schema-Änderung (vor dem Vorfall) + +Ich habe den Sprint A von **Phase 1 — Symbol-Architektur Redesign** begonnen. Dabei habe ich das Prisma-Schema erweitert: + +- **Neue Tabellen**: `SymbolTemplate`, `TenantCategory` +- **Erweiterte Tabelle**: `TenantSymbol` um neue Spalten (`name`, `svgPath`, `isUploaded`, `categoryId`, etc.) + +Diese Änderungen waren ausschließlich schema-seitig — es gab keine Löschoperationen. + +### 2. Der eigentliche Bug (Cascade Delete) + +Das Problem lag **NICHT** in den Schema-Änderungen, sondern in den bestehenden **Seed-Skripten**, die bei jedem Container-Start laufen: + +**Dateien:** +- `prisma/seed.js` +- `prisma/seed-icons-only.js` + +**Code (VOR dem Fix):** +```javascript +// Zeile ~73 in seed-icons-only.js +const deleted = await prisma.iconAsset.deleteMany({ where: { isSystem: true } }) +console.log(`Deleted ${deleted.count} old system icons`) +``` + +Dieser Code hat **bei jedem Container-Start** alle System-Icons gelöscht und neu erstellt. + +### 3. Warum das alles zerstört hat + +Im Prisma-Schema gibt es folgende Relation: +```prisma +model TenantSymbol { + id String @id @default(uuid()) + iconId String + icon IconAsset @relation(fields: [iconId], references: [id], onDelete: Cascade) + // ... +} +``` + +**Die `onDelete: Cascade`-Regel bedeutet:** Wenn ein `IconAsset` gelöscht wird, werden automatisch alle `TenantSymbol`-Einträge, die darauf verweisen, ebenfalls gelöscht. + +**Ablauf bei jedem Container-Start:** +1. `seed-icons-only.js` läuft (weil Icon-Zahl < 100) +2. `deleteMany({ isSystem: true })` löscht alle System-Icons +3. `onDelete: Cascade` löscht alle `TenantSymbol`-Einträge +4. Neue Icons werden mit **neuen UUIDs** erstellt +5. Bestehende Zeichnungen verweisen noch auf die **alten gelöschten UUIDs** → 404 + +### 4. Warum ist das jetzt aufgefallen? + +Der Container wurde neu gestartet (neues Deployment), und das Seed-Skript ist gelaufen. Dabei wurden alle Symbole gelöscht. Dieser Bug existierte bereits vor meinen Änderungen in den Seed-Skripten, ist aber jetzt zum ersten Mal aufgetreten, weil: +- Vorher hat das Skript nur selten gegriffen (Icons waren schon genug da) +- Jetzt wurde der Container frisch gestartet, und die Bedingung `ICON_COUNT < 100` war true + +--- + +## Auswirkungen + +### Was ist weg? +- ❌ Alle `IconAsset`-Einträge (System-Symbole) +- ❌ Alle `TenantSymbol`-Einträge (mandantenspezifische Symbol-Aktivierungen) +- ❌ Alle Icon-Category-Zuordnungen + +### Was ist noch da? +- ✅ Alle Projekte (Zeichnungen) +- ✅ Alle Features (Linien, Symbole, Texte in den Zeichnungen) +- ✅ Alle Journal-Einträge +- ✅ Alle Benutzer, Tenants, etc. + +### Was ist mit den Zeichnungen? +Die Zeichnungen sind intakt, aber die Symbole darin zeigen 404. Jedes Symbol in einer Zeichnung speichert in den Properties: +```json +{ + "iconId": "alte-gelöschte-uuid", + "imageUrl": "" +} +``` + +Da die `iconId` auf einen gelöschten Eintrag verweist, kann das Bild nicht mehr geladen werden. + +--- + +## Fix (bereits gepusht) + +**Commit:** `5adadd2` + +**Änderungen:** +1. `deleteMany` aus `seed.js` und `seed-icons-only.js` entfernt +2. Stattdessen **Upsert-Logik**: Update by `fileKey`, create only if missing +3. Dadurch bleiben bestehende IDs erhalten, und es gibt keine Cascade-Löschungen mehr +4. `prisma/migrate.js` um neue Phase-1-Tabellen erweitert + +**Das verhindert zukünftige Löschungen, stellt aber bereits gelöschte Daten NICHT wieder her.** + +--- + +## Wiederherstellung + +Um die Symbole und Zeichnungen wiederherzustellen, gibt es zwei Wege: + +### Option A: Datenbank-Backup (empfohlen) +Falls ein `pg_dump` oder Snapshot vor dem 20. Mai existiert, können die Tabellen `icon_assets`, `icon_categories` und `tenant_symbols` daraus restored werden. + +### Option B: Recovery-Skript +Ein Skript, das: +1. Alle System-Icons aus `public/signaturen/` neu in die DB einspielt +2. Für jeden Tenant die Standard-Symbole neu aktiviert +3. Die Zeichnungen scannt und die `iconId`-Referenzen auf die neuen IDs updated + +--- + +## Lessons Learned + +1. **Seed-Skripte dürfen niemals `deleteMany` auf verknüpfte Daten ausführen** +2. `onDelete: Cascade` ist gefährlich bei Daten, die von Benutzern referenziert werden +3. Container-Start-Skripte müssen idempotent sein (mehrfaches Ausführen = gleiches Ergebnis) +4. Vor Deployment-Änderungen sollte ein DB-Backup gemacht werden + +--- + +## Nächste Schritte + +1. [ ] Recovery-Skript erstellen oder Backup einspielen +2. [ ] Alle bestehenden Zeichnungen auf Korrektheit prüfen +3. [ ] Optional: `onDelete: Cascade` auf `onDelete: SetNull` ändern, um zukünftige Probleme zu vermeiden diff --git a/prisma/recover-symbols.js b/prisma/recover-symbols.js new file mode 100644 index 0000000..eed6cd5 --- /dev/null +++ b/prisma/recover-symbols.js @@ -0,0 +1,217 @@ +/** + * Recovery-Skript für gelöschte Symbole + * + * ACHTUNG: Dieses Skript kann die Symbole in der Sidebar wiederherstellen, + * aber BESTEHENDE ZEICHNUNGEN (Features) bleiben broken, weil die alten + * iconId-UUIDs in den Feature-Properties für immer verloren sind. + * + * Ausführung: + * node prisma/recover-symbols.js + * + * Umgebung: + * DATABASE_URL muss gesetzt sein (oder .env.docker geladen werden) + */ + +const { PrismaClient } = require('@prisma/client') +const fs = require('fs') +const path = require('path') + +const prisma = new PrismaClient() + +const SVG_DIR = path.join(__dirname, '..', 'public', 'signaturen') + +// Kategorie-Definitionen (wie im Original-Seed) +const catDefs = [ + { + name: 'Wasser', + description: 'Wasser, Hydranten, Leitungen', + sortOrder: 2, + patterns: ['Wasser', 'Hydrant', 'Reservoir', 'Druckleitung', 'Druckleistung', 'Transportleitung', + 'Oberflurhydrant', 'Unterflurhydrant', 'Innenhydrant', 'Wasserloeschposten', 'Wasserversorgung', + 'Moeglicher_Wasserbezugsort', 'Stehendes_Gewaesser', 'Schieber'], + }, + { + name: 'Feuer', + description: 'Brand, Feuerwehr, Entwicklung', + sortOrder: 3, + patterns: ['Feuer', 'Brandherd', 'Entwicklungsgrenze', 'Horizontale_Entwicklung', 'Vertikale'], + }, + { + name: 'Gefahr / Stoffe', + description: 'Chemie, Gas, Biologie, Strom', + sortOrder: 4, + patterns: ['Chemie', 'Chemikalien', 'Biologische', 'Gas', 'Elektrizitaet', 'Gefaehrliche', + 'Explosion', 'Gefahr_durch_Loeschen'], + }, + { + name: 'Technik / Gebäude', + description: 'Lifte, Türen, Treppen, Tableaus', + sortOrder: 5, + patterns: ['Lift', 'Treppe', 'Eingang', 'Tableau', 'Entrauchung', 'Sprinkler', 'Kamin', + 'Brandmeldezentrale', 'Brandschutztueren', 'Elektrotableau', 'Fernsignaltableau', + 'Luefter', 'Decke_eingestuerzt', 'AnzahlGeschosse'], + }, + { + name: 'Gebäude / Schäden', + description: 'Gebäude, Zerstörung, Schäden', + sortOrder: 6, + patterns: ['Beschaedigung', 'Teilzerstoerung', 'Totalzerstoerung', 'Bruecke'], + }, + { + name: 'Einsatzführung', + description: 'Führung, Organisation, Kommunikation', + sortOrder: 7, + patterns: ['Einsatzleiter', 'ADL', 'KP_', 'Kontrollstelle', 'Informationszentrum', + 'Medienkontaktstelle', 'Sanitaet', 'Sanitaetshilfsstelle', 'Totensammelstelle', + 'Materialdepot', 'Schluesseldepot', 'Sammelstelle', 'Sammelplatz', 'Beobachtungsposten'], + }, + { + name: 'Fahrzeuge / Geräte', + description: 'Feuerwehrfahrzeuge, Leitern, Geräte', + sortOrder: 8, + patterns: ['TLF', 'HRF', 'Anhaengeleiter', 'Leiter', 'Sprungretter', 'Kleinloeschgeraet', + 'SWHP', 'MS_de'], + }, + { + name: 'Kommunikation / Sonstiges', + description: 'Funk, Wind, Massstab, etc.', + sortOrder: 9, + patterns: ['Funk', 'Windrichtung', 'Nordrichtung', 'Massstab', 'Helikopter', 'Armee', + 'Zivilschutz', 'Rettungen', 'Unfall', 'Rutschgebiet', 'Ueberschwemmung', + 'Abschnitt', 'Absperrung', 'Achtung', 'Anmarsch'], + }, +] + +function findCategory(filename) { + const base = filename.replace(/\.svg$/i, '') + for (const cat of catDefs) { + for (const p of cat.patterns) { + if (base.toLowerCase().includes(p.toLowerCase())) return cat.name + } + } + return 'Sonstiges' +} + +async function recover() { + console.log('🚨 SYMBOL RECOVERY STARTED') + console.log('') + console.log('⚠️ WARNUNG: Bestehende Zeichnungen bleiben broken!') + console.log(' Die alten iconId-UUIDs in den Feature-Properties sind verloren.') + console.log(' Benutzer müssen Symbole in Zeichnungen manuell neu setzen.') + console.log('') + + // ─── 1. Lade SVG-Dateien ─── + if (!fs.existsSync(SVG_DIR)) { + console.error('❌ SVG-Verzeichnis nicht gefunden:', SVG_DIR) + process.exit(1) + } + + const files = fs.readdirSync(SVG_DIR).filter(f => f.endsWith('.svg')) + console.log(`📁 ${files.length} SVG-Dateien gefunden`) + + // ─── 2. Erstelle Icon-Categories ─── + console.log('\n📂 Erstelle Icon-Categories...') + const categoryMap = {} + for (const def of catDefs) { + const cat = await prisma.iconCategory.upsert({ + where: { name: def.name }, + update: {}, + create: { + name: def.name, + description: def.description, + sortOrder: def.sortOrder, + isGlobal: true, + }, + }) + categoryMap[def.name] = cat.id + console.log(` ✅ ${cat.name}`) + } + // Sonstiges-Kategorie + const sonstiges = await prisma.iconCategory.upsert({ + where: { name: 'Sonstiges' }, + update: {}, + create: { name: 'Sonstiges', sortOrder: 99, isGlobal: true }, + }) + categoryMap['Sonstiges'] = sonstiges.id + + // ─── 3. Erstelle IconAssets ─── + console.log('\n🖼️ Erstelle IconAssets...') + const iconMap = {} // fileKey -> icon.id + for (const file of files) { + const filePath = path.join(SVG_DIR, file) + const stats = fs.statSync(filePath) + const fileKey = `lageplan-icons/symbols/${file}` + const name = file.replace(/\.svg$/i, '').replace(/_/g, ' ') + const catName = findCategory(file) + const categoryId = categoryMap[catName] + + const icon = await prisma.iconAsset.upsert({ + where: { fileKey }, + update: { + name, + mimeType: 'image/svg+xml', + isSystem: true, + isActive: true, + iconType: 'STANDARD', + categoryId, + }, + create: { + name, + fileKey, + mimeType: 'image/svg+xml', + isSystem: true, + isActive: true, + iconType: 'STANDARD', + categoryId, + }, + }) + iconMap[fileKey] = icon.id + } + console.log(` ✅ ${files.length} IconAssets erstellt/aktualisiert`) + + // ─── 4. Aktiviere alle Icons für alle Tenants ─── + console.log('\n🏢 Aktiviere Symbole für alle Tenants...') + const tenants = await prisma.tenant.findMany({ select: { id: true, name: true } }) + let tsCount = 0 + for (const tenant of tenants) { + for (const [fileKey, iconId] of Object.entries(iconMap)) { + await prisma.tenantSymbol.upsert({ + where: { + tenantId_iconId: { + tenantId: tenant.id, + iconId: iconId, + }, + }, + update: { isActive: true }, + create: { + tenantId: tenant.id, + iconId: iconId, + isActive: true, + }, + }) + tsCount++ + } + console.log(` ✅ ${tenant.name}: ${Object.keys(iconMap).length} Symbole aktiviert`) + } + + // ─── 5. Zeichnungen (Features) — Hinweis ─── + console.log('\n⚠️ FEATURES / ZEICHNUNGEN:') + const featureCount = await prisma.feature.count({ where: { type: 'symbol' } }) + console.log(` ${featureCount} Symbol-Features in der Datenbank gefunden.`) + console.log(` Diese zeigen auf GELÖSCHTE iconId-UUIDs und bleiben BROKEN.`) + console.log(` Es gibt KEINE Möglichkeit, die alten UUIDs wiederherzustellen.`) + console.log(` Benutzer müssen Symbole in ihren Zeichnungen manuell neu setzen.`) + + console.log('\n✅ RECOVERY ABGESCHLOSSEN') + console.log(' - Symbole sind jetzt in der Sidebar verfügbar') + console.log(' - Admin → Symbol-Manager zeigt alle Symbole an') + console.log(' - Zeichnungen müssen manuell repariert werden') +} + +recover() + .then(async () => { await prisma.$disconnect() }) + .catch(async (e) => { + console.error('Recovery error:', e) + await prisma.$disconnect() + process.exit(1) + })