rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled

rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)

Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
This commit was merged in pull request #61.
This commit is contained in:
2026-05-21 16:28:40 +02:00
committed by Hartmut
parent d9a7ec0338
commit b41c1d2501
943 changed files with 24548 additions and 16832 deletions
+69 -18
View File
@@ -1,6 +1,7 @@
# CapaKraken Projekt-Learnings
# Nexus Projekt-Learnings
## Format
**Datum | Kategorie | Problem → Lösung**
---
@@ -12,11 +13,13 @@
**Problem:** Auth.js `authorize()` callback uses `@node-rs/argon2` (native module, not Edge-compatible). Using `auth()` directly in `middleware.ts` would pull argon2 into the Edge bundle and crash.
**Solution — split config pattern:**
- `auth.config.ts` — edge-safe subset: `pages`, `session`, `cookies`, no providers, no callbacks that touch DB or argon2
- `auth-edge.ts``NextAuth(authConfig)` with the lean config; used only by middleware
- `auth.ts` — spreads `authConfig`, adds Credentials provider + argon2 callbacks + prisma session tracking
**Middleware wrapping:**
```ts
import { auth } from "./server/auth-edge.js";
export default auth(function middleware(request) {
@@ -28,17 +31,19 @@ export default auth(function middleware(request) {
```
**Three-layer defence:**
1. Middleware — server-side redirect before page renders
2. `SessionGuard` client component — `useSession()``router.replace()` on SPA navigation
3. `QueryCache` / `MutationCache` in TRPCProvider — UNAUTHORIZED tRPC errors → `window.location.replace()`
**Test mock pattern for middleware tests:**
```ts
vi.mock("./server/auth-edge.js", () => ({
auth: (handler) => (req) =>
handler(Object.assign(req, { auth: { user: { id: "test-user" } } })),
auth: (handler) => (req) => handler(Object.assign(req, { auth: { user: { id: "test-user" } } })),
}));
```
Needed because `vi.resetModules()` inside the helper function doesn't re-apply top-level mocks — always declare `vi.mock(...)` at file scope.
---
@@ -50,12 +55,14 @@ Needed because `vi.resetModules()` inside the helper function doesn't re-apply t
**Repo path:** `Hartmut/plANARCHY`
Usage example (list open issues):
```bash
curl -s -H "Authorization: token $(cat ~/.gitea-token)" \
"https://gitea.hartmut-noerenberg.com/api/v1/repos/Hartmut/plANARCHY/issues?state=open&type=issues&limit=50"
```
Close an issue with a comment:
```bash
TOKEN=$(cat ~/.gitea-token)
REPO="Hartmut/plANARCHY"
@@ -75,18 +82,22 @@ curl -s -X PATCH -H "Authorization: token $TOKEN" -H "Content-Type: application/
**Problem:** After adding a new column to `schema.prisma` and running `prisma generate` on the host, the running Docker app container still used the old Prisma client (the container's `node_modules` is a named Docker volume, isolated from the host filesystem). Queries referencing the new field (`isActive`) failed at runtime, causing tRPC procedures to return errors.
**Solution:** Always restart the app container after Prisma schema changes:
```
docker compose --profile full restart app
```
The startup script `tooling/docker/app-dev-start.sh` already runs `prisma generate` + `db:migrate:deploy` on every container start — so a restart is sufficient. No rebuild needed unless `pnpm-lock.yaml` or `Dockerfile.dev` changed.
**Rule:** Prisma schema change checklist:
1. Edit `packages/db/prisma/schema.prisma`
2. Write migration SQL in `packages/db/prisma/migrations/<timestamp>_<name>/migration.sql`
3. Apply migration to the running DB directly (for dev speed): `docker exec capakraken-postgres-1 psql -U capakraken -d capakraken < migration.sql`
3. Apply migration to the running DB directly (for dev speed): `docker exec nexus-postgres-1 psql -U nexus -d nexus < migration.sql`
4. `docker compose --profile full restart app` — regenerates Prisma client + runs migrations inside the container
### 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)`.
@@ -95,24 +106,28 @@ The startup script `tooling/docker/app-dev-start.sh` already runs `prisma genera
- React Query v5 (tRPC v11): `keepPreviousData` removed, use `placeholderData: (prev) => prev` instead.
### 2026-03-12 | UX/DX | Deep tRPC mutation inference in large client files
- `BlueprintsClient.tsx` hit `TS2589` when multiple `trpc.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 explicit `mutateAsync()` 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 (`SortableColumnHeader` currently exposes `string` fields).
### 2026-03-12 | Build | NextAuth portable export typing
- `export const { handlers, auth, signIn, signOut } = NextAuth(...)` triggered `TS2742` because the inferred `signIn` type captured provider internals from `@auth/core`.
- If the server-side `signIn`/`signOut` exports are unused, export only `handlers` and `auth`. Also prefer a named `authConfig satisfies NextAuthConfig` object for clearer config typing.
### 2026-03-11 | Architecture | Phase 1: Application Layer Extraction
- Created `packages/application` with `createAllocation` and `fillPlaceholder` use-case services
- `packages/api` router procedures now delegate to use cases; they only check permissions and emit SSE events
- `packages/application` depends on `@capakraken/db`, `@capakraken/engine`, `@capakraken/shared`; `packages/api` depends on `@capakraken/application`
- `packages/application` depends on `@nexus/db`, `@nexus/engine`, `@nexus/shared`; `packages/api` depends on `@nexus/application`
- Use cases throw `TRPCError` directly (pragmatic — project only uses tRPC transport)
- `Prisma.AllocationGetPayload<{ include: ... }>` used for precise return type in use cases
- `exactOptionalPropertyTypes` + optional params: caller must use spread `...(val !== undefined ? { key: val } : {})` when passing zod inputs to use cases with `{ key?: T }` interfaces
- `fillPlaceholder` returns `{ filled, decrementedPlaceholder? }` — UI `onSuccess` callbacks 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.ts` into `packages/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.
@@ -120,12 +135,14 @@ The startup script `tooling/docker/app-dev-start.sh` already runs `prisma genera
- While extracting `getDemand`, fix the chapter grouping bug where `resourceCount` was always `0`; 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`.
- Shared estimate enums/types/schemas now live in `@nexus/shared`, and initial application commands/queries (`createEstimate`, `listEstimates`, `getEstimateById`) live in `@nexus/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 into `resource.update` by fetching the blueprint's fieldDefs and calling `validateCustomFields()` before saving. Throw `TRPCError({ code: "UNPROCESSABLE_CONTENT" })` on error.
- Batch JSONB merge (without overwriting other keys): use `$executeRaw` with PostgreSQL's `||` JSONB merge operator: `UPDATE "Resource" SET "dynamicFields" = "dynamicFields" || ${JSON.stringify(fields)}::jsonb WHERE id = ${id}`. Cannot use Prisma `update()` 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, then `onDrop` calls the `reorder()` function.
@@ -134,45 +151,55 @@ The startup script `tooling/docker/app-dev-start.sh` already runs `prisma genera
- 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 as `any[]` array and spread as `AND: cfConditions` — avoids Prisma union type issues.
- `flatMap` with multiple return types causes TS union inference that Prisma WHERE types reject. Use a `for` loop with `push` into an explicitly typed `any[]` instead.
- Next.js typed routes (`typedRoutes: true`) rejects dynamic URL strings even with `as unknown as RouteImpl`. Fix: cast the router itself with `useRouter() as unknown as { replace: (url: string, opts?) => void }` to escape the branded type system for dynamic URLs.
- `useSearchParams` requires `<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.create` was hashing passwords with SHA-256; `auth.ts` verifies with Argon2 → users created via admin couldn't log in. Fix: import `hash` from `@node-rs/argon2` in the router. Must also declare `@node-rs/argon2` in `packages/api/package.json` — being a dep of `@capakraken/db` is not enough for TS resolution.
- `user.create` was hashing passwords with SHA-256; `auth.ts` verifies with Argon2 → users created via admin couldn't log in. Fix: import `hash` from `@node-rs/argon2` in the router. Must also declare `@node-rs/argon2` in `packages/api/package.json` — being a dep of `@nexus/db` is not enough for TS resolution.
- `notification.create` was `protectedProcedure` → any logged-in user could create notifications for arbitrary users. Fix: changed to `managerProcedure`.
- `testAiConnection` always built Azure deployment URLs regardless of `aiProvider`. Fix: branch on provider, use `https://api.openai.com/v1/chat/completions` with `Authorization: Bearer` for OpenAI.
- `@capakraken/shared` had `test:unit: vitest run` in package.json but no test files → turbo failed. Fix: remove the script (tests live only in engine/staffing).
- `@nexus/shared` had `test:unit: vitest run` in package.json but no test files → turbo failed. Fix: remove the script (tests live only in engine/staffing).
- `crypto.randomUUID()` in `packages/shared/src/schemas/project.schema.ts` failed 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.
@@ -180,6 +207,7 @@ For modal focus trapping: create a `panelRef = useRef<HTMLDivElement>(null)`, ca
---
### 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.
@@ -187,6 +215,7 @@ For modal focus trapping: create a `panelRef = useRef<HTMLDivElement>(null)`, ca
---
### 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.
@@ -194,19 +223,22 @@ For modal focus trapping: create a `panelRef = useRef<HTMLDivElement>(null)`, ca
---
### 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.
**Lösung:** MCP-Server-Einträge manuell in `~/.claude.json` in den richtigen Projekt-Pfad (`/home/hartmut/Documents/Copilot/nexus`) 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.
@@ -214,6 +246,7 @@ For modal focus trapping: create a `panelRef = useRef<HTMLDivElement>(null)`, ca
---
### 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.
@@ -221,6 +254,7 @@ For modal focus trapping: create a `panelRef = useRef<HTMLDivElement>(null)`, ca
---
### 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.
@@ -228,8 +262,10 @@ For modal focus trapping: create a `panelRef = useRef<HTMLDivElement>(null)`, ca
---
### 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.
**Problem:** Prisma generiert eigene Enum-Typen, die TypeScript-seitig nicht mit den `@nexus/shared`-Enums kompatibel sind.
**Lösung:** An Client-Grenzen `as unknown as SharedType` casten:
- `project as unknown as Project`
- `form.orderType as unknown as OrderType`
- `resource.skills as unknown as SkillEntry[]`
@@ -237,6 +273,7 @@ For modal focus trapping: create a `panelRef = useRef<HTMLDivElement>(null)`, ca
---
### 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.
@@ -247,20 +284,21 @@ For modal focus trapping: create a `panelRef = useRef<HTMLDivElement>(null)`, ca
**Problem:** `TimelineView.tsx` wuchs auf 1863 Zeilen schwer wartbar, kaum testbar.
**Lösung:** Schrittweise Extraktion:
1. Konstanten → `timelineConstants.ts` (keine State-Abhängigkeit)
2. Heatmap-Utilities → `heatmapUtils.ts`
3. Layout-Berechnungen → `useTimelineLayout.tsx` Hook
4. Header-JSX → `TimelineHeader.tsx`
5. Toolbar-JSX → `TimelineToolbar.tsx`
**Ergebnis:** 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.
**Ergebnis:** 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.
**Lösung:** `ioredis` in `@nexus/api` hinzugefügt. Publisher schreibt Events in Redis-Channel `nexus: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.
@@ -340,10 +378,12 @@ For modal focus trapping: create a `panelRef = useRef<HTMLDivElement>(null)`, ca
**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:
```typescript
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.
---
@@ -383,18 +423,20 @@ prisma.user.upsert({ where: ..., update: { passwordHash: adminHash }, create: {
**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):**
1. `SessionProvider` einmalig in `TRPCProvider` (`apps/web/src/lib/trpc/provider.tsx`) einbauen — zentraler Ort, funktioniert für alle `useSession()`-Aufrufe in der App.
2. `AppShell.tsx`: `useSession()` entfernt, `userRole` stattdessen als Prop vom Server-Component-Layout durchgereicht (sauberer, kein Client-Context nötig für diesen Fall).
**Regel:** Vor `useSession()` immer prüfen ob `SessionProvider` im Baum liegt. In Next.js App Router: `SessionProvider` gehört in ein Client-Component (z.B. Provider-Wrapper), nicht direkt ins Server-Layout.
**Für künftige Projekte:** Wer `next-auth/react`-Hooks nutzt, muss sicherstellen dass `SessionProvider` genau einmal in `apps/web/src/lib/trpc/provider.tsx` oder einem dedizierten `Providers.tsx` Client-Component vorhanden ist.
**Regel:** Vor `useSession()` immer prüfen ob `SessionProvider` im Baum liegt. In Next.js App Router: `SessionProvider` gehört in ein Client-Component (z.B. Provider-Wrapper), nicht direkt ins Server-Layout.
**Für künftige Projekte:** Wer `next-auth/react`-Hooks nutzt, muss sicherstellen dass `SessionProvider` genau einmal in `apps/web/src/lib/trpc/provider.tsx` oder einem dedizierten `Providers.tsx` Client-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.
**Kontext:** Nexus hatte 3 hartkodierte Procedure-Levels (protectedProcedure → managerProcedure → adminProcedure) ohne Granularität. Ziel: neue Rolle CONTROLLER + individuelle Permission-Overrides pro User.
**Lösung:** Zweigeteiltes System:
1. **`ROLE_DEFAULT_PERMISSIONS`** — statische Lookup-Tabelle: jede SystemRole hat eine Default-Menge an PermissionKeys.
2. **`permissionOverrides: Json?`** auf dem User-Model (war bereits vorhanden, aber ungenutzt) — `{ granted: [], denied: [], chapterIds: [] }` für individuelle Anpassungen.
3. **`resolvePermissions(role, overrides)`** — gibt `Set<PermissionKey>` zurück, wendet grants/denials auf die Rolle-Defaults an.
@@ -421,6 +463,7 @@ prisma.user.upsert({ where: ..., update: { passwordHash: adminHash }, create: {
---
## Offene Fragen
- [x] Wie skalieren wir den SSE Event-Bus bei mehreren Server-Instanzen? → P7.1 umgesetzt (Redis Pub/Sub)
- [x] Playwright E2E-Tests sind eingerichtet aber noch nicht befüllt → P5.4 umgesetzt (auth, resources, timeline, projects)
- [x] P7.2 Touch-Support → umgesetzt
@@ -430,6 +473,7 @@ prisma.user.upsert({ where: ..., update: { passwordHash: adminHash }, create: {
---
### 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.
@@ -439,14 +483,17 @@ prisma.user.upsert({ where: ..., update: { passwordHash: adminHash }, create: {
---
### 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:**
- **`useTableSort` erweitert** mit `options.initialField/Dir` + `options.onSortChange` callback. `isFirstRender` ref verhindert, dass die erste Render-Runde einen save auslöst.
- **`useViewPrefs(view)`** neuer Hook: liest/schreibt `viewprefs_<view>` localStorage (getrennt von `colvis_<view>` des bestehenden `useColumnConfig`). Server-sync via debounced (600ms) `trpc.user.setColumnPreferences` mit merge-Logik (null=clear, undefined=keep, value=set).
- **`useRowOrder`** neuer Hook: gibt `orderedRows` zurück. Wenn `activeSortField !== null` → sort gewinnt, rowOrder wird ignoriert. Drag aktiviert manuelle Reihenfolge + resettet sort.
@@ -457,15 +504,17 @@ prisma.user.upsert({ where: ..., update: { passwordHash: adminHash }, create: {
- **DraggableTableRow `onDrop` Semantik-Bug:** Initial wurde `onDrop(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.ts` for widget types, catalog metadata, persisted layout shape, and default config values.
- **Schema + migration path:** added `packages/shared/src/schemas/dashboard.schema.ts` with `normalizeDashboardLayout()`, `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.ts` now validates `saveDashboardLayout` input through the shared dashboard schema and normalizes DB reads before returning them.
- **Registry-driven rendering:** `DashboardClient` now renders widgets from a registry in `widget-registry.ts` rather than a hardcoded switch. Widget metadata is sourced from the shared catalog.
- **Bug fix:** new widgets are now appended at `getNextDashboardWidgetY(existingWidgets)` rather than using `Infinity`, so persisted layouts remain JSON-safe.
- **Regression coverage:** added `packages/shared/src/__tests__/dashboard-layout.test.ts` for default fallback, invalid-coordinate repair, duplicate-ID normalization, and next-row calculation.
**TypeScript note:** `exactOptionalPropertyTypes` required building option objects with conditional spreads rather than passing `{ title: undefined }` into helper APIs. This matters for any future shared normalizer helpers.
**TypeScript note:** `exactOptionalPropertyTypes` required building option objects with conditional spreads rather than passing `{ title: undefined }` into helper APIs. This matters for any future shared normalizer helpers.
### 2026-04-01 | Architecture Decision | API Keys — no implementation without explicit product decision
@@ -474,11 +523,13 @@ prisma.user.upsert({ where: ..., update: { passwordHash: adminHash }, create: {
**Decision: No code is written until the product decision is made.**
The core trade-off is:
- **Short-lived JWTs (current approach):** Zero DB footprint, automatic expiry, no revocation surface. Works well for a single-tenant SaaS where all clients are browser sessions. No additional attack surface.
- **Long-lived API keys stored in DB:** Enables CLI tooling, CI/CD pipelines, and machine-to-machine workflows. Requires: secure token generation (crypto.randomBytes, bcrypt hash stored, raw key shown once), per-key scopes, revocation endpoint, key rotation policy, audit log for key usage. Significantly larger attack surface and ops burden.
- **Short-lived API tokens (OAuth-style):** Suitable if CapaKraken exposes a public API. Over-engineered for an internal tool with no current integration story.
- **Short-lived API tokens (OAuth-style):** Suitable if Nexus exposes a public API. Over-engineered for an internal tool with no current integration story.
**Engineering guidance for when the decision is made:**
1. Store only the SHA-256 or bcrypt hash of the key, never the raw token.
2. Enforce per-key scopes aligned with the `SystemRole` permission model.
3. Add `keyUsedAt` tracking and hard expiry via TTL field on the DB row.