35 KiB
CapaKraken – Projekt-Learnings
Format
Datum | Kategorie | Problem → Lösung
Learnings
2026-03-13 | Architecture | Dispo v2 chargeability calculator design
- Pure functions in
packages/engine/src/chargeability/calculator.ts— no DB imports, all data passed as arguments. deriveResourceForecast()takes SAH + assignment slices per month, returns ratio breakdown (Chg/BD/MD&I/M&O/PD&R/Absence/Unassigned).- Group aggregation uses FTE-weighted averages:
SUM(fte * chg) / SUM(fte). - Assignment slices derived from
countWorkingDaysInOverlap()per month, category code from project's utilization category. - tRPC
clientis a reserved router key — renamed toclientEntityto avoid collision with built-in.clientproperty. - React Query v5 (tRPC v11):
keepPreviousDataremoved, useplaceholderData: (prev) => previnstead.
2026-03-12 | UX/DX | Deep tRPC mutation inference in large client files
BlueprintsClient.tsxhitTS2589when multipletrpc.blueprint.*.useMutation({ onSuccess ... })hooks lived in the same large component together with heavily inferred table/sort state.- Stable fix: use bare
useMutation()hooks and move invalidation / selection cleanup into explicitmutateAsync()handlers. This reduces generic expansion and keeps side effects easier to follow. - For shared sortable tables, keep the internal sort union typed (
BlueprintSortField) and cast only at the generic UI boundary (SortableColumnHeadercurrently exposesstringfields).
2026-03-12 | Build | NextAuth portable export typing
export const { handlers, auth, signIn, signOut } = NextAuth(...)triggeredTS2742because the inferredsignIntype captured provider internals from@auth/core.- If the server-side
signIn/signOutexports are unused, export onlyhandlersandauth. Also prefer a namedauthConfig satisfies NextAuthConfigobject for clearer config typing.
2026-03-11 | Architecture | Phase 1: Application Layer Extraction
- Created
packages/applicationwithcreateAllocationandfillPlaceholderuse-case services packages/apirouter procedures now delegate to use cases; they only check permissions and emit SSE eventspackages/applicationdepends on@capakraken/db,@capakraken/engine,@capakraken/shared;packages/apidepends on@capakraken/application- Use cases throw
TRPCErrordirectly (pragmatic — project only uses tRPC transport) Prisma.AllocationGetPayload<{ include: ... }>used for precise return type in use casesexactOptionalPropertyTypes+ optional params: caller must use spread...(val !== undefined ? { key: val } : {})when passing zod inputs to use cases with{ key?: T }interfacesfillPlaceholderreturns{ filled, decrementedPlaceholder? }— UIonSuccesscallbacks that don't use result data are unaffected by return shape changes
2026-03-12 | Architecture | Dashboard query extraction into application layer
- Moved dashboard aggregation/query logic out of
packages/api/src/router/dashboard.tsintopackages/application/src/use-cases/dashboard/*. - Keep transport concerns in the router: Zod input validation and procedure permissions remain there, while query composition and aggregation now sit in reusable application services.
- Add small shared helpers (
calculateInclusiveDays, bucket-key builders, average daily availability) to avoid repeating date math across dashboard slices. - Added
packages/application/src/__tests__/dashboard.test.tsso the extraction is covered at the package boundary rather than only indirectly through API procedures. - While extracting
getDemand, fix the chapter grouping bug whereresourceCountwas always0; it now counts distinct resources per chapter.
2026-03-12 | Architecture | Estimating foundation slice
- Added first-class Prisma estimating models for
Estimate,EstimateVersion, assumptions, scope items, demand lines, rate cards, resource snapshots, metrics, and exports. - Keep this slice deliberately narrow: persistence + shared contracts + application/engine boundaries first, before any wizard/workspace UI. That avoids baking spreadsheet-shaped UI assumptions into the domain model.
- Shared estimate enums/types/schemas now live in
@capakraken/shared, and initial application commands/queries (createEstimate,listEstimates,getEstimateById) live in@capakraken/application. - Added a small engine contract
summarizeEstimateDemandLines()for aggregate financial totals so later estimate work can reuse a typed pure-function boundary instead of recomputing ad hoc in routers/components.
2026-03-11 | Architecture | Tasks 23-27: Bulk Edit, Validation, Export, Reorder
- Blueprint custom field validation lives in
packages/engine/src/blueprint/validator.ts(pure function, no DB). Wire intoresource.updateby fetching the blueprint's fieldDefs and callingvalidateCustomFields()before saving. ThrowTRPCError({ code: "UNPROCESSABLE_CONTENT" })on error. - Batch JSONB merge (without overwriting other keys): use
$executeRawwith PostgreSQL's||JSONB merge operator:UPDATE "Resource" SET "dynamicFields" = "dynamicFields" || ${JSON.stringify(fields)}::jsonb WHERE id = ${id}. Cannot use Prismaupdate()for JSONB partial merge. - Column drag-to-reorder: HTML5 draggable API works for lists without external libraries. Use
useRef<string | null>to track drag source key, thenonDropcalls thereorder()function. - Virtual scroll for table rows is complex: absolute positioning breaks
<table>column widths. Cursor-based infinite scroll (50 rows/page) is sufficient for typical resource counts (<500). Skip virtualizer for HTML tables. - JSONB field name in Prisma schema is
dynamicFields(notcustomFields). Always verify the actual Prisma model field name before writing filter/update code. - CSV export with proper escaping: wrap value in double quotes and escape internal
"as""when the value contains commas, quotes, or newlines.
2026-03-11 | Architecture | JSONB filtering + useFilters hook (Tasks 20-22)
- Prisma JSONB path filtering:
{ customFields: { path: [key], string_contains: value } }for text;{ equals: bool }for BOOLEAN;{ array_contains: value }for MULTI_SELECT. Build asany[]array and spread asAND: cfConditions— avoids Prisma union type issues. flatMapwith multiple return types causes TS union inference that Prisma WHERE types reject. Use aforloop withpushinto an explicitly typedany[]instead.- Next.js typed routes (
typedRoutes: true) rejects dynamic URL strings even withas unknown as RouteImpl. Fix: cast the router itself withuseRouter() as unknown as { replace: (url: string, opts?) => void }to escape the branded type system for dynamic URLs. useSearchParamsrequires<Suspense>wrapping at the page level in Next.js App Router or the page will be statically rendered without search param access.
2026-03-11 | Security | Phase 0 critical fixes
user.createwas hashing passwords with SHA-256;auth.tsverifies with Argon2 → users created via admin couldn't log in. Fix: importhashfrom@node-rs/argon2in the router. Must also declare@node-rs/argon2inpackages/api/package.json— being a dep of@capakraken/dbis not enough for TS resolution.notification.createwasprotectedProcedure→ any logged-in user could create notifications for arbitrary users. Fix: changed tomanagerProcedure.testAiConnectionalways built Azure deployment URLs regardless ofaiProvider. Fix: branch on provider, usehttps://api.openai.com/v1/chat/completionswithAuthorization: Bearerfor OpenAI.@capakraken/sharedhadtest:unit: vitest runin package.json but no test files → turbo failed. Fix: remove the script (tests live only in engine/staffing).crypto.randomUUID()inpackages/shared/src/schemas/project.schema.tsfailed typecheck because base tsconfig uses"lib": ["ES2022"]without DOM. Fix: add"lib": ["ES2022", "DOM"]in the shared package's own tsconfig.
2026-03-09 | Performance | Budget utilization showing 562% due to wrong aggregation
Problem: getOverview summed allocation.project.budgetCents once per allocation, counting project budgets multiple times for multi-resource projects.
Fix: Sum allProjects.budgetCents (already fetched) for total budget; compute cost as dailyCostCents × days per allocation.
Fix: Removed redundant second db.project.findMany call — allProjects already had budgetCents.
2026-03-09 | Performance | batchImportSkillMatrices N+1 pattern
Problem: 1 findUnique + 1 update per resource = O(2n) sequential queries.
Fix: Single findMany({ where: { eid: { in: eids } } }) + $transaction([...updates]) = 2 round-trips total.
2026-03-09 | Performance | recomputeValueScores sequential updates
Problem: Sequential await ctx.db.resource.update(...) in for-loop.
Fix: Build array of Prisma operations, then $transaction(updates) for single round-trip.
2026-03-09 | Performance | AuditLog extra findUnique in resource.create
Problem: findUnique({ where: { email } }) to get userId already available as ctx.dbUser?.id.
Fix: Use ctx.dbUser?.id directly.
2026-03-09 | UX/DX | Allocation router resource select missing lcrCents
AllocationWithDetails shared type declared resource.lcrCents but the Prisma select in allocation.ts only fetched { id, displayName, eid }. The TS error appeared in AllocationPopover.tsx when trying to use lcrCents. Fix: add lcrCents: true to every resource select in the allocation router. Lesson: When shared types include more fields than the Prisma select, TypeScript will catch it at the usage site (not definition), which can be confusing.
2026-03-08 | UX/DX | getSkillsAnalytics returns object, not array
trpc.resource.getSkillsAnalytics returns { totalResources, totalSkillEntries, aggregated, categories, allChapters } — not a flat array. Usage in SkillTagInput must use data?.aggregated to get the { skill, category, count }[] list.
2026-03-08 | Focus Trap | useFocusTrap hook pattern
For modal focus trapping: create a panelRef = useRef<HTMLDivElement>(null), call useFocusTrap(panelRef, true), and add onKeyDown={(e) => { if (e.key === "Escape") onClose(); }} to the inner panel div. The hook queries for all focusable elements on open and wraps Tab/Shift+Tab at the boundaries. Does NOT need to be applied to the overlay div — only the inner panel.
2026-03-05 | Setup | Prisma Client nach Schema-Änderung nicht aktuell
Problem: ctx.db.role war undefined obwohl das Role-Model in schema.prisma definiert war.
Lösung: prisma generate regeneriert den Client, aber der Next.js Dev-Server cached die alte Version. Lösung: .next/-Verzeichnis löschen und Dev-Server neu starten.
Für künftige Projekte: Nach Schema-Änderungen immer rm -rf apps/web/.next + pnpm dev neu starten.
2026-03-05 | Architektur | Nullable FK bricht Prisma-Typen
Problem: Allocation.resourceId wurde nullable gemacht (für Platzhalter). Prisma typisiert dann resource immer als T | null, auch wenn man mit isPlaceholder: false filtert.
Lösung: An allen Stellen, die a.resource verwenden, optional chaining (a.resource?.id) oder Null-Guards (if (!a.resource) continue) einbauen. Dashboard-Queries bekamen isPlaceholder: false im where-Clause.
Für künftige Projekte: Nullable FKs immer vollständig durch den Stack propagieren – TypeScript erzwingt das ohnehin.
2026-03-05 | TypeScript | exactOptionalPropertyTypes und optionale Felder
Problem: Mit exactOptionalPropertyTypes: true kann man { field: undefined } nicht an Funktionen übergeben, die field?: string erwarten.
Lösung: Entweder das Feld weglassen (Spread-Pattern: { ...(cond ? { field: val } : {}) }) oder den Record ohne das Feld neu aufbauen (const { field: _r, ...rest } = obj).
Für künftige Projekte: Bei optionalen Feldern immer Spread-Conditional statt explizit undefined setzen.
2026-03-05 | Architektur | Prisma include: undefined mit exactOptionalPropertyTypes
Problem: Konditionaler include-Parameter (include: condition ? {...} : undefined) wird von Prisma mit exactOptionalPropertyTypes abgelehnt.
Lösung: Zwei separate Query-Aufrufe mit vollständiger Typsicherheit oder Spread-Pattern auf Query-Objekt-Ebene: ctx.db.resource.findMany({ ...baseQuery, ...(cond ? { include: {...} } : {}) }).
2026-03-05 | Build | MCP-Server im falschen Projektpfad registriert
Problem: claude mcp add wurde aus einem Unterverzeichnis (packages/db) heraus ausgeführt. Die Server wurden unter dem Unterverzeichnis-Pfad registriert, nicht unter dem Projekt-Root.
Lösung: MCP-Server-Einträge manuell in ~/.claude.json in den richtigen Projekt-Pfad (/home/hartmut/Documents/Copilot/capakraken) verschieben.
Für künftige Projekte: claude mcp add immer vom Projekt-Root aus ausführen.
2026-03-05 | UI | Sticky-Label-Transparenz in der Timeline
Problem: Beim horizontalen Scrollen in der Timeline schienen Balken durch die sticky linken Spalten-Labels hindurch. Ursache: bg-amber-50/40 (40% transparent) und dark:bg-emerald-950/60 (60% transparent im Dark Mode).
Lösung: Alle sticky Label-Cells bekommen vollständig opake Hintergründe. Transparenz-Modifier (/40, /60) aus den sticky Elementen entfernt.
Regel: Sticky-positionierte Elemente müssen immer opake Hintergründe haben.
2026-02-xx | Architektur | tRPC-Routen-Registrierung
Entscheidung: Jeder neue Router wird in packages/api/src/router/index.ts registriert.
Muster: roleRouter als role: registriert → Frontend nutzt trpc.role.list.useQuery().
Achtung: trpc.role.list gibt ein Array zurück, kein { roles: [] } Objekt.
2026-02-xx | Architektur | Zod-Schema-Tiefe und tRPC-Typen
Problem: TS2589: Type instantiation is excessively deep bei BlueprintFieldEditor.tsx – tRPC leitet Typen rekursiv ab.
Lösung: Ist ein bekannter Pre-existing-Error durch zu tiefe Zod-Schema-Verschachtelung. Separate Mutations wie updateRolePresets (statt in update einzubauen) umgehen das Problem.
Für künftige Projekte: Bei tRPC-Schemas .refine() nie vor .partial() anwenden; komplexe Schemas in separate Procedures auslagern.
2026-02-xx | Architektur | Prisma-Enum vs. Shared-Enum
Problem: Prisma generiert eigene Enum-Typen, die TypeScript-seitig nicht mit den @capakraken/shared-Enums kompatibel sind.
Lösung: An Client-Grenzen as unknown as SharedType casten:
project as unknown as Projectform.orderType as unknown as OrderTyperesource.skills as unknown as SkillEntry[]
2026-02-xx | Architektur | SSE statt WebSockets
Entscheidung: Server-Sent Events (SSE) für Realtime-Updates, kein WebSocket. Begründung: Simpler zu implementieren, keine Bidirektionalität nötig, funktioniert hinter Standard-HTTP-Proxies. Trade-off: Nur Server→Client-Push; Client-initiierte Updates laufen weiter über tRPC-Mutations.
2026-03-06 | Refactoring | 1863-Zeilen-Komponente aufteilen
Problem: TimelineView.tsx wuchs auf 1863 Zeilen – schwer wartbar, kaum testbar.
Lösung: Schrittweise Extraktion:
- Konstanten →
timelineConstants.ts(keine State-Abhängigkeit) - Heatmap-Utilities →
heatmapUtils.ts - Layout-Berechnungen →
useTimelineLayout.tsxHook - Header-JSX →
TimelineHeader.tsx - Toolbar-JSX →
TimelineToolbar.tsxErgebnis: TimelineView.tsx von 1863 → 1597 Zeilen, 0 neue TS-Fehler. Nicht extrahiert: Render-Funktionen (renderAllocBlocks etc.) – diese schließen über zu viele State-Variablen und brauchen eine Context-Lösung in einem separaten Schritt.
2026-03-06 | Architektur | Redis Pub/Sub für SSE
Problem: SSE Event-Bus war ein In-Memory-Singleton, funktioniert nicht bei mehreren Server-Instanzen.
Lösung: ioredis in @capakraken/api hinzugefügt. Publisher schreibt Events in Redis-Channel capakraken:sse, Subscriber auf jeder Instanz empfängt und liefert lokal aus. Graceful Degradation: bei Redis-Ausfall weiterhin lokale Delivery.
Import-Pattern: import { Redis } from "ioredis" (named export, nicht default) – notwendig mit moduleResolution: NodeNext + ioredis v5.
Offene Frage: In Dev-Umgebung reicht lokale Delivery; Redis läuft auf Port 6380 via Docker Compose.
2026-03-06 | Auth | Argon2 statt SHA-256
Problem: SHA-256 ohne Salt ist dev-only und nicht produktionstauglich.
Lösung: @node-rs/argon2 als Drop-in für Auth.js v5 Credentials Provider. verify(hash, password) ersetzt den SHA-256-Vergleich. Seed nutzt await hash("password") (Salt wird automatisch generiert).
Seed-Bug entdeckt: prisma.vacation.deleteMany({}) fehlte im Cleanup-Block → FK-Constraint-Fehler. Behoben.
2026-03-06 | Performance | @tanstack/react-virtual für Timeline
Problem: Timeline renderte alle Resource-Rows auf einmal (auch off-screen).
Lösung: useVirtualizer mit getScrollElement: () => scrollContainerRef.current. Virtualisierter Container hat position: relative; height: getTotalSize(), jede Row position: absolute; transform: translateY(...).
Sticky Labels: Funktionieren trotz position: absolute – Sticky arbeitet gegen den horizontalen Scroll-Container (nicht den vertikalen).
Nicht virtualisiert: Project-View (typisch 20-40 Gruppen, kein Performance-Problem).
2026-03-06 | Build | Laufender Dev-Server überlebt Prisma-Schema-Änderung nicht
Problem: Nach prisma generate + rm -rf .next durch einen Hintergrund-Agenten lief der Dev-Server noch mit dem alten Node.js-Prozess. Der gecachte Prisma-Client kannte das neue Notification-Model nicht → ctx.db.notification war undefined → 500 Internal Server Error auf allen Seiten die den neuen Router nutzten (NotificationBell pollt beim Start).
Root Cause: rm -rf .next löscht den Build-Cache für neue Requests. Aber der laufende Node.js-Prozess hat den alten PrismaClient bereits im Heap. Dieser wird erst bei Prozess-Neustart neu initialisiert.
Lösung: Dev-Server-Prozess töten und neu starten: fuser -k 3100/tcp && pnpm dev
Regel: Nach jeder Prisma-Schema-Änderung die durch einen Agenten im Hintergrund gemacht wird: Dev-Server immer manuell neu starten. Symptom: 500 auf allen Seiten obwohl tsc sauber ist.
2026-03-06 | Playwright | Falsche API für page.click() mit hasText
Problem: page.click("button", { hasText: "..." }) ist keine valide Playwright-API.
Lösung: page.locator("button", { hasText: "..." }).click() – zuerst Locator erstellen, dann .click() aufrufen.
2026-03-06 | Role-Migration | Namens-Inkonsistenz bei Legacy-Daten
Problem: 61 von 133 Allocations mit role-String konnten nicht auf roleId gemappt werden (z.B. "Unreal Dev" vs. "Unreal Developer"). Ursache: Seed wurde überarbeitet, alte Allocation-Daten nicht migriert.
Lösung: Migration-Script erstellt, das case-insensitiv matched. 72 Allocations korrekt backgefüllt. Für die 61 Unmatched bleibt der role-String als Fallback.
Für künftige Projekte: Bei Seed-Überarbeitungen auch Migrations-Script für bestehende Produktionsdaten vorbereiten.
2026-03-06 | Architektur | Parallele Agenten auf demselben File — Race Condition vermeidbar
Problem: P7.2 und P7.3 modifizierten beide useTimelineDrag.ts in parallel gestarteten Agenten.
Ergebnis: Kein Konflikt, weil P7.3 später abschloss und den von P7.2 bereits geänderten File las, bevor er seine eigenen Änderungen hinzufügte.
Risiko: Wenn beide Agenten exakt gleichzeitig fertig würden, könnte einer die Änderungen des anderen überschreiben.
Regel: Bei parallelen Agenten, die dieselbe Datei modifizieren könnten, immer prüfen ob die Änderungen unabhängig sind — oder den zweiten Agenten anweisen, den File nach Start des ersten Agenten zu lesen.
2026-03-06 | UX | Touch-Drag Disambiguierung
Pattern: touchStartRef = useRef({ x, y, decided }) — beim ersten Touchstart Position merken. Im touchmove: wenn dy > dx und noch nicht decided → vertical scroll, kein Drag. Wenn dx > dy → horizontal drag, decided = true, preventDefault().
Wichtig: e.changedTouches[0] statt e.touches[0] bei touchend verwenden (touches ist bei touchend leer).
iOS Safari: touch-action: none (CSS) oder style={{ touchAction: "none" }} auf dem Canvas-Div verhindert Browser-Scroll-Interception.
2026-03-06 | Architektur | Undo/Redo ohne Hook-Kopplung
Pattern: Optionaler onAllocationMoved?: (snapshot) => void Callback in useTimelineDrag, der im onSuccess der Mutation aufgerufen wird. Konsumenten können History-Stack führen ohne den Hook zu kennen.
Vorteil: Klar getrennte Verantwortlichkeiten — der Drag-Hook kennt keine History-Logik.
Keyboard-Shortcut: Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y im useEffect mit window.addEventListener("keydown", ...).
2026-03-06 | Auth | Prisma upsert mit update: {} updatet Passwort nicht
Problem: prisma.user.upsert({ update: {} }) lässt bestehende User-Records unverändert. Nach einer Passwort-Hash-Migration (SHA-256 → Argon2) behielten die Seed-User ihre alten SHA-256-Hashes. verify(sha256hash, password) warf eine Exception ("Invalid hashed password: password hash string missing field"), was NextAuth als error=Configuration surfacete — Login unmöglich.
Symptom: DB passwordHash hatte Länge 64 (SHA-256 Hex), kein $argon2id$-Prefix.
Lösung: Im Seed alle drei User-Hash-Variablen vorher awaiten und in beide Blöcke (create und update) einsetzen:
const adminHash = await hash("admin123");
prisma.user.upsert({ where: ..., update: { passwordHash: adminHash }, create: { ..., passwordHash: adminHash } });
Für künftige Projekte: Prisma upsert mit update: {} stellt sicher, dass ein Record existiert, updated ihn aber nie. Bei Auth-Migrations immer update-Block befüllen.
2026-03-06 | Feature | Multi-Format-Export über einen Route Handler
Pattern: Einen Route Handler (/api/reports/allocations) für mehrere Exportformate nutzen via ?format=xlsx|pdf. Die Datenbankabfrage wird einmal ausgeführt, danach teilt ein if (format === "xlsx") Block die Verarbeitung auf.
XLS-Generierung server-seitig: xlsx Package (bereits im Projekt für Import vorhanden) — XLSX.utils.json_to_sheet(data) + XLSX.utils.book_new() + XLSX.write(wb, { type: "buffer", bookType: "xlsx" }) gibt einen Buffer zurück.
MIME-Type XLS: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + filename="*.xlsx".
UI: <a href="...?format=xlsx" download> — download Attribut erzwingt Download statt Browser-Öffnung.
Wiederverwendung: Da xlsx bereits für den Import installiert ist, kein neues Package nötig. Import als import * as XLSX from "xlsx".
2026-03-06 | Next.js | "use client" in @react-pdf/renderer Komponente bricht renderToBuffer
Problem: AllocationReport.tsx hatte "use client" am Anfang. Wenn diese Komponente in einem API Route Handler (Server Context / RSC) importiert und mit renderToBuffer gerendert wird, wickelt Next.js sie in einen __nextjs-internal-proxy.mjs-Wrapper. renderToBuffer bekommt nicht das echte React Component sondern den Proxy → 500 Internal Server Error.
Fehlersymptom: at eval (...AllocationReport.tsx/__nextjs-internal-proxy.mjs:1:1) im Stack Trace.
Lösung: "use client" aus AllocationReport.tsx entfernen. @react-pdf/renderer (Document, Page, Text, View) ist eine rein server-seitige Bibliothek — sie verwendet keine Browser-APIs, kein DOM, kein window. renderToBuffer läuft im Node.js-Prozess. Server Components sind hier korrekt.
Regel: Komponenten für @react-pdf/renderer sind immer Server Components. "use client" dort ist falsch und bricht renderToBuffer in Route Handlers.
Merkhilfe: "use client" nur dort, wo echte Browser-APIs (useState, useEffect, DOM-Events, window, localStorage) gebraucht werden. Reine Darstellungskomponenten für PDF/Email/Export sind Server Components.
2026-03-06 | Seed | Seed löscht Daten ohne sie neu anzulegen → stille Datenverluste
Problem: seed.ts führt prisma.vacation.deleteMany({}) im Cleanup-Block aus, aber erstellt danach keine Vacation-Einträge. Jedes Re-Seeding (z.B. nach Passwort-Hash-Migration) löscht alle Vacations dauerhaft. Die App zeigt danach leere Listen, ohne Fehlermeldung — "No vacations found" ist korrekt aber irreführend.
Symptom: Vacation-Tab zeigt "No vacations found" obwohl die API 200 zurückgibt.
Lösung: Vacation-Seed-Daten in seed.ts hinzugefügt (10 Einträge: ANNUAL, SICK, PUBLIC_HOLIDAY, OTHER; Status APPROVED + PENDING). Seed gibt jetzt Vacations: N created aus.
Regel: Jedes Model das im deleteMany-Cleanup-Block steht, muss auch im Seed-Daten-Block befüllt werden. Nach jedem Seed-Lauf: alle Models auf erwartete Counts prüfen.
Für künftige Projekte: Nach db:seed immer spot-check: prisma.vacation.count(), prisma.allocation.count() etc. — nicht nur "Seed complete!" als Erfolg werten.
2026-03-06 | Next-Auth | useSession ohne SessionProvider → Runtime Error
Problem: useSession() aus next-auth/react wirft einen Runtime Error ("useSession must be wrapped in a SessionProvider"), wenn kein <SessionProvider> im React-Baum existiert. Der Agent nutzte useSession() in AppShell.tsx und usePermissions.ts, obwohl das Root-Layout keinen SessionProvider enthielt.
Symptom: 500 Internal Server Error auf allen App-Seiten nach Login — dieselbe Oberfläche wie beim Stale-Prisma-Client-Bug.
Lösung (zweistufig):
SessionProvidereinmalig inTRPCProvider(apps/web/src/lib/trpc/provider.tsx) einbauen — zentraler Ort, funktioniert für alleuseSession()-Aufrufe in der App.AppShell.tsx:useSession()entfernt,userRolestattdessen als Prop vom Server-Component-Layout durchgereicht (sauberer, kein Client-Context nötig für diesen Fall). Regel: VoruseSession()immer prüfen obSessionProviderim Baum liegt. In Next.js App Router:SessionProvidergehört in ein Client-Component (z.B. Provider-Wrapper), nicht direkt ins Server-Layout. Für künftige Projekte: Wernext-auth/react-Hooks nutzt, muss sicherstellen dassSessionProvidergenau einmal inapps/web/src/lib/trpc/provider.tsxoder einem dediziertenProviders.tsxClient-Component vorhanden ist.
2026-03-06 | Architektur | Granulares RBAC-System: Permission-Override-Muster
Kontext: CapaKraken hatte 3 hartkodierte Procedure-Levels (protectedProcedure → managerProcedure → adminProcedure) ohne Granularität. Ziel: neue Rolle CONTROLLER + individuelle Permission-Overrides pro User.
Lösung: Zweigeteiltes System:
ROLE_DEFAULT_PERMISSIONS— statische Lookup-Tabelle: jede SystemRole hat eine Default-Menge an PermissionKeys.permissionOverrides: Json?auf dem User-Model (war bereits vorhanden, aber ungenutzt) —{ granted: [], denied: [], chapterIds: [] }für individuelle Anpassungen.resolvePermissions(role, overrides)— gibtSet<PermissionKey>zurück, wendet grants/denials auf die Rolle-Defaults an.requirePermission(ctx, key)— wirftTRPCError FORBIDDENwenn Permission fehlt.controllerProcedure— neues Procedure-Level für ADMIN+MANAGER+CONTROLLER (lesend + Export).
Neue Dateien: packages/shared/src/types/permissions.ts
Geändert: packages/api/src/trpc.ts, packages/api/src/router/user.ts (+ 3 neue Procedures), 6 Router mit Permission-Checks, AppShell.tsx, usePermissions.ts Hook, 4 UI-Komponenten.
Parallelisierungserfolg: 4 Wellen, je 2-3 parallele Agenten. Welle 1 (Shared+Prisma) → Welle 2 (Middleware+AppShell) → Welle 3 (Router+AdminPage+UI) → Welle 4 (Users-Page). Kein Konflikt, da saubere Datei-Grenzen.
Pre-existing Bug gefunden: packages/shared/src/schemas/project.schema.ts Zeile 5: crypto.randomUUID() ist nicht im lib: ["ES2022"] ohne DOM — hat nil Auswirkung auf Laufzeit (Node 18+ hat crypto global), aber tsc --noEmit auf shared direkt schlägt fehl. Fixbar mit import { randomUUID } from "node:crypto".
2026-03-06 | Architektur | permissionOverrides als JSON statt eigenem Modell
Entscheidung: Permission-Overrides als Json? auf User-Modell statt separatem Permission/RolePermission-Datenbankmodell.
Begründung: Kein db:push mit Tabellenmigrationen, keine Join-Queries, direkt serialisierbar in JWT/Session. Für teams < 100 User ausreichend.
Vorteil: Prisma.DbNull setzt JSON-Feld auf NULL (kein undefined übergeben).
Trade-off: Keine DB-level Constraints auf Permission-Keys — Validierung liegt komplett in der API-Middleware.
Für künftige Projekte: JSON-Overrides bis ~100 User ideal; bei größeren Systemen separate Tabellen für auditierbare History.
Offene Fragen
- Wie skalieren wir den SSE Event-Bus bei mehreren Server-Instanzen? → P7.1 umgesetzt (Redis Pub/Sub)
- Playwright E2E-Tests sind eingerichtet aber noch nicht befüllt → P5.4 umgesetzt (auth, resources, timeline, projects)
- P7.2 Touch-Support → umgesetzt
- P7.3 Undo/Redo → umgesetzt
- GitHub MCP braucht einen
GITHUB_TOKENin~/.claude.json– noch nicht gesetzt.
2026-03-07 | Architektur | Resource Value Score – kontext-freie Composite-Metrik
Problem: Staffing-Scorer in skill-matcher.ts ist projekt-kontextabhängig (requiredSkills, budget). Ein persistenter "Price/Quality"-Score pro Resource brauchte eine neue, kontext-freie Berechnung.
Lösung: Neues Pure-Function-Modul packages/staffing/src/value-scorer.ts mit computeValueScore(). 5 Dimensionen (skillDepth, skillBreadth, costEfficiency, chargeability, experience) werden gewichtet summiert. Score wird asynchron via recomputeValueScores in DB persistiert (nicht live berechnet).
Pattern: JSONB-Breakdown (valueScoreBreakdown) direkt auf Resource speichern → kein N+1 bei List-Queries. Sichtbarkeit per scoreVisibleRoles in SystemSettings konfigurierbar.
Tiebreaker: In staffing.ts getSuggestions sort-Tiebreaker: wenn zwei Resourcen ≤2 Punkte auseinander, bevorzuge höheren valueScore.
Tests: 8 neue Unit-Tests in value-scorer.test.ts (edge cases: empty skills, maxLcrCents=0, clamp 0-100).
2026-03-07 | DevOps | Dev-Server nach prisma generate immer neu starten
Problem: Nach prisma generate + rm -rf .next/ lieferte der laufende Next.js Dev-Server für ALLE Seiten HTTP 500 — kein tRPC-Fehler, sondern ein globaler Absturz.
Ursache: Node.js cached geladene Module im Speicher. Der laufende Prozess hatte den alten Prisma-Client geladen; nach prisma generate überschrieb das neue Client-JS die Dateien in node_modules, aber der Prozess nutzte noch den alten In-Memory-Cache. Zusammen mit einem geleerten .next/-Verzeichnis führte dies zu einem inkonsistenten Zustand.
Lösung: Dev-Server nach jeder prisma generate-Ausführung neu starten (kill + pnpm dev).
Merkregel: db:push → .next/ löschen → Dev-Server neu starten (immer alle drei Schritte zusammen).
2026-03-11 | UI/UX | Universal Table Sorting + Drag-and-Drop Row Reordering + Persistent View State
Problem: Column sort was only on the Resources page; no drag-to-reorder rows; view state (sort + row order) not persisted per user. Lösung:
useTableSorterweitert mitoptions.initialField/Dir+options.onSortChangecallback.isFirstRenderref verhindert, dass die erste Render-Runde einen save auslöst.useViewPrefs(view)neuer Hook: liest/schreibtviewprefs_<view>localStorage (getrennt voncolvis_<view>des bestehendenuseColumnConfig). Server-sync via debounced (600ms)trpc.user.setColumnPreferencesmit merge-Logik (null=clear, undefined=keep, value=set).useRowOrderneuer Hook: gibtorderedRowszurück. WennactiveSortField !== null→ sort gewinnt, rowOrder wird ignoriert. Drag aktiviert manuelle Reihenfolge + resettet sort.DraggableTableRowneue Komponente:<tr>mit erstem<td>als Drag-Handle (⠿).dragRef(shared ref) hält die aktuell gezogene Row-ID.onDrop(draggedId)gibt die gezogene ID weiter (nicht die Ziel-ID) — Pattern:onDrop={(draggedId) => reorder(draggedId, currentRow.id)}.- Per-User-Isolation: Alle Prefs (sort + rowOrder) landen im
User.columnPreferencesJSONB keyed by User-ID — vollständige Isolation zwischen Benutzern. - Blueprints ausgelassen: Card-Grid-Layout, keine Tabelle → sorting/drag nicht anwendbar.
- exactOptionalPropertyTypes-Falle:
writeLocal(view, { sort: sort ?? undefined })ist ungültig. Fix: Funktionssignatur auf{ sort?: SavedSort | null; rowOrder?: string[] | null }ändern, dann{ sort: sort ?? null }übergeben.nullwird dann perif (next.sort == null) delete next.sortbereinigt. - DraggableTableRow
onDropSemantik-Bug: Initial wurdeonDrop(id)mit der Ziel-Row-ID aufgerufen →reorder(project.id, project.id)war ein No-Op. Fix:onDrop(dragRef.current)übergibt die GEZOGENE ID; Prop-Vertrag entsprechend angepasst.
2026-03-12 | Dashboard | Shared widget contracts + persisted layout normalization
Problem: Dashboard layout was persisted as unchecked JSON in User.dashboardLayout. The web layer still rendered widgets via a manual switch, and AddWidgetModal created y: Infinity, which became null after JSON serialization and left persisted layouts invalid.
Lösung:
- Canonical shared contract: added
packages/shared/src/types/dashboard.tsfor widget types, catalog metadata, persisted layout shape, and default config values. - Schema + migration path: added
packages/shared/src/schemas/dashboard.schema.tswithnormalizeDashboardLayout(),createDashboardWidget(),createDefaultDashboardLayout(), and per-widget config schemas. Invalid persisted values are repaired on load/save instead of crashing or drifting. - API normalization:
packages/api/src/router/user.tsnow validatessaveDashboardLayoutinput through the shared dashboard schema and normalizes DB reads before returning them. - Registry-driven rendering:
DashboardClientnow renders widgets from a registry inwidget-registry.tsrather than a hardcoded switch. Widget metadata is sourced from the shared catalog. - Bug fix: new widgets are now appended at
getNextDashboardWidgetY(existingWidgets)rather than usingInfinity, so persisted layouts remain JSON-safe. - Regression coverage: added
packages/shared/src/__tests__/dashboard-layout.test.tsfor default fallback, invalid-coordinate repair, duplicate-ID normalization, and next-row calculation. TypeScript note:exactOptionalPropertyTypesrequired building option objects with conditional spreads rather than passing{ title: undefined }into helper APIs. This matters for any future shared normalizer helpers.