docs: add incident report for symbol loss + recovery script
This commit is contained in:
134
INCIDENT-REPORT-2026-05-20.md
Normal file
134
INCIDENT-REPORT-2026-05-20.md
Normal file
@@ -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
|
||||||
217
prisma/recover-symbols.js
Normal file
217
prisma/recover-symbols.js
Normal file
@@ -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)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user