fix(symbol-arch): robuste API-Endpoints + docker-entrypoint Migration/Seed
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 14m59s

This commit is contained in:
Pepe Ziberi
2026-05-21 07:05:49 +02:00
parent e9f66b2c3d
commit 56895be16f
5 changed files with 132 additions and 42 deletions

View File

@@ -5,7 +5,11 @@ echo "=== Lageplan Startup ==="
# ─── Step 1: Run migrations (uses PrismaClient raw SQL, no CLI needed) ─── # ─── Step 1: Run migrations (uses PrismaClient raw SQL, no CLI needed) ───
echo "[1/3] Running database migrations..." 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 ─── # ─── Step 2: Conditional seeding ───
echo "[2/3] Checking if seeding is needed..." echo "[2/3] Checking if seeding is needed..."
@@ -34,6 +38,44 @@ else
fi fi
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..." echo "[3/3] Starting server..."
exec node server-custom.js exec node server-custom.js

View File

@@ -3,11 +3,12 @@ import { prisma } from '@/lib/db'
import { getSession } from '@/lib/auth' import { getSession } from '@/lib/auth'
export async function GET() { export async function GET() {
/* ─── 1. Global library (legacy IconAsset) ─── */
let categoriesWithUrls: any[] = []
try { try {
const user = await getSession() const user = await getSession()
const tenantId = user?.tenantId const tenantId = user?.tenantId
/* ─── 1. Global library (legacy IconAsset) ─── */
const categoryWhere: any = tenantId const categoryWhere: any = tenantId
? { OR: [{ tenantId: null }, { tenantId }] } ? { OR: [{ tenantId: null }, { tenantId }] }
: {} : {}
@@ -34,7 +35,7 @@ export async function GET() {
hiddenIconIds = tenant?.hiddenIconIds || [] hiddenIconIds = tenant?.hiddenIconIds || []
} }
const categoriesWithUrls = categories.map((cat: any) => ({ categoriesWithUrls = categories.map((cat: any) => ({
...cat, ...cat,
icons: cat.icons icons: cat.icons
.filter((icon: any) => !hiddenIconIds.includes(icon.id)) .filter((icon: any) => !hiddenIconIds.includes(icon.id))
@@ -43,11 +44,19 @@ export async function GET() {
url: `/api/icons/${icon.id}/image`, url: `/api/icons/${icon.id}/image`,
})), })),
})) }))
} catch (err) {
console.error('Error fetching legacy icon categories:', err)
categoriesWithUrls = []
}
/* ─── 2. Tenant symbols (Phase 1 architecture) ─── */ /* ─── 2. Tenant symbols (Phase 1 architecture) ─── */
let tenantSymbolGroups: any[] = [] let tenantSymbolGroups: any[] = []
let flatTenantSymbols: any[] = [] let flatTenantSymbols: any[] = []
try {
const user = await getSession()
const tenantId = user?.tenantId
if (tenantId) { if (tenantId) {
const tenantSymbols = await (prisma as any).tenantSymbol.findMany({ const tenantSymbols = await (prisma as any).tenantSymbol.findMany({
where: { tenantId }, where: { tenantId },
@@ -70,7 +79,6 @@ export async function GET() {
: `/api/icons/${ts.id}/image`, : `/api/icons/${ts.id}/image`,
})) }))
// Group by category for sidebar display
const groups = new Map<string | null, any[]>() const groups = new Map<string | null, any[]>()
for (const sym of flatTenantSymbols) { for (const sym of flatTenantSymbols) {
const key = sym.categoryId || null const key = sym.categoryId || null
@@ -78,7 +86,6 @@ export async function GET() {
groups.get(key)!.push(sym) 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 catIds = Array.from(groups.keys()).filter(Boolean) as string[]
const tenantCategories = catIds.length const tenantCategories = catIds.length
? await (prisma as any).tenantCategory.findMany({ ? await (prisma as any).tenantCategory.findMany({
@@ -100,9 +107,18 @@ export async function GET() {
}) })
tenantSymbolGroups.sort((a, b) => a.sortOrder - b.sortOrder) 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) ─── */ /* ─── 3. Legacy mySymbols (keep for old clients during transition) ─── */
let mySymbolsLegacy: any[] = [] let mySymbolsLegacy: any[] = []
try {
const user = await getSession()
const tenantId = user?.tenantId
if (tenantId) { if (tenantId) {
const legacy = await (prisma as any).tenantSymbol.findMany({ const legacy = await (prisma as any).tenantSymbol.findMany({
where: { tenantId, iconId: { not: null } }, where: { tenantId, iconId: { not: null } },
@@ -119,15 +135,15 @@ export async function GET() {
url: `/api/icons/${ts.icon.id}/image`, url: `/api/icons/${ts.icon.id}/image`,
})) }))
} }
} catch (err) {
console.error('Error fetching legacy mySymbols:', err)
mySymbolsLegacy = []
}
return NextResponse.json({ return NextResponse.json({
categories: categoriesWithUrls, categories: categoriesWithUrls,
mySymbols: mySymbolsLegacy, // legacy shape for old clients mySymbols: mySymbolsLegacy,
tenantSymbols: flatTenantSymbols, // new flat list tenantSymbols: flatTenantSymbols,
tenantSymbolGroups, // grouped by TenantCategory tenantSymbolGroups,
}) })
} catch (error) {
console.error('Error fetching icons:', error)
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
}
} }

View File

@@ -43,8 +43,12 @@ export async function GET() {
) )
return NextResponse.json({ packages: result }) return NextResponse.json({ packages: result })
} catch (error) { } catch (error: any) {
console.error('Error fetching templates:', error) 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 }) return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
} }
} }

View File

@@ -24,7 +24,9 @@ export async function GET() {
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const { tenantId } = auth const { tenantId } = auth
const categories = await (prisma as any).tenantCategory.findMany({ let categories: any[] = []
try {
categories = await (prisma as any).tenantCategory.findMany({
where: { tenantId }, where: { tenantId },
include: { include: {
_count: { _count: {
@@ -33,13 +35,20 @@ export async function GET() {
}, },
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) => ({ const result = categories.map((cat: any) => ({
id: cat.id, id: cat.id,
name: cat.name, name: cat.name,
sortOrder: cat.sortOrder, sortOrder: cat.sortOrder,
icon: cat.icon, icon: cat.icon,
symbolCount: cat._count.symbols, symbolCount: cat._count?.symbols ?? 0,
createdAt: cat.createdAt, createdAt: cat.createdAt,
})) }))

View File

@@ -29,13 +29,22 @@ export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url) const { searchParams } = new URL(req.url)
const grouped = searchParams.get('grouped') !== 'false' const grouped = searchParams.get('grouped') !== 'false'
const tenantSymbols = await (prisma as any).tenantSymbol.findMany({ let tenantSymbols: any[] = []
try {
tenantSymbols = await (prisma as any).tenantSymbol.findMany({
where: { tenantId }, where: { tenantId },
include: { include: {
category: { select: { id: true, name: true, sortOrder: true } }, category: { select: { id: true, name: true, sortOrder: true } },
}, },
orderBy: [{ category: { sortOrder: 'asc' } }, { sortOrder: 'asc' }, { name: 'asc' }], 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) => ({ const mapped = tenantSymbols.map((ts: any) => ({
id: ts.id, id: ts.id,
@@ -62,10 +71,20 @@ export async function GET(req: NextRequest) {
} }
// Sort categories by sortOrder from category // Sort categories by sortOrder from category
const categories = await (prisma as any).tenantCategory.findMany({ let categories: any[] = []
try {
categories = await (prisma as any).tenantCategory.findMany({
where: { tenantId }, where: { tenantId },
orderBy: { sortOrder: 'asc' }, 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[] }> = [] const ordered: Array<{ categoryId: string | null; categoryName: string; symbols: any[] }> = []
for (const cat of categories) { for (const cat of categories) {