/** * 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: * Analyse: node prisma/recover-symbols.js --dry-run * Anwenden: node prisma/recover-symbols.js --apply */ const { PrismaClient } = require('@prisma/client') const fs = require('fs') const path = require('path') const prisma = new PrismaClient() 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 ──────────────────────────────────────────────── const catDefs = [ { 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', '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) { for (const def of catDefs) { for (const pattern of def.patterns) { if (filename.includes(pattern)) return def.name } } return 'Verschiedenes' } // ─── Main ────────────────────────────────────────────────────────────────── 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')).sort() console.log(`\n📁 ${files.length} SVG-Dateien in public/signaturen/ gefunden`) // ─── 1. Kategorien Upserten ─── console.log('\n[1/4] Kategorien...') const catMap = {} for (const def of catDefs) { 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 }, }) 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(` 📊 ${features.length} symbol-Features gefunden`) console.log(` ✅ ${validRefs} gültige/keine Referenzen`) console.log(` ❌ ${brokenRefs} kaputte Referenzen (nicht reparierbar ohne Backup)`) 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('❌ Fehler:', e) await prisma.$disconnect() process.exit(1) })