From 56895be16fe5338f230bcdd0cc6e176866cf4970 Mon Sep 17 00:00:00 2001 From: Pepe Ziberi Date: Thu, 21 May 2026 07:05:49 +0200 Subject: [PATCH] fix(symbol-arch): robuste API-Endpoints + docker-entrypoint Migration/Seed --- docker-entrypoint.sh | 46 +++++++++++++++++++++- src/app/api/icons/route.ts | 54 +++++++++++++++++--------- src/app/api/templates/route.ts | 6 ++- src/app/api/tenant/categories/route.ts | 27 ++++++++----- src/app/api/tenant/symbols/route.ts | 41 +++++++++++++------ 5 files changed, 132 insertions(+), 42 deletions(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 2c8ff2a..7bae9a0 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -5,7 +5,11 @@ echo "=== Lageplan Startup ===" # ─── Step 1: Run migrations (uses PrismaClient raw SQL, no CLI needed) ─── echo "[1/3] Running database migrations..." -node prisma/migrate.js || echo " Warning: migrations had issues (may be first run)" +node prisma/migrate.js +if [ $? -ne 0 ]; then + echo "❌ Database migrations failed. Aborting startup." + exit 1 +fi # ─── Step 2: Conditional seeding ─── echo "[2/3] Checking if seeding is needed..." @@ -34,6 +38,44 @@ else fi fi -# ─── Step 3: Start server ─── +# ─── Step 3: Seed SymbolTemplates (idempotent) ─── +echo "[2b/3] Seeding symbol templates..." +node -e " +const { PrismaClient } = require('@prisma/client'); +const fs = require('fs'); +const path = require('path'); +const prisma = new PrismaClient(); + +async function seedTemplates() { + const sigDir = path.join(process.cwd(), 'public', 'signaturen'); + let files = []; + try { files = fs.readdirSync(sigDir).filter(f => f.endsWith('.svg')).sort(); } catch(e) { console.log('No signaturen dir'); } + let created = 0, skipped = 0; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const displayName = file.replace(/\.svg$/i, '').replace(/[_-]/g, ' ').trim(); + const fileKey = 'signaturen/' + file; + const existing = await prisma.symbolTemplate.findFirst({ where: { svgPath: fileKey } }).catch(() => null); + if (existing) { skipped++; continue; } + await prisma.symbolTemplate.create({ + data: { + packageId: 'feuerwehr-ch', + packageName: 'Feuerwehr Schweiz', + categoryName: 'Sonstiges', + name: displayName, + svgPath: fileKey, + tags: [displayName.toLowerCase()], + sortOrder: i, + } + }).catch(() => {}); + created++; + } + console.log('Templates: ' + created + ' created, ' + skipped + ' skipped'); + await prisma.\$disconnect(); +} +seedTemplates().catch(() => { console.log('Template seed skipped'); prisma.\$disconnect(); }); +" || echo " Warning: template seed failed" + +# ─── Step 4: Start server ─── echo "[3/3] Starting server..." exec node server-custom.js diff --git a/src/app/api/icons/route.ts b/src/app/api/icons/route.ts index 386d1a8..a8e223e 100644 --- a/src/app/api/icons/route.ts +++ b/src/app/api/icons/route.ts @@ -3,11 +3,12 @@ import { prisma } from '@/lib/db' import { getSession } from '@/lib/auth' export async function GET() { + /* ─── 1. Global library (legacy IconAsset) ─── */ + let categoriesWithUrls: any[] = [] try { const user = await getSession() const tenantId = user?.tenantId - /* ─── 1. Global library (legacy IconAsset) ─── */ const categoryWhere: any = tenantId ? { OR: [{ tenantId: null }, { tenantId }] } : {} @@ -34,7 +35,7 @@ export async function GET() { hiddenIconIds = tenant?.hiddenIconIds || [] } - const categoriesWithUrls = categories.map((cat: any) => ({ + categoriesWithUrls = categories.map((cat: any) => ({ ...cat, icons: cat.icons .filter((icon: any) => !hiddenIconIds.includes(icon.id)) @@ -43,10 +44,18 @@ export async function GET() { url: `/api/icons/${icon.id}/image`, })), })) + } catch (err) { + console.error('Error fetching legacy icon categories:', err) + categoriesWithUrls = [] + } - /* ─── 2. Tenant symbols (Phase 1 architecture) ─── */ - let tenantSymbolGroups: any[] = [] - let flatTenantSymbols: any[] = [] + /* ─── 2. Tenant symbols (Phase 1 architecture) ─── */ + let tenantSymbolGroups: any[] = [] + let flatTenantSymbols: any[] = [] + + try { + const user = await getSession() + const tenantId = user?.tenantId if (tenantId) { const tenantSymbols = await (prisma as any).tenantSymbol.findMany({ @@ -70,7 +79,6 @@ export async function GET() { : `/api/icons/${ts.id}/image`, })) - // Group by category for sidebar display const groups = new Map() for (const sym of flatTenantSymbols) { const key = sym.categoryId || null @@ -78,7 +86,6 @@ export async function GET() { groups.get(key)!.push(sym) } - // Fetch categories that have symbols but may not be in the symbol list (empty ones are omitted) const catIds = Array.from(groups.keys()).filter(Boolean) as string[] const tenantCategories = catIds.length ? await (prisma as any).tenantCategory.findMany({ @@ -100,9 +107,18 @@ export async function GET() { }) tenantSymbolGroups.sort((a, b) => a.sortOrder - b.sortOrder) } + } catch (err) { + console.error('Error fetching tenant symbols:', err) + tenantSymbolGroups = [] + flatTenantSymbols = [] + } + + /* ─── 3. Legacy mySymbols (keep for old clients during transition) ─── */ + let mySymbolsLegacy: any[] = [] + try { + const user = await getSession() + const tenantId = user?.tenantId - /* ─── 3. Legacy mySymbols (keep for old clients during transition) ─── */ - let mySymbolsLegacy: any[] = [] if (tenantId) { const legacy = await (prisma as any).tenantSymbol.findMany({ where: { tenantId, iconId: { not: null } }, @@ -119,15 +135,15 @@ export async function GET() { url: `/api/icons/${ts.icon.id}/image`, })) } - - return NextResponse.json({ - categories: categoriesWithUrls, - mySymbols: mySymbolsLegacy, // legacy shape for old clients - tenantSymbols: flatTenantSymbols, // new flat list - tenantSymbolGroups, // grouped by TenantCategory - }) - } catch (error) { - console.error('Error fetching icons:', error) - return NextResponse.json({ error: 'Serverfehler' }, { status: 500 }) + } catch (err) { + console.error('Error fetching legacy mySymbols:', err) + mySymbolsLegacy = [] } + + return NextResponse.json({ + categories: categoriesWithUrls, + mySymbols: mySymbolsLegacy, + tenantSymbols: flatTenantSymbols, + tenantSymbolGroups, + }) } diff --git a/src/app/api/templates/route.ts b/src/app/api/templates/route.ts index 41ab93e..60769dc 100644 --- a/src/app/api/templates/route.ts +++ b/src/app/api/templates/route.ts @@ -43,8 +43,12 @@ export async function GET() { ) return NextResponse.json({ packages: result }) - } catch (error) { + } catch (error: any) { console.error('Error fetching templates:', error) + // Return empty packages list on DB schema mismatch so UI doesn't crash + if (error?.message?.includes('symbol_templates') || error?.code === 'P2021') { + return NextResponse.json({ packages: [] }) + } return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 }) } } diff --git a/src/app/api/tenant/categories/route.ts b/src/app/api/tenant/categories/route.ts index 885c8c6..dc6d006 100644 --- a/src/app/api/tenant/categories/route.ts +++ b/src/app/api/tenant/categories/route.ts @@ -24,22 +24,31 @@ export async function GET() { if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) const { tenantId } = auth - const categories = await (prisma as any).tenantCategory.findMany({ - where: { tenantId }, - include: { - _count: { - select: { symbols: true }, + let categories: any[] = [] + try { + categories = await (prisma as any).tenantCategory.findMany({ + where: { tenantId }, + include: { + _count: { + select: { symbols: true }, + }, }, - }, - orderBy: { sortOrder: 'asc' }, - }) + orderBy: { sortOrder: 'asc' }, + }) + } catch (dbErr: any) { + console.error('tenantCategory.findMany failed:', dbErr) + if (dbErr?.message?.includes('tenant_categories') || dbErr?.code === 'P2021') { + return NextResponse.json({ categories: [] }) + } + throw dbErr + } const result = categories.map((cat: any) => ({ id: cat.id, name: cat.name, sortOrder: cat.sortOrder, icon: cat.icon, - symbolCount: cat._count.symbols, + symbolCount: cat._count?.symbols ?? 0, createdAt: cat.createdAt, })) diff --git a/src/app/api/tenant/symbols/route.ts b/src/app/api/tenant/symbols/route.ts index 3aae328..20d17d0 100644 --- a/src/app/api/tenant/symbols/route.ts +++ b/src/app/api/tenant/symbols/route.ts @@ -29,13 +29,22 @@ export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url) const grouped = searchParams.get('grouped') !== 'false' - const tenantSymbols = await (prisma as any).tenantSymbol.findMany({ - where: { tenantId }, - include: { - category: { select: { id: true, name: true, sortOrder: true } }, - }, - orderBy: [{ category: { sortOrder: 'asc' } }, { sortOrder: 'asc' }, { name: 'asc' }], - }) + let tenantSymbols: any[] = [] + try { + tenantSymbols = await (prisma as any).tenantSymbol.findMany({ + where: { tenantId }, + include: { + category: { select: { id: true, name: true, sortOrder: true } }, + }, + orderBy: [{ category: { sortOrder: 'asc' } }, { sortOrder: 'asc' }, { name: 'asc' }], + }) + } catch (dbErr: any) { + console.error('tenantSymbol.findMany failed:', dbErr) + if (dbErr?.message?.includes('tenant_symbols') || dbErr?.code === 'P2021') { + return NextResponse.json({ categories: [], symbols: [] }) + } + throw dbErr + } const mapped = tenantSymbols.map((ts: any) => ({ id: ts.id, @@ -62,10 +71,20 @@ export async function GET(req: NextRequest) { } // Sort categories by sortOrder from category - const categories = await (prisma as any).tenantCategory.findMany({ - where: { tenantId }, - orderBy: { sortOrder: 'asc' }, - }) + let categories: any[] = [] + try { + categories = await (prisma as any).tenantCategory.findMany({ + where: { tenantId }, + orderBy: { sortOrder: 'asc' }, + }) + } catch (catErr: any) { + console.error('tenantCategory.findMany failed:', catErr) + if (catErr?.message?.includes('tenant_categories') || catErr?.code === 'P2021') { + categories = [] + } else { + throw catErr + } + } const ordered: Array<{ categoryId: string | null; categoryName: string; symbols: any[] }> = [] for (const cat of categories) {