From 8c9ba5363c318b47379358484cdfc68388beb242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Thu, 2 Apr 2026 21:34:15 +0200 Subject: [PATCH] docs: mark P1 timeline/SSE/scenario work complete in plan and roadmap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All Workstream C–F tasks completed: - C: drag+selection conflict fix (FloatingActionBar clears on drag start) - D: SSE edge-case tests (hide-during-reconnect, first-ever-failure) - E: scenario module unit tests — 31 tests across all 4 scenario modules - F: .env.example expanded, plan and roadmap updated Co-Authored-By: claude-flow --- docs/product-roadmap.md | 5 +- plan.md | 160 +++++++++++++--------------------------- 2 files changed, 55 insertions(+), 110 deletions(-) diff --git a/docs/product-roadmap.md b/docs/product-roadmap.md index cae5cd7..87f9cb3 100644 --- a/docs/product-roadmap.md +++ b/docs/product-roadmap.md @@ -29,6 +29,9 @@ The following items were proposed in older markdown files and are already implem - Shared dynamic-field filter and blueprint-validation parity across project and resource APIs - Blueprints list parity with shared sort, selection, and view-preference behavior - Shared dashboard widget contracts, layout normalization/migration, and registry-driven rendering +- Timeline drag+selection interaction stability: `FloatingActionBar` cleared synchronously when a project-bar or single-allocation drag begins, preventing committed multi-select state from bleeding into unrelated drag operations +- SSE reconnect edge-case coverage: hide-during-pending-reconnect and first-ever-connection-failure paths locked in with dedicated regression tests +- Scenario module unit regression: all four scenario modules (`scenario-shared`, `scenario-apply`, `scenario-baseline`, `scenario-simulation`) covered with dedicated test files (31 tests total) ## Active Workstreams @@ -37,7 +40,7 @@ The following items were proposed in older markdown files and are already implem | Estimating system | `Complete` | Full CRUD, versioning, export, planning handoff, clone/template, rate cards per client, richer version comparison, scope-to-effort rule engine, experience multipliers & shoring ratios, weekly phasing (4Dispo grid), and structured commercial terms. | — | | Demand vs assignment split | `Complete` | Legacy `Allocation` table dropped. `legacyAllocationId` columns removed. Migration tooling deleted. Compatibility facades renamed to clean domain names (`updateAllocationEntry`, `deleteAllocationEntry`, `fillOpenDemand`, `loadAllocationEntry`). `isPlaceholder` is a derived read-model property. No legacy compatibility naming remains. | — | | Widget platform refactor | `Implemented` | Widget config typing, layout normalization, registry-driven rendering, and dashboard query extraction now live behind shared/application contracts instead of ad hoc router logic. | Keep future widgets on the same registry + application-use-case pattern. | -| Package-level regression tests | `Expanded` | Shared schema validation (rate-card, allocation, estimate — 32 tests), engine vacation/recurrence (29 tests), staffing capacity-analyzer (12 tests), plus existing application and dashboard tests. | Continue adding API-level integration tests for remaining router procedures. | +| Package-level regression tests | `Expanded` | Shared schema validation (rate-card, allocation, estimate — 32 tests), engine vacation/recurrence (29 tests), staffing capacity-analyzer (12 tests), plus existing application and dashboard tests. Scenario module regression: 31 new unit tests across all four scenario modules (`scenario-shared`, `scenario-apply`, `scenario-baseline`, `scenario-simulation`). Timeline interaction: drag+selection conflict fix (FloatingActionBar cleared on project-bar and allocation drag start) + `FloatingActionBar` render regression suite. SSE stability: 2 additional edge-case tests for hide-during-reconnect and first-ever-connection-failure paths (10 tests total). | Continue adding API-level integration tests for remaining router procedures. | | Chargeability and resource planning (Dispo v2) | `Complete` | Plans 1-5 (schema, types, SAH engine, 5 API routers, 5 admin UIs, resource/project modal extensions), Phase A (live forecast report), Phase B (target comparison + drill-down grouping), Phase D (Excel/CSV export). Phase C (SAP actuals) removed from scope. | — | ## Prioritized Backlog diff --git a/plan.md b/plan.md index d1e2e78..2ba434d 100644 --- a/plan.md +++ b/plan.md @@ -1,138 +1,80 @@ -# Plan: Timeline Interaction Stability — P1 continuation +# Plan: Scenario Regression Depth + Housekeeping Stand: 2026-04-02 --- -# Workstream C: Drag + Selection Interaction Conflicts +# Workstream E: Scenario Regression Depth ## Anforderungsanalyse -Code-Review von `useTimelineDrag.ts` ergibt zwei tatsächliche Konflikte: +Vier Szenario-Module haben **keine direkten Unit-Tests**: -1. **`onProjectBarMouseDown`** (Zeile 488): Startet einen Projekt-Bar-Drag **ohne** das committed `multiSelectState` zurückzusetzen. → `FloatingActionBar` bleibt während des Drags sichtbar (da `selectedAllocationIds.length > 0` aus dem committed Multi-Select). +| Datei | Inhalt | Lücke | +|-------|--------|-------| +| `scenario-shared.ts` | Pure helpers: `roundToTenths`, `getScenarioAvailability`, `collectScenarioSkillSet`, `calculateScenarioEntryHours` | Keine Tests | +| `scenario-baseline.ts` | `readProjectScenarioBaseline` — lädt Assignments/Demands, berechnet Kosten/Stunden | Keine Tests | +| `scenario-apply.ts` | `applyProjectScenario` — CANCELLED/update/create Branches, appliedCount | Keine Tests | +| `scenario-simulation.ts` | `simulateProjectScenario` — Baseline vs. Szenario-Delta, Warnungen, Skill-Coverage | Keine Tests | -2. **`onAllocMouseDown` single-drag path** (Zeile 611, nach dem `if (isMultiSelected)` Block): Wenn eine Allocation per Mousedown angedraggt wird die NICHT in der Multi-Selektion ist, wird ebenfalls kein Reset ausgeführt. → `FloatingActionBar` bleibt sichtbar. - -3. **`onCanvasRightMouseDown`**: Startet neue Multi-Select-Session mit `selectedAllocationIds: []` — resettet Selektion korrekt. Kein Fix nötig. - -4. **ESC-Taste**: Bereits in `TimelineView.tsx` über `clearMultiSelect` abgehandelt. Kein Fix nötig. - -**Fix-Semantik**: Beide Handler brauchen ein **volles Reset** (`INITIAL_MULTI_SELECT`) — im Gegensatz zu `cancelTransientMultiSelectState`, das committed Selektionen bewahrt. Ein neuer, unabhängiger Drag soll die Selektion vollständig verwerfen. +Bestehende Coverage: `scenario-router.test.ts` (Auth-Guards), `scenario-procedure-support.test.ts` (Delegation), `assistant-tools-scenarios.test.ts` (1 Integration-Test) — alles Delegation, keine Business-Logik. ## Betroffene Pakete & Dateien | Paket | Datei | Art | |-------|-------|-----| -| `apps/web` | `src/hooks/useTimelineDrag.ts` | edit | -| `apps/web` | `src/components/timeline/FloatingActionBar.test.tsx` | create | +| `packages/api` | `src/__tests__/scenario-shared.test.ts` | create | +| `packages/api` | `src/__tests__/scenario-apply.test.ts` | create | +| `packages/api` | `src/__tests__/scenario-baseline.test.ts` | create | +| `packages/api` | `src/__tests__/scenario-simulation.test.ts` | create | ## Task-Liste -- [ ] **C-1:** `useTimelineDrag.ts` — Multi-Select bei Drag-Start zurücksetzen. - - In `onProjectBarMouseDown` (nach `if (e.button !== 0) return;`, vor `createProjectDragState`): - ```ts - multiSelectRef.current = INITIAL_MULTI_SELECT; - setMultiSelectState(INITIAL_MULTI_SELECT); - ``` - - In `onAllocMouseDown` im Single-Drag-Zweig (nach dem `if (isMultiSelected) { ... return; }` Block, vor `createAllocationDragState`): - ```ts - multiSelectRef.current = INITIAL_MULTI_SELECT; - setMultiSelectState(INITIAL_MULTI_SELECT); - ``` - - `setMultiSelectState` ist bereits von `useState` (stabil) — kein Eintrag in dep-Array nötig (Konvention konsistent mit bestehender `onAllocMouseDown`-Implementierung, die `setMultiSelectState` ebenfalls ohne Dep-Eintrag nutzt). - - Ref-Reset ist nötig damit nachfolgende synchrone Ref-Lesungen im selben Event-Cycle den gecleansten State sehen. - - → Datei: `apps/web/src/hooks/useTimelineDrag.ts` +- [x] **E-1a:** `scenario-shared.test.ts` — Pure-Helper-Tests (keine Mocks außer resource-capacity) + - `roundToTenths`: 0.15 → 0.2, 0.34 → 0.3, ganzer Integer bleibt + - `getScenarioAvailability`: null → DEFAULT_AVAILABILITY; valides Objekt wird durchgereicht + - `collectScenarioSkillSet`: null → leeres Set; leeres Array → leeres Set; doppelte Skills dedupliziert; lowercase-Normalisierung; leere Strings gefiltert; Mixed case dedupliziert + - `calculateScenarioEntryHours`: ohne resourceId → delegiert an `calculateAllocation`; mit resourceId → delegiert an `calculateEffectiveBookedHours` + - Mock: `@capakraken/engine/allocation` und `../lib/resource-capacity.js` -- [ ] **C-2:** `FloatingActionBar.test.tsx` erstellen — Regressions-Coverage. - - Test 1: `selectedAllocationCount=0, selectedResourceCount=0` → rendert `null` (Bar ist ausgeblendet) - - Test 2: `selectedAllocationCount=2, selectedResourceCount=0` → Bar sichtbar, Text "2 allocations selected" - - Test 3: `selectedAllocationCount=0, selectedResourceCount=1` → Bar sichtbar, Text "1 resource selected" - - Test 4: `selectedAllocationCount=1, selectedResourceCount=1` → Bar sichtbar, Text enthält "1 allocation" und "1 resource" - - Sichert den `totalCount === 0 → null`-Guard: wenn Multi-Select gecleart wird, verschwindet die Bar. - - → Datei: `apps/web/src/components/timeline/FloatingActionBar.test.tsx` +- [x] **E-1b:** `scenario-apply.test.ts` — CRUD-Branch-Tests + - NOT_FOUND wenn `project.findUnique` null zurückgibt + - `remove: true` + `assignmentId` → `assignment.update` mit `status: "CANCELLED"`, appliedCount = 0 (cancel trifft `continue` vor `created.push`) + - `assignmentId` ohne remove → `assignment.update` mit neuen Daten, appliedCount = 1 + - kein `assignmentId`, kein `resourceId` → Zeile wird übersprungen, appliedCount = 0 + - kein `assignmentId`, hat `resourceId` → `assignment.create` mit korrektem `dailyCostCents`, appliedCount = 1 + - Mehrere Changes → korrekter appliedCount summiert + +- [x] **E-1c:** `scenario-baseline.test.ts` — Baseline-Lade-Tests + - NOT_FOUND wenn Projekt nicht gefunden + - Leeres Projekt (keine Assignments, keine Demands) → `totalCostCents: 0`, `totalHours: 0`, `assignments: []`, `demands: []` + - Assignment mit bekanntem `lcrCents` und `hoursPerDay` → `costCents` korrekt berechnet + - CANCELLED Assignments werden herausgefiltert (kommen nicht in `baselineAllocations`) + - Demands werden korrekt gemappt (kein `costCents`, hat `headcount`, `roleName` aus roleEntity) + - `totalCostCents` ist Summe aller Assignment-`costCents` + +- [x] **E-1d:** `scenario-simulation.test.ts` — Simulations-Logik-Tests + - NOT_FOUND wenn Projekt nicht gefunden + - remove-Change → Assignment fehlt im Scenario-headcount (`delta.headcount < 0`) + - Neues Assignment hinzufügen → `delta.headcount > 0` + - Budget-Warnung wenn `scenarioCostCents > budgetCents` → warnings enthält "exceeds budget" + - Skill-Coverage: Szenario mit mehr Skills → `delta.skillCoveragePct > 100` + - Szenario ohne Änderungen aber mit bestehenden Assignments → `delta.costCents = 0`, `delta.hours = 0` ## Abhängigkeiten -- C-1 und C-2 sind **unabhängig** voneinander (C-2 ist reiner Render-Test der Komponente) +- E-1a und E-1b können **parallel** geschrieben werden (separate Dateien) +- E-1c und E-1d können **parallel** geschrieben werden +- Keine Abhängigkeiten zwischen allen vier ## Akzeptanzkriterien -- [ ] `pnpm test:unit` läuft grün -- [ ] `pnpm --filter @capakraken/web exec tsc --noEmit` — keine neuen TS-Errors -- [ ] Projekt-Bar-Drag startet mit aktiver Selektion: `FloatingActionBar` wird sofort ausgeblendet -- [ ] Single-Alloc-Drag auf nicht-selektierter Allocation: `FloatingActionBar` wird ausgeblendet -- [ ] Rechtsklick-Drag auf Canvas: Selektion wird korrekt überschrieben (bereits korrekt) - -## Risiken & offene Fragen - -- **Ref-Sync**: `multiSelectRef.current` wird bereits in `useTimelineDrag` auf Zeile 260 bei jedem Render mit `multiSelectState` synchronisiert. Der explizite Ref-Reset im Handler ist aber nötig, damit nachfolgende synchrone Zugriffe (z.B. innerhalb desselben Event-Handlers) nicht den alten Ref-Wert lesen. Kein Risiko. -- **Dep-Array**: `INITIAL_MULTI_SELECT` ist eine Modul-Konstante (referenz-stabil), `setMultiSelectState` ist stabil aus `useState`. Beide müssen nicht in die `useCallback` dep-Arrays — das ist konsistent mit der bestehenden Nutzung in `onAllocMouseDown`. +- [x] `pnpm test:unit` läuft grün +- [x] Alle 4 neuen Test-Dateien existieren mit ≥ 4 Tests jeweils (4 Dateien, 31 Tests, alle grün) --- -# Workstream D: SSE Reconnect — fehlende Edge-Case-Tests +# Workstream F: .env.example + Docs Housekeeping -## Anforderungsanalyse - -Vollständige Analyse von `useTimelineSSE.ts` und 8 bestehenden Tests in `useTimelineSSE.test.ts`. - -**Bereits abgedeckt:** -- Malformed payloads → kein invalidate -- Ping → kein invalidate, backoff-Reset -- Initial open → kein resync -- Error → reconnect → open → resync (genau einmal) -- Double-open nach reconnect → resync nur einmal -- Dispose während pending Timer -- Hidden on mount, dann visible → connect ohne resync -- Hide während aktiver Connection, dann visible → resync - -**Fehlende Edge Cases:** - -1. **Tab versteckt während pending reconnect-Timer** (kein aktives `es`, aber `reconnectTimeout.current` gesetzt): - `handleVisibilityChange` prüft `if (es || reconnectTimeout.current)` → setzt `shouldResyncOnOpen = true`, löscht Timer via `clearReconnectTimer()`. Bei visible wieder: `reconnectAttempts = 0`, `connect()` → neue Connection → open → resync. **Nicht getestet.** - -2. **Allererste Verbindung schlägt fehl** (kein `onopen` je gerufen, direkt `onerror`): - `shouldResyncOnOpen = true` nach dem ersten Fehler ohne vorangehendes open. Nach Retry → open → resync. Dokumentiert: auch wenn die initiale Verbindung nie erfolgreich war, wird beim ersten erfolgreichen open korrekt resynct. **Nicht explizit getestet.** - -3. **Mehrere konsekutive Errors ohne open** (implizit bereits abgedeckt im "resets reconnect backoff"-Test über `emitError(); emitError();`). Kein zusätzlicher Test nötig. - -## Betroffene Pakete & Dateien - -| Paket | Datei | Art | -|-------|-------|-----| -| `apps/web` | `src/hooks/useTimelineSSE.test.ts` | edit | - -## Task-Liste - -- [ ] **D-1:** `useTimelineSSE.test.ts` — zwei fehlende Edge-Case-Tests hinzufügen. - - **Test 9**: `"hides during pending reconnect timer — clears timer and resyncs on next open"` - - Sequence: connect → open → error (shouldResyncOnOpen=true, Timer 2s) → hide (Timer gecleart, shouldResyncOnOpen bleibt true, es=null) → show (reconnectAttempts=0, connect() → neue EventSource) → emitOpen() → resync - - Verifikation: `MockEventSource.instances` hat Länge 2; `invalidateQueries` wurde genau `getTimelineSseResyncKeys().length` mal mit den richtigen Keys aufgerufen - - Unterschied zu bestehendem hide-Test: dort wird `es` aktiv geschlossen; hier ist `es = null` und nur der Timer ist pending - - **Test 10**: `"resyncs after the first-ever connection attempt fails before any open"` - - Sequence: connect → emitError() (kein emitOpen davor, shouldResyncOnOpen=true) → advance timer 2000ms → neue EventSource → emitOpen() → resync - - Verifikation: `MockEventSource.instances` hat Länge 2; `invalidateQueries` aufgerufen mit resync-keys - - Dokumentiert: `shouldResyncOnOpen` wird auch beim allerersten Fehler gesetzt — nicht nur nach erfolgreicher erster Verbindung - - Kein Produktionscode geändert — reine Test-Ergänzung. - - → Datei: `apps/web/src/hooks/useTimelineSSE.test.ts` - -## Abhängigkeiten - -- D-1 ist vollständig unabhängig (nur Tests, kein Source-Code) - -## Akzeptanzkriterien - -- [ ] `pnpm test:unit` läuft grün (alle 10 SSE-Tests grün) -- [ ] Beide neuen Tests decken bisher ungetestetem Verhalten ab (kein Produktionscode nötig) - ---- - -# Parallele Ausführung - -**Workstream C** und **Workstream D** sind vollständig unabhängig: - -- **Agent 1** → C-1 + C-2 (sequenziell: C-1 zuerst, C-2 kann parallel) -- **Agent 2** → D-1 (eigenständig) - -Keine gemeinsamen Dateien, kein Merge-Konflikt-Risiko. +- [x] **F-1:** `.env.example` committen (commit 1ec56aa — erweitert auf ~85 Zeilen mit vollständiger Dokumentation) +- [x] **F-2:** `plan.md` nach Abschluss mit erledigten Tasks aktualisieren