fix(db): comprehensive symbol recovery + safety fixes
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 22m1s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 22m1s
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Recovery-Skript für gelöschte/neu erstellte Symbole
|
||||
*
|
||||
* Stellt sicher, dass alle Icons aus public/signaturen/ als IconAssets
|
||||
* in der DB vorhanden sind und für alle Tenants aktiviert sind.
|
||||
*
|
||||
* Unterstützt Dry-Run und Apply-Modus.
|
||||
*
|
||||
* Ausführung:
|
||||
* node prisma/recover-symbols.js
|
||||
*
|
||||
* Umgebung:
|
||||
* DATABASE_URL muss gesetzt sein (oder .env.docker geladen werden)
|
||||
* Analyse: node prisma/recover-symbols.js --dry-run
|
||||
* Anwenden: node prisma/recover-symbols.js --apply
|
||||
*/
|
||||
|
||||
const { PrismaClient } = require('@prisma/client')
|
||||
@@ -18,200 +17,242 @@ const path = require('path')
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const SVG_DIR = path.join(__dirname, '..', 'public', 'signaturen')
|
||||
const IS_DRY_RUN = process.argv.includes('--dry-run')
|
||||
const IS_APPLY = process.argv.includes('--apply')
|
||||
const IS_PROD = process.env.NODE_ENV === 'production'
|
||||
|
||||
// ─── Safety Guards ─────────────────────────────────────────────────────────
|
||||
|
||||
if (!IS_DRY_RUN && !IS_APPLY) {
|
||||
console.error('❌ Fehler: Bitte --dry-run oder --apply angeben')
|
||||
console.error('')
|
||||
console.error(' Analyse: node prisma/recover-symbols.js --dry-run')
|
||||
console.error(' Anwenden: node prisma/recover-symbols.js --apply')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (IS_DRY_RUN && IS_APPLY) {
|
||||
console.error('❌ Fehler: --dry-run und --apply können nicht zusammen verwendet werden')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (IS_APPLY && IS_PROD) {
|
||||
console.log('⚠️ Production-Umgebung erkannt. Fortfahren in 3 Sekunden...')
|
||||
await new Promise(r => setTimeout(r, 3000))
|
||||
}
|
||||
|
||||
console.log(IS_DRY_RUN ? '🔍 DRY-RUN MODUS' : '🔧 APPLY-MODUS')
|
||||
|
||||
// ─── Kategorie-Definitionen ────────────────────────────────────────────────
|
||||
|
||||
// Kategorie-Definitionen (wie im Original-Seed)
|
||||
const catDefs = [
|
||||
{
|
||||
name: 'Wasser',
|
||||
description: 'Wasser, Hydranten, Leitungen',
|
||||
sortOrder: 2,
|
||||
{ name: 'Feuer / Brand', description: 'Feuer, Brand, Brandschutz', sortOrder: 1,
|
||||
patterns: ['Feuer', 'Brand', 'Explosion', 'Entrauchung', 'Rauch', 'Kamin', 'Luefter', 'Sprinkler'] },
|
||||
{ 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'],
|
||||
},
|
||||
'Schieber', 'Wasserloeschposten', 'Wasserversorgung', 'Wasserleitung', 'Ueberschwemmung',
|
||||
'Offener_Wasserverlauf', 'Moeglicher_Wasserbezugsort', 'Stehendes_Gewaesser',
|
||||
'Kleinloeschgeraet', 'Gefahr_durch_Loeschen_mit_Wasser'] },
|
||||
{ name: 'Gefahren / Stoffe', description: 'Gefahrstoffe, Chemie, ABC', sortOrder: 3,
|
||||
patterns: ['Gefaehrlich', 'Chemie', 'Chemikalien', 'Biologisch', 'Radioaktiv', 'Gas', 'Elektri',
|
||||
'Elektrotableau', 'Achtung', 'Leitungsdaehte'] },
|
||||
{ name: 'Rettung / Personen', description: 'Rettung, Sanität, Personen', sortOrder: 4,
|
||||
patterns: ['Rettung', 'Retten', 'Sanitaet', 'Patienten', 'Sammelplatz', 'Sammelstelle',
|
||||
'Totensammelstelle', 'Sprungretter', 'Helikopterlandeplatz'] },
|
||||
{ name: 'Leitern / Geräte', description: 'Leitern, Fahrzeuge, Geräte', sortOrder: 5,
|
||||
patterns: ['Leiter', 'Anhaengeleiter', 'Strebenleiter', 'ADL', 'TLF', 'HRF', 'SWHP'] },
|
||||
{ name: 'Gebäude / Schäden', description: 'Gebäude, Zerstörung, Schäden', sortOrder: 6,
|
||||
patterns: ['Beschaedigung', 'Teilzerstoerung', 'Totalzerstoerung', 'Decke_eingestuerzt',
|
||||
'Umfassungswaende', 'AnzahlGeschosse', 'Eingang', 'Treppen', 'Lift', 'Bruecke',
|
||||
'Rutschgebiet', 'Strasse', 'Bahnlin'] },
|
||||
{ name: 'Einsatzführung', description: 'Führung, Organisation, Kommunikation', sortOrder: 7,
|
||||
patterns: ['Einsatzleiter', 'KP_Font', 'Offizier', 'Beobachtungsposten', 'Kontrollstelle',
|
||||
'Funk', 'Informationszentrum', 'Medienkontaktstelle', 'Fernsignaltableau',
|
||||
'Materialdepot', 'Schluesseldepot', 'Abschnitt', 'Absperrung'] },
|
||||
{ name: 'Organisationen', description: 'Feuerwehr, Polizei, Armee, Zivilschutz', sortOrder: 8,
|
||||
patterns: ['Feuerwehr', 'Polizei', 'Armee', 'Zivilschutz', 'Chemiewehr', 'MS_de', 'Anmarsch'] },
|
||||
{ name: 'Entwicklung / Taktik', description: 'Entwicklungsgrenzen, Ausbreitung', sortOrder: 9,
|
||||
patterns: ['Entwicklungsgrenze', 'Horizontale_Entwicklung', 'Vertikale_Entwicklung',
|
||||
'VertikaleEntwicklung', 'Unfall'] },
|
||||
{ name: 'Verschiedenes', description: 'Weitere Signaturen', sortOrder: 10,
|
||||
patterns: ['Massstab', 'Nordrichtung', 'Windrichtung'] },
|
||||
]
|
||||
|
||||
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
|
||||
for (const def of catDefs) {
|
||||
for (const pattern of def.patterns) {
|
||||
if (filename.includes(pattern)) return def.name
|
||||
}
|
||||
}
|
||||
return 'Sonstiges'
|
||||
return 'Verschiedenes'
|
||||
}
|
||||
|
||||
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('')
|
||||
// ─── Main ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// ─── 1. Lade SVG-Dateien ───
|
||||
async function recover() {
|
||||
console.log('\n🚨 SYMBOL RECOVERY')
|
||||
console.log('────────────────────────────────────────')
|
||||
|
||||
const SVG_DIR = path.join(__dirname, '..', 'public', 'signaturen')
|
||||
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`)
|
||||
const files = fs.readdirSync(SVG_DIR).filter(f => f.endsWith('.svg')).sort()
|
||||
console.log(`\n📁 ${files.length} SVG-Dateien in public/signaturen/ gefunden`)
|
||||
|
||||
// ─── 2. Erstelle Icon-Categories ───
|
||||
console.log('\n📂 Erstelle Icon-Categories...')
|
||||
const categoryMap = {}
|
||||
// ─── 1. Kategorien Upserten ───
|
||||
console.log('\n[1/4] Kategorien...')
|
||||
const catMap = {}
|
||||
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,
|
||||
},
|
||||
const catId = def.name.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()
|
||||
if (IS_APPLY) {
|
||||
const cat = await prisma.iconCategory.upsert({
|
||||
where: { id: catId },
|
||||
update: { name: def.name, description: def.description, sortOrder: def.sortOrder, isGlobal: true },
|
||||
create: { id: catId, name: def.name, description: def.description, sortOrder: def.sortOrder, isGlobal: true, tenantId: null },
|
||||
})
|
||||
tsCount++
|
||||
catMap[def.name] = cat.id
|
||||
} else {
|
||||
catMap[def.name] = catId
|
||||
console.log(` 📝 ${def.name} (ID: ${catId})`)
|
||||
}
|
||||
}
|
||||
if (IS_APPLY) console.log(` ✅ ${catDefs.length} Kategorien upserted`)
|
||||
|
||||
// ─── 2. IconAssets Upserten ───
|
||||
console.log('\n[2/4] IconAssets...')
|
||||
const iconMap = {} // fileKey -> { id, name }
|
||||
let created = 0
|
||||
let updated = 0
|
||||
|
||||
for (const file of files) {
|
||||
let name = file.replace('.svg', '')
|
||||
name = name.replace(/_de$/i, '').replace(/_DE$/i, '').replace(/-de$/i, '')
|
||||
name = name.replace(/[_-]/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
|
||||
const fileKey = `signaturen/${file}` // WICHTIG: Gleiches Format wie Seed!
|
||||
const category = findCategory(file)
|
||||
const categoryId = catMap[category]
|
||||
|
||||
if (IS_APPLY) {
|
||||
const existing = await prisma.iconAsset.findFirst({ where: { fileKey } })
|
||||
if (existing) {
|
||||
await prisma.iconAsset.update({
|
||||
where: { id: existing.id },
|
||||
data: { name, categoryId, mimeType: 'image/svg+xml', isSystem: true, isActive: true, width: 48, height: 48 },
|
||||
})
|
||||
iconMap[fileKey] = { id: existing.id, name }
|
||||
updated++
|
||||
} else {
|
||||
const icon = await prisma.iconAsset.create({
|
||||
data: { name, categoryId, fileKey, mimeType: 'image/svg+xml', isSystem: true, width: 48, height: 48 },
|
||||
})
|
||||
iconMap[fileKey] = { id: icon.id, name }
|
||||
created++
|
||||
}
|
||||
} else {
|
||||
// Dry-Run: Simuliere Lookup
|
||||
const existing = await prisma.iconAsset.findFirst({ where: { fileKey }, select: { id: true, name: true } })
|
||||
if (existing) {
|
||||
iconMap[fileKey] = { id: existing.id, name: existing.name }
|
||||
updated++
|
||||
} else {
|
||||
iconMap[fileKey] = { id: `(neu: ${fileKey})`, name }
|
||||
created++
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(` ${IS_APPLY ? '✅' : '📝'} ${created} neu, ${updated} aktualisiert, ${files.length} total`)
|
||||
|
||||
// ─── 3. TenantSymbols erstellen ───
|
||||
console.log('\n[3/4] TenantSymbols...')
|
||||
const tenants = await prisma.tenant.findMany({ select: { id: true, name: true } })
|
||||
let tsCreated = 0
|
||||
let tsExisting = 0
|
||||
|
||||
for (const tenant of tenants) {
|
||||
let tenantNew = 0
|
||||
let tenantExist = 0
|
||||
|
||||
for (const [fileKey, icon] of Object.entries(iconMap)) {
|
||||
if (IS_APPLY) {
|
||||
// Prüfe ob bereits existiert
|
||||
const existing = await prisma.tenantSymbol.findFirst({
|
||||
where: { tenantId: tenant.id, iconId: icon.id },
|
||||
})
|
||||
if (existing) {
|
||||
tenantExist++
|
||||
tsExisting++
|
||||
} else {
|
||||
await prisma.tenantSymbol.create({
|
||||
data: { tenantId: tenant.id, iconId: icon.id, isActive: true },
|
||||
})
|
||||
tenantNew++
|
||||
tsCreated++
|
||||
}
|
||||
} else {
|
||||
// Dry-Run: Prüfe Existenz
|
||||
const existing = await prisma.tenantSymbol.findFirst({
|
||||
where: { tenantId: tenant.id, iconId: icon.id },
|
||||
select: { id: true },
|
||||
})
|
||||
if (existing) {
|
||||
tenantExist++
|
||||
tsExisting++
|
||||
} else {
|
||||
tenantNew++
|
||||
tsCreated++
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(` ${IS_APPLY ? '✅' : '📝'} ${tenant.name}: ${tenantNew} neu, ${tenantExist} bereits vorhanden`)
|
||||
}
|
||||
console.log(` Gesamt: ${tsCreated} neu, ${tsExisting} bereits vorhanden`)
|
||||
|
||||
// ─── 4. Feature-Analyse ───
|
||||
console.log('\n[4/4] Feature-Analyse (Zeichnungen)...')
|
||||
const features = await prisma.feature.findMany({
|
||||
where: { type: 'symbol' },
|
||||
select: { id: true, properties: true, projectId: true },
|
||||
})
|
||||
|
||||
const validIconIds = new Set(Object.values(iconMap).map(i => i.id))
|
||||
let validRefs = 0
|
||||
let brokenRefs = 0
|
||||
|
||||
for (const f of features) {
|
||||
const iconId = f.properties?.iconId
|
||||
if (!iconId) {
|
||||
validRefs++ // Keine Referenz = nicht kaputt
|
||||
} else if (validIconIds.has(iconId)) {
|
||||
validRefs++
|
||||
} else {
|
||||
brokenRefs++
|
||||
}
|
||||
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(` 📊 ${features.length} symbol-Features gefunden`)
|
||||
console.log(` ✅ ${validRefs} gültige/keine Referenzen`)
|
||||
console.log(` ❌ ${brokenRefs} kaputte Referenzen (nicht reparierbar ohne Backup)`)
|
||||
|
||||
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')
|
||||
if (brokenRefs > 0) {
|
||||
console.log('\n ⚠️ HINWEIS: Kaputte Features können NICHT automatisch repariert werden.')
|
||||
console.log(' Die alten iconId-UUIDs sind verloren.')
|
||||
console.log(' Verwende: npm run repair:features:dry-run')
|
||||
}
|
||||
|
||||
console.log('\n✅ RECOVERY ABgeschlossen')
|
||||
if (IS_DRY_RUN) {
|
||||
console.log(' Zum Anwenden: node prisma/recover-symbols.js --apply')
|
||||
}
|
||||
}
|
||||
|
||||
recover()
|
||||
.then(async () => { await prisma.$disconnect() })
|
||||
.catch(async (e) => {
|
||||
console.error('Recovery error:', e)
|
||||
console.error('❌ Fehler:', e)
|
||||
await prisma.$disconnect()
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user