From b41c1d25010c1ebaa1f90f157339789605c49f21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Thu, 21 May 2026 16:28:40 +0200 Subject: [PATCH] =?UTF-8?q?rename(phase=201):=20CapaKraken=20=E2=86=92=20N?= =?UTF-8?q?exus=20across=20code,=20UI,=20docs,=20CI=20(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61) Co-authored-by: Hartmut Nörenberg Co-committed-by: Hartmut Nörenberg --- .env.example | 10 +- .gitea/gitea_compose_qnap_all_in_one.md | 2 +- .github/SECURITY.md | 2 +- .github/workflows/ci.yml | 28 +- CLAUDE.md | 8 +- Dockerfile.dev | 2 +- Dockerfile.prod | 6 +- LEARNINGS.md | 87 +- README.md | 196 +- apps/web/e2e/a11y.spec.ts | 2 +- apps/web/e2e/admin.spec.ts | 31 +- apps/web/e2e/allocations.spec.ts | 38 +- apps/web/e2e/analytics.spec.ts | 8 +- apps/web/e2e/assistant-approvals.spec.ts | 20 +- apps/web/e2e/auth.spec.ts | 2 +- apps/web/e2e/bench.spec.ts | 9 +- apps/web/e2e/dashboard.spec.ts | 6 +- apps/web/e2e/dev-system/mfa.spec.ts | 42 +- apps/web/e2e/dev-system/nav-smoke.spec.ts | 2 +- .../e2e/dev-system/rbac-permissions.spec.ts | 22 +- apps/web/e2e/estimates.spec.ts | 43 +- apps/web/e2e/holiday-calendar.spec.ts | 26 +- apps/web/e2e/navigation.spec.ts | 14 +- apps/web/e2e/project-detail.spec.ts | 14 +- apps/web/e2e/projects.spec.ts | 34 +- apps/web/e2e/reports.spec.ts | 29 +- apps/web/e2e/resources.spec.ts | 11 +- apps/web/e2e/scenarios.spec.ts | 11 +- apps/web/e2e/smoke.spec.ts | 4 +- apps/web/e2e/staffing.spec.ts | 16 +- apps/web/e2e/test-server.mjs | 6 +- apps/web/e2e/timeline.spec.ts | 443 ++- apps/web/e2e/vacations.spec.ts | 39 +- apps/web/eslint.config.mjs | 2 +- apps/web/next.config.ts | 12 +- apps/web/package.json | 18 +- apps/web/playwright.dev.config.ts | 2 +- apps/web/public/manifest.json | 4 +- apps/web/public/sw.js | 6 +- .../src/app/(app)/admin/vacations/page.tsx | 26 +- .../app/(app)/estimates/EstimatesClient.tsx | 23 +- apps/web/src/app/(app)/mobile/page.tsx | 2 +- .../src/app/(app)/projects/ProjectsClient.tsx | 4 +- .../app/(app)/resources/ResourcesClient.tsx | 8 +- .../web/src/app/(app)/resources/[id]/page.tsx | 18 +- .../(app)/resources/resource-client/types.ts | 4 +- apps/web/src/app/(app)/vacations/my/page.tsx | 2 +- .../app/api/cron/auth-anomaly-check/detect.ts | 2 +- .../api/cron/auth-anomaly-check/route.test.ts | 6 +- .../app/api/cron/auth-anomaly-check/route.ts | 6 +- .../api/cron/chargeability-alerts/route.ts | 6 +- .../app/api/cron/estimate-reminders/route.ts | 6 +- .../src/app/api/cron/health-check/route.ts | 6 +- .../src/app/api/cron/public-holidays/route.ts | 14 +- .../src/app/api/cron/security-audit/route.ts | 6 +- .../src/app/api/cron/weekly-digest/route.ts | 6 +- apps/web/src/app/api/health/route.ts | 12 +- apps/web/src/app/api/perf/route.test.ts | 10 +- apps/web/src/app/api/perf/route.ts | 2 +- apps/web/src/app/api/ready/route.ts | 9 +- .../app/api/reports/allocations/route.test.ts | 6 +- .../src/app/api/reports/allocations/route.ts | 8 +- apps/web/src/app/api/sse/timeline/route.ts | 12 +- apps/web/src/app/api/trpc/[trpc]/route.ts | 6 +- .../app/auth/reset-password/[token]/page.tsx | 2 +- apps/web/src/app/auth/signin/page.tsx | 4 +- apps/web/src/app/invite/[token]/page.tsx | 4 +- apps/web/src/app/layout.tsx | 61 +- apps/web/src/app/setup/SetupClient.tsx | 4 +- apps/web/src/app/setup/actions.ts | 10 +- apps/web/src/app/setup/page.tsx | 2 +- .../src/components/admin/BatchSkillImport.tsx | 128 +- .../src/components/admin/InviteUserModal.tsx | 11 +- .../components/admin/SystemRolesClient.tsx | 2 +- .../components/admin/SystemSettingsClient.tsx | 2 +- .../src/components/admin/UserCreateModal.tsx | 2 +- .../src/components/admin/UserEditModal.tsx | 2 +- apps/web/src/components/admin/UsersClient.tsx | 4 +- .../src/components/admin/WebhooksClient.tsx | 34 +- .../system-settings/AiSettingsPanels.tsx | 22 +- .../system-settings/SmtpSettingsPanel.tsx | 2 +- .../allocations/AllocationBatchDialogs.tsx | 2 +- .../allocations/AllocationGroupedBody.tsx | 2 +- .../allocations/AllocationModal.tsx | 214 +- .../components/allocations/AllocationRow.tsx | 2 +- .../allocations/AllocationsClient.tsx | 6 +- .../allocations/ConflictWarningPanel.tsx | 32 +- .../allocations/FillOpenDemandModal.tsx | 219 +- .../allocations/OpenDemandsPanel.tsx | 2 +- .../allocations/RecurrenceEditor.tsx | 27 +- .../src/components/assistant/ChatPanel.tsx | 4 +- .../blueprints/BlueprintFieldCatalog.tsx | 113 +- .../blueprints/BlueprintFieldEditor.tsx | 54 +- .../blueprints/BlueprintsClient.tsx | 6 +- .../src/components/blueprints/FieldCard.tsx | 32 +- .../blueprints/RolePresetsEditor.tsx | 2 +- .../src/components/comments/CommentInput.tsx | 67 +- .../src/components/comments/CommentThread.tsx | 59 +- .../components/dashboard/AddWidgetModal.tsx | 10 +- .../components/dashboard/DashboardClient.tsx | 20 +- .../components/dashboard/widget-registry.ts | 48 +- .../dashboard/widgets/ProjectTableWidget.tsx | 8 +- .../dynamic-fields/DynamicFieldEditor.tsx | 49 +- .../dynamic-fields/DynamicFieldRenderer.tsx | 4 +- .../estimates/CommercialTermsEditor.tsx | 104 +- .../components/estimates/EstimateWizard.tsx | 527 +++- .../EstimateWorkspace.calculations.ts | 5 +- .../estimates/EstimateWorkspace.types.ts | 2 +- .../estimates/EstimateWorkspaceClient.tsx | 2 +- .../EstimateWorkspaceDraftEditor.tsx | 94 +- .../components/estimates/VersionCompare.tsx | 219 +- .../estimates/editors/DemandLineEditor.tsx | 312 +- .../components/estimates/tabs/ExportsTab.tsx | 29 +- .../components/estimates/tabs/OverviewTab.tsx | 2 +- .../components/estimates/tabs/StaffingTab.tsx | 115 +- .../components/estimates/tabs/VersionsTab.tsx | 34 +- apps/web/src/components/layout/AppShell.tsx | 2 +- .../src/components/layout/InstallPrompt.tsx | 13 +- .../src/components/layout/ThemeProvider.tsx | 2 +- .../components/mobile/MobileSummaryClient.tsx | 2 +- .../projects/ProjectDemandsTable.tsx | 166 +- .../projects/ProjectDetailClient.tsx | 25 +- .../src/components/projects/ProjectModal.tsx | 4 +- .../components/projects/ScenarioPlanner.tsx | 130 +- .../project-wizard/DynamicFieldInput.tsx | 4 +- .../projects/project-wizard/Step1Identity.tsx | 4 +- .../projects/project-wizard/Step3Staffing.tsx | 2 +- .../project-wizard/Step4Suggestions.tsx | 2 +- .../projects/project-wizard/types.ts | 2 +- .../project-wizard/useProjectWizardForm.ts | 4 +- .../components/reports/AllocationReport.tsx | 24 +- .../components/reports/ReportResultsPanel.tsx | 2 +- .../reports/ResourceMonthConfigSection.tsx | 7 +- .../components/resources/BulkEditModal.tsx | 4 +- .../components/resources/ResourceDetail.tsx | 2 +- .../components/resources/ResourceModal.tsx | 2 +- .../resources/ResourceOrgClassification.tsx | 2 +- .../resources/SkillMatrixUpload.tsx | 74 +- .../components/resources/SkillRadarChart.tsx | 23 +- apps/web/src/components/roles/RoleModal.tsx | 205 +- apps/web/src/components/roles/RolesClient.tsx | 7 +- .../components/security/MfaPromptBanner.tsx | 2 +- apps/web/src/components/security/MfaSetup.tsx | 4 +- .../src/components/staffing/StaffingPanel.tsx | 19 +- .../staffing/StaffingResultCard.tsx | 216 +- .../components/timeline/AllocationPopover.tsx | 2 +- .../timeline/BatchAssignPopover.tsx | 2 +- .../src/components/timeline/DemandPopover.tsx | 2 +- .../timeline/NewAllocationPopover.tsx | 2 +- .../src/components/timeline/ProjectPanel.tsx | 2 +- .../components/timeline/ResourceHoverCard.tsx | 63 +- .../components/timeline/TimelineContext.tsx | 2 +- .../timeline/TimelineDragOverlays.tsx | 2 +- .../components/timeline/TimelineTooltip.tsx | 14 +- .../timeline/timelineAvailability.ts | 6 +- .../components/timeline/timelineCapacity.ts | 12 +- .../src/components/timeline/timelineHover.ts | 47 +- .../timeline/timelineResourceRender.tsx | 2 +- .../components/ui/ColumnTogglePanel.test.tsx | 2 +- .../src/components/ui/ColumnTogglePanel.tsx | 44 +- .../components/ui/CustomFieldFilterBar.tsx | 8 +- .../web/src/components/ui/ProjectCombobox.tsx | 21 +- .../vacations/MyVacationsClient.tsx | 113 +- .../vacations/PublicHolidayBatch.tsx | 48 +- .../src/components/vacations/TeamCalendar.tsx | 57 +- .../components/vacations/VacationCalendar.tsx | 83 +- .../components/vacations/VacationClient.tsx | 162 +- .../components/vacations/VacationModal.tsx | 2 +- .../vacations/vacationExplainability.ts | 14 +- apps/web/src/hooks/timelineRangeSelection.ts | 2 +- apps/web/src/hooks/timelineSsePolicy.test.ts | 2 +- apps/web/src/hooks/timelineSsePolicy.ts | 18 +- apps/web/src/hooks/useAppPreferences.test.ts | 2 +- apps/web/src/hooks/useAppPreferences.ts | 58 +- apps/web/src/hooks/useColumnConfig.test.ts | 2 +- apps/web/src/hooks/useColumnConfig.ts | 14 +- apps/web/src/hooks/useDashboardLayout.test.ts | 50 +- apps/web/src/hooks/useDashboardLayout.ts | 6 +- apps/web/src/hooks/useFilters.test.ts | 2 +- apps/web/src/hooks/useFilters.ts | 2 +- apps/web/src/hooks/useProjectDragContext.ts | 2 +- apps/web/src/hooks/useTheme.test.ts | 2 +- apps/web/src/hooks/useTheme.ts | 20 +- apps/web/src/hooks/useTimelineSSE.test.ts | 4 +- apps/web/src/hooks/useTimelineSSE.ts | 2 +- apps/web/src/hooks/useViewPrefs.ts | 2 +- apps/web/src/instrumentation.ts | 2 +- .../src/lib/blueprint-field-catalog.test.ts | 2 +- apps/web/src/lib/blueprint-field-catalog.ts | 12 +- apps/web/src/lib/skillMatrixParser.ts | 25 +- apps/web/src/lib/trpc/client.ts | 2 +- apps/web/src/server/auth.test.ts | 10 +- apps/web/src/server/auth.ts | 14 +- apps/web/src/server/runtime-env.test.ts | 12 +- apps/web/src/server/runtime-env.ts | 2 +- apps/web/src/server/trpc.ts | 8 +- apps/web/tsconfig.json | 10 +- docker-compose.yml | 4 +- docs/README.md | 48 +- docs/acn-full-standards-compliance.md | 234 +- docs/acn-security-compliance-status.md | 271 +- docs/acn-security-compliance-todo.md | 188 +- docs/acn-standards-applicability.md | 142 +- docs/ai-excellence-due-diligence-roadmap.md | 18 +- .../0001-runtime-secret-provisioning.md | 2 +- docs/assistant-tool-test-split-migration.md | 2 +- docs/audience-scoping-backlog.md | 2 +- docs/calculation-reference.md | 165 +- docs/ci-cd-manual.md | 34 +- docs/cicd-target-architecture.md | 34 +- docs/demand-assignment-migration-cutover.md | 13 +- docs/developer-runbook.md | 44 +- docs/dispo-import-implementation-tickets.md | 6 +- docs/dispo-import-implementation.md | 36 +- docs/domain-slices-backlog.md | 322 +- docs/estimating-extension-design.md | 154 +- docs/gitlooper-strategy.md | 43 +- docs/holiday-calendar-implementation-plan.md | 2 +- docs/import-hardening.md | 6 +- docs/imports/departed-sync-2026-03-14.md | 13 +- docs/installation.md | 24 +- docs/old-markdowns/PLAN_SKILLMATRIX.md | 2 +- .../architecture-evaluation-2026-03-06.md | 2 +- .../cgi-breakdown-implementation-proposal.md | 2 +- .../old-markdowns/estimating-field-mapping.md | 2 +- docs/old-markdowns/perf-audit-2026-03-09.md | 2 +- docs/old-markdowns/plan.md | 2 +- docs/old-markdowns/refactor-sprint-plan.md | 2 +- ...iew-report-2026-03-15-computation-graph.md | 24 +- ...formance-optimization-review-2026-03-18.md | 30 +- docs/product-roadmap.md | 149 +- docs/refactor-v2-plan.md | 114 +- docs/sanity-check.md | 199 +- docs/sdlc.md | 22 +- docs/security-architecture.md | 10 +- docs/security-audit-2026-03-15.md | 8 +- docs/showcase-execution-batches.md | 7 +- docs/showcase-quality-backlog.md | 26 +- eslint.config.mjs | 2 +- knip.json | 2 +- package.json | 21 +- packages/api/package.json | 14 +- packages/api/src/__tests__/ai-client.test.ts | 2 +- .../allocation-conflict-check.test.ts | 4 +- .../src/__tests__/allocation-router.test.ts | 2 +- .../src/__tests__/assistant-chat-loop.test.ts | 272 +- .../__tests__/assistant-router-auth.test.ts | 60 +- .../assistant-tool-policy-access.test.ts | 95 +- .../assistant-tool-policy-admin.test.ts | 30 +- .../assistant-tool-policy-planning.test.ts | 53 +- .../assistant-tool-selection.test.ts | 17 +- ...assistant-tools-admin-crud-test-helpers.ts | 2 +- ...ols-advanced-project-shift-preview.test.ts | 81 +- ...-advanced-project-timeline-context.test.ts | 31 +- ...nt-tools-advanced-resource-ranking.test.ts | 24 +- ...ols-advanced-timeline-entries-view.test.ts | 13 +- ...advanced-timeline-holiday-overlays.test.ts | 6 +- ...nt-tools-advanced-timeline-test-helpers.ts | 2 +- ...ant-tools-allocation-cancel-errors.test.ts | 10 +- ...nt-tools-allocation-cancel-success.test.ts | 2 +- ...nt-tools-allocation-cancel-test-helpers.ts | 4 +- ...ant-tools-allocation-create-errors.test.ts | 43 +- ...nt-tools-allocation-create-success.test.ts | 6 +- ...-tools-allocation-planning-test-helpers.ts | 2 +- .../assistant-tools-allocation-read.test.ts | 6 +- ...ant-tools-allocation-status-errors.test.ts | 10 +- ...nt-tools-allocation-status-success.test.ts | 26 +- ...nt-tools-allocation-status-test-helpers.ts | 4 +- ...sistant-tools-audit-entity-summary.test.ts | 6 +- .../assistant-tools-audit-errors-auth.test.ts | 14 +- .../assistant-tools-audit-log-list.test.ts | 6 +- .../assistant-tools-audit-read.test.ts | 12 +- ...assistant-tools-audit-task-test-helpers.ts | 18 +- .../assistant-tools-auth-guard.test.ts | 6 +- .../assistant-tools-broadcast-detail.test.ts | 6 +- .../assistant-tools-broadcast-list.test.ts | 18 +- ...dcast-send-fanout-recipient-errors.test.ts | 5 +- ...roadcast-send-fanout-sender-errors.test.ts | 5 +- ...broadcast-send-finalization-errors.test.ts | 6 +- ...stant-tools-broadcast-send-success.test.ts | 6 +- ...stant-tools-broadcast-send-test-helpers.ts | 4 +- ...s-broadcast-send-validation-errors.test.ts | 2 +- .../assistant-tools-broadcast-send.test.ts | 6 +- .../assistant-tools-budget-status.test.ts | 8 +- ...sistant-tools-chargeability-report.test.ts | 14 +- ...sistant-tools-client-delete-errors.test.ts | 30 +- ...istant-tools-client-delete-success.test.ts | 8 +- ...ent-mutations-create-update-errors.test.ts | 14 +- ...nt-mutations-create-update-success.test.ts | 42 +- ...stant-tools-comments-create-errors.test.ts | 9 +- ...tant-tools-comments-create-resolve.test.ts | 16 +- .../assistant-tools-comments-list.test.ts | 16 +- ...tant-tools-comments-resolve-errors.test.ts | 2 +- .../assistant-tools-comments-test-helpers.ts | 11 +- .../assistant-tools-country-get.test.ts | 21 +- ...ant-tools-country-mutations-errors.test.ts | 2 +- .../assistant-tools-country-test-helpers.ts | 2 +- .../assistant-tools-dashboard-detail.test.ts | 16 +- ...assistant-tools-dashboard-overview.test.ts | 2 +- ...ant-tools-dashboard-project-health.test.ts | 2 +- ...sistant-tools-dashboard-skill-gaps.test.ts | 10 +- .../assistant-tools-dashboard-test-helpers.ts | 13 +- ...nt-tools-demand-create-race-errors.test.ts | 55 +- ...nt-tools-demand-create-role-errors.test.ts | 39 +- ...istant-tools-demand-create-success.test.ts | 13 +- ...istant-tools-demand-create-test-helpers.ts | 4 +- ...assistant-tools-demand-fill-errors.test.ts | 38 +- .../assistant-tools-demand-fill.test.ts | 35 +- ...ools-dispo-import-batch-delegation.test.ts | 10 +- ...ols-dispo-import-batch-list-cancel.test.ts | 8 +- .../assistant-tools-dispo-import.test.ts | 2 +- ...ools-dispo-staged-assignments-read.test.ts | 8 +- ...staged-listings-resources-projects.test.ts | 8 +- ...tant-tools-dispo-staged-resolution.test.ts | 11 +- ...tools-dispo-staged-unresolved-read.test.ts | 8 +- ...-tools-dispo-staged-vacations-read.test.ts | 6 +- .../assistant-tools-dispo-test-helpers.ts | 2 +- ...istant-tools-estimate-clone-errors.test.ts | 33 +- ...s-estimate-commercial-terms-errors.test.ts | 6 +- ...tant-tools-estimate-creation-races.test.ts | 15 +- ...istant-tools-estimate-draft-errors.test.ts | 16 +- ...ate-generate-weekly-phasing-errors.test.ts | 8 +- ...estimate-get-weekly-phasing-errors.test.ts | 26 +- ...s-estimate-planning-handoff-errors.test.ts | 67 +- ...-tools-estimate-read-detail-access.test.ts | 8 +- ...tant-tools-estimate-read-not-found.test.ts | 8 +- ...imate-read-version-snapshot-access.test.ts | 10 +- ...estimate-read-versions-list-access.test.ts | 6 +- ...ls-estimate-revision-export-errors.test.ts | 53 +- .../assistant-tools-estimate-test-helpers.ts | 4 +- ...ols-estimate-version-status-errors.test.ts | 60 +- .../assistant-tools-export-projects.test.ts | 2 +- .../__tests__/assistant-tools-export.test.ts | 2 +- ...stant-tools-holiday-budget-shoring.test.ts | 6 +- ...-holiday-calendar-mutations-guards.test.ts | 6 +- ...holiday-calendar-mutations-success.test.ts | 31 +- ...ant-tools-holiday-capacity-test-helpers.ts | 2 +- .../assistant-tools-holiday-capacity.test.ts | 6 +- ...istant-tools-holiday-chargeability.test.ts | 33 +- ...ols-holiday-entry-mutations-errors.test.ts | 4 +- ...ls-holiday-entry-mutations-success.test.ts | 28 +- ...tant-tools-holiday-mutation-errors.test.ts | 12 +- ...sistant-tools-holiday-read-test-helpers.ts | 4 +- ...oliday-resolution-calendar-preview.test.ts | 7 +- ...assistant-tools-holiday-simulation.test.ts | 4 +- ...tools-holiday-staffing-suggestions.test.ts | 11 +- .../assistant-tools-holiday-test-helpers.ts | 2 +- ...ools-import-dispo-webhooks-test-helpers.ts | 11 +- .../__tests__/assistant-tools-import.test.ts | 2 +- ...assistant-tools-insights-anomalies.test.ts | 6 +- ...t-tools-insights-scenarios-test-helpers.ts | 2 +- .../assistant-tools-insights-summary.test.ts | 6 +- ...-master-data-blueprints-rate-cards.test.ts | 8 +- ...master-data-calculation-rules-read.test.ts | 8 +- ...ant-tools-master-data-clients-read.test.ts | 8 +- ...master-data-effort-experience-read.test.ts | 8 +- ...r-data-management-utilization-read.test.ts | 8 +- ...tools-master-data-mutation-test-helpers.ts | 2 +- ...t-tools-master-data-org-units-read.test.ts | 14 +- ...tant-tools-master-data-rate-lookup.test.ts | 8 +- ...ant-tools-master-data-read-test-helpers.ts | 2 +- ...stant-tools-master-data-roles-read.test.ts | 8 +- ...t-tools-notification-create-errors.test.ts | 6 +- ...-tools-notification-create-success.test.ts | 14 +- ...nt-tools-notification-inbox-errors.test.ts | 19 +- ...tools-notification-inbox-mutations.test.ts | 12 +- ...tant-tools-notification-inbox-read.test.ts | 12 +- ...sistant-tools-notification-test-helpers.ts | 17 +- ...nt-tools-org-unit-mutations-errors.test.ts | 32 +- ...t-tools-org-unit-mutations-success.test.ts | 86 +- ...t-tools-pending-vacation-approvals.test.ts | 6 +- ...t-tools-planning-availability-read.test.ts | 8 +- ...assistant-tools-planning-rate-read.test.ts | 12 +- ...istant-tools-planning-read-test-helpers.ts | 2 +- ...ssistant-tools-planning-skill-read.test.ts | 12 +- ...ject-admin-create-blueprint-errors.test.ts | 5 +- ...tools-project-admin-create-success.test.ts | 2 +- ...tools-project-admin-create-test-helpers.ts | 4 +- ...ls-project-admin-create-validation.test.ts | 5 +- ...sistant-tools-project-admin-delete.test.ts | 40 +- ...istant-tools-project-admin-test-helpers.ts | 8 +- ...sistant-tools-project-admin-update.test.ts | 51 +- ...nt-tools-project-computation-graph.test.ts | 7 +- ...stant-tools-project-cover-generate.test.ts | 6 +- ...sistant-tools-project-cover-remove.test.ts | 14 +- .../assistant-tools-project-detail.test.ts | 5 +- ...istant-tools-project-media-test-helpers.ts | 2 +- .../assistant-tools-project-narrative.test.ts | 11 +- ...ssistant-tools-project-read-errors.test.ts | 13 +- .../assistant-tools-project-search.test.ts | 2 +- .../assistant-tools-project-test-helpers.ts | 11 +- ...sistant-tools-query-change-history.test.ts | 6 +- .../assistant-tools-registry-access.test.ts | 22 +- ...reminder-create-persistence-errors.test.ts | 6 +- ...-reminder-create-validation-errors.test.ts | 12 +- .../assistant-tools-reminder-list.test.ts | 12 +- ...nt-tools-reminder-mutations-errors.test.ts | 38 +- ...t-tools-reminder-mutations-success.test.ts | 12 +- .../assistant-tools-report-read.test.ts | 34 +- .../assistant-tools-report-test-helpers.ts | 2 +- ...ce-admin-create-persistence-errors.test.ts | 2 +- ...ools-resource-admin-create-success.test.ts | 2 +- ...ools-resource-admin-create-test-helpers.ts | 4 +- ...rce-admin-create-validation-errors.test.ts | 2 +- ...s-resource-admin-update-deactivate.test.ts | 12 +- ...istant-tools-resource-availability.test.ts | 16 +- ...t-tools-resource-computation-graph.test.ts | 21 +- .../assistant-tools-resource-detail.test.ts | 5 +- .../assistant-tools-resource-search.test.ts | 2 +- .../assistant-tools-resource-test-helpers.ts | 11 +- ...ssistant-tools-role-delete-success.test.ts | 15 +- ...istant-tools-role-mutation-test-helpers.ts | 6 +- ...le-mutations-create-update-success.test.ts | 91 +- ...istant-tools-role-mutations-errors.test.ts | 42 +- .../assistant-tools-scenarios.test.ts | 8 +- ...stant-tools-settings-ai-configured.test.ts | 2 +- ...sistant-tools-settings-connections.test.ts | 2 +- ...t-tools-settings-role-config-admin.test.ts | 2 +- .../assistant-tools-settings-runtime.test.ts | 2 +- .../assistant-tools-settings-test-helpers.ts | 11 +- ...ools-task-action-assignment-errors.test.ts | 7 +- ...istant-tools-task-action-execution.test.ts | 7 +- ...assistant-tools-task-action-guards.test.ts | 7 +- ...ssistant-tools-task-action-test-helpers.ts | 4 +- ...-tools-task-action-vacation-errors.test.ts | 9 +- .../assistant-tools-task-counts.test.ts | 2 +- ...assistant-tools-task-create-errors.test.ts | 6 +- ...ssistant-tools-task-create-success.test.ts | 10 +- .../assistant-tools-task-detail.test.ts | 8 +- .../assistant-tools-task-read.test.ts | 6 +- ...ant-tools-task-workflow-assignment.test.ts | 2 +- ...sistant-tools-task-workflow-status.test.ts | 2 +- ...istant-tools-task-workflow-test-helpers.ts | 4 +- ...istant-tools-team-vacation-overlap.test.ts | 4 +- ...s-timeline-allocation-shift-errors.test.ts | 2 +- ...t-tools-timeline-allocation-shifts.test.ts | 7 +- ...imeline-batch-quick-assign-test-helpers.ts | 10 +- ...ne-inline-allocation-update-errors.test.ts | 6 +- ...e-inline-allocation-update-success.test.ts | 11 +- ...tant-tools-timeline-project-shifts.test.ts | 11 +- ...ools-timeline-quick-assign-test-helpers.ts | 6 +- ...-tools-timeline-resource-selection.test.ts | 6 +- ...tant-tools-timeline-shifts-test-helpers.ts | 6 +- ...-tools-user-admin-assignable-users.test.ts | 46 +- ...nt-tools-user-admin-inventory-read.test.ts | 29 +- ...stant-tools-user-admin-name-errors.test.ts | 30 +- ...ls-user-admin-password-role-errors.test.ts | 6 +- ...s-user-admin-resource-auto-linking.test.ts | 11 +- ...n-resource-linking-conflict-errors.test.ts | 11 +- ...-resource-linking-not-found-errors.test.ts | 26 +- ...ser-admin-resource-linking-success.test.ts | 11 +- ...assistant-tools-user-admin-test-helpers.ts | 7 +- ...ools-user-admin-user-create-errors.test.ts | 6 +- ...s-user-admin-user-permissions-totp.test.ts | 38 +- ...tools-user-self-service-auth-guard.test.ts | 6 +- ...er-self-service-column-preferences.test.ts | 6 +- ...user-self-service-dashboard-layout.test.ts | 12 +- ...ser-self-service-favorite-projects.test.ts | 6 +- ...tools-user-self-service-mfa-enable.test.ts | 2 +- ...tools-user-self-service-mfa-errors.test.ts | 2 +- ...tools-user-self-service-mfa-status.test.ts | 2 +- ...ools-user-self-service-mfa-test-helpers.ts | 4 +- ...nt-tools-user-self-service-profile.test.ts | 17 +- ...nt-tools-user-self-service-test-helpers.ts | 2 +- ...ant-tools-vacation-approval-errors.test.ts | 12 +- .../assistant-tools-vacation-balance.test.ts | 287 +- ...tools-vacation-cancellation-errors.test.ts | 14 +- .../assistant-tools-vacation-create.test.ts | 8 +- ...ant-tools-vacation-creation-errors.test.ts | 37 +- ...-tools-vacation-entitlement-errors.test.ts | 23 +- ...ant-tools-vacation-entitlement-set.test.ts | 30 +- ...tools-vacation-entitlement-summary.test.ts | 6 +- ...tools-vacation-entitlement-test-helpers.ts | 2 +- ...nt-tools-vacation-mutation-test-helpers.ts | 13 +- ...nt-tools-vacation-rejection-errors.test.ts | 15 +- ...stant-tools-vacation-review-cancel.test.ts | 6 +- ...stant-tools-vacation-upcoming-read.test.ts | 8 +- .../assistant-tools-webhooks-errors.test.ts | 2 +- .../assistant-tools-webhooks-read.test.ts | 2 +- ...assistant-tools-workflow-scenarios.test.ts | 96 +- .../src/__tests__/audit-log-router.test.ts | 2 +- .../blueprint-procedure-support.test.ts | 8 +- .../src/__tests__/blueprint-router.test.ts | 2 +- .../src/__tests__/blueprint-support.test.ts | 53 +- .../__tests__/blueprint-validation.test.ts | 2 +- .../calculation-rules-router.test.ts | 2 +- .../__tests__/chargeability-alerts.test.ts | 6 +- ...geability-report-procedure-support.test.ts | 2 +- .../chargeability-report-router.test.ts | 32 +- .../api/src/__tests__/client-router.test.ts | 2 +- .../src/__tests__/comment-router-auth.test.ts | 423 +-- .../comment-sanitization-router.test.ts | 2 +- .../computation-graph-router.test.ts | 141 +- .../api/src/__tests__/country-router.test.ts | 51 +- .../__tests__/custom-field-filters.test.ts | 2 +- .../dashboard-procedure-support.test.ts | 24 +- .../src/__tests__/dashboard-router.test.ts | 12 +- .../dispo-management-support.test.ts | 44 +- .../__tests__/dispo-procedure-support.test.ts | 47 +- .../api/src/__tests__/dispo-router.test.ts | 6 +- .../effort-rule-procedure-support.test.ts | 108 +- .../src/__tests__/effort-rule-router.test.ts | 45 +- .../email-runtime-config-hardening.test.ts | 2 +- .../__tests__/email-smtp-env-override.test.ts | 8 +- .../__tests__/entitlement-router-auth.test.ts | 170 +- .../src/__tests__/entitlement-router.test.ts | 417 +-- .../api/src/__tests__/estimate-router.test.ts | 6 +- .../src/__tests__/event-bus-debounce.test.ts | 154 +- ...ience-multiplier-procedure-support.test.ts | 114 +- .../experience-multiplier-router.test.ts | 20 +- .../holiday-calendar-catalog-read.test.ts | 2 +- .../holiday-calendar-router-auth.test.ts | 95 +- .../__tests__/holiday-calendar-router.test.ts | 100 +- .../__tests__/identifier-resolvers.test.ts | 218 +- .../import-export-procedure-support.test.ts | 55 +- .../__tests__/import-export-router.test.ts | 17 +- .../insights-procedure-support.test.ts | 11 +- .../api/src/__tests__/insights-router.test.ts | 14 +- packages/api/src/__tests__/invite.test.ts | 7 +- .../__tests__/management-level-router.test.ts | 33 +- .../__tests__/master-data-router-auth.test.ts | 2 +- .../notification-router-auth.test.ts | 142 +- .../src/__tests__/notification-router.test.ts | 285 +- .../api/src/__tests__/org-unit-router.test.ts | 49 +- .../__tests__/prisma-error-middleware.test.ts | 6 +- .../project-lifecycle-router.test.ts | 2 +- .../src/__tests__/project-mutations.test.ts | 2 +- .../project-planning-read-model.test.ts | 2 +- .../project-procedure-support.test.ts | 51 +- .../src/__tests__/project-read-router.test.ts | 4 +- .../project-router-planning-counts.test.ts | 2 +- .../api/src/__tests__/project-router.test.ts | 94 +- .../rate-card-procedure-support.test.ts | 152 +- .../src/__tests__/rate-card-router.test.ts | 78 +- .../__tests__/rbac-cache-redis-pubsub.test.ts | 4 +- .../src/__tests__/reminder-scheduler.test.ts | 2 +- .../api/src/__tests__/report-router.test.ts | 136 +- .../api/src/__tests__/resource-access.test.ts | 117 +- .../resource-identifier-read.test.ts | 2 +- .../src/__tests__/resource-mutations.test.ts | 2 +- .../__tests__/resource-router-auth.test.ts | 184 +- .../__tests__/resource-router-crud.test.ts | 48 +- .../api/src/__tests__/resource-router.test.ts | 280 +- .../__tests__/role-procedure-support.test.ts | 80 +- .../src/__tests__/role-router-auth.test.ts | 34 +- .../role-router-planning-counts.test.ts | 107 +- .../api/src/__tests__/role-router.test.ts | 4 +- .../scenario-procedure-support.test.ts | 73 +- .../api/src/__tests__/scenario-router.test.ts | 44 +- .../api/src/__tests__/scenario-shared.test.ts | 13 +- .../settings-procedure-support.test.ts | 126 +- .../__tests__/settings-router-auth.test.ts | 37 +- .../api/src/__tests__/settings-router.test.ts | 2 +- .../settings-runtime-config-hardening.test.ts | 28 +- .../__tests__/sse-subscription-policy.test.ts | 18 +- .../api/src/__tests__/staffing-router.test.ts | 225 +- .../system-role-config-router.test.ts | 2 +- ...ation-assignment-procedure-support.test.ts | 7 +- ...meline-allocation-fragment-support.test.ts | 4 +- ...timeline-allocation-inline-support.test.ts | 81 +- ...allocation-mutation-schema-support.test.ts | 2 +- .../timeline-allocation-shift-support.test.ts | 97 +- .../src/__tests__/timeline-allocation.test.ts | 5 +- .../timeline-cost-load-support.test.ts | 80 +- .../__tests__/timeline-cost-support.test.ts | 4 +- .../timeline-project-load-support.test.ts | 13 +- ...timeline-project-procedure-support.test.ts | 77 +- .../api/src/__tests__/timeline-router.test.ts | 104 +- .../timeline-shift-mutation-support.test.ts | 4 +- .../__tests__/timeline-shift-planning.test.ts | 2 +- .../timeline-shift-procedure-support.test.ts | 52 +- .../__tests__/timeline-shift-support.test.ts | 41 +- .../__tests__/user-procedure-support.test.ts | 2 +- ...le-permission-session-invalidation.test.ts | 2 +- .../src/__tests__/user-router-auth.test.ts | 2 +- .../api/src/__tests__/user-router.test.ts | 4 +- .../__tests__/user-self-service-mfa.test.ts | 2 +- .../utilization-category-router.test.ts | 2 +- .../__tests__/vacation-create-support.test.ts | 81 +- .../vacation-management-support.test.ts | 104 +- .../__tests__/vacation-read-router.test.ts | 24 +- .../__tests__/vacation-router-auth.test.ts | 109 +- .../api/src/__tests__/vacation-router.test.ts | 343 ++- .../src/__tests__/webhook-router-auth.test.ts | 26 +- .../api/src/__tests__/webhook-router.test.ts | 2 +- .../api/src/__tests__/webhook-support.test.ts | 66 +- packages/api/src/ai-client.ts | 39 +- packages/api/src/lib/anonymization.ts | 14 +- packages/api/src/lib/audit-helpers.ts | 2 +- packages/api/src/lib/audit.ts | 2 +- packages/api/src/lib/auto-staffing.ts | 8 +- packages/api/src/lib/budget-alerts.ts | 4 +- packages/api/src/lib/chargeability-alerts.ts | 8 +- .../api/src/lib/comment-entity-registry.ts | 9 +- packages/api/src/lib/create-notification.ts | 2 +- packages/api/src/lib/email.ts | 23 +- packages/api/src/lib/estimate-reminders.ts | 2 +- packages/api/src/lib/holiday-availability.ts | 76 +- .../api/src/lib/notification-targeting.ts | 2 +- packages/api/src/lib/pruning.ts | 6 +- packages/api/src/lib/read-only-prisma.ts | 2 +- packages/api/src/lib/reminder-scheduler.ts | 2 +- packages/api/src/lib/resource-access.ts | 11 +- packages/api/src/lib/resource-capacity.ts | 7 +- .../api/src/lib/resource-holiday-context.ts | 56 +- packages/api/src/lib/runtime-security.ts | 2 +- packages/api/src/lib/task-actions.ts | 2 +- packages/api/src/lib/vacation-conflicts.ts | 4 +- packages/api/src/lib/vacation-day-count.ts | 10 +- .../src/lib/vacation-deduction-snapshot.ts | 10 +- .../api/src/lib/weekly-digest-template.ts | 6 +- packages/api/src/lib/weekly-digest.ts | 8 +- .../router/allocation-conflict-procedures.ts | 4 +- .../router/allocation/assignment-mutations.ts | 18 +- .../allocation/assignment-procedures.ts | 6 +- .../api/src/router/allocation/availability.ts | 65 +- packages/api/src/router/allocation/demand.ts | 8 +- packages/api/src/router/allocation/effects.ts | 11 +- packages/api/src/router/allocation/read.ts | 2 +- packages/api/src/router/allocation/shared.ts | 11 +- packages/api/src/router/allocation/support.ts | 63 +- .../api/src/router/assistant-approvals.ts | 133 +- .../api/src/router/assistant-chat-loop.ts | 8 +- .../src/router/assistant-procedure-support.ts | 2 +- .../api/src/router/assistant-system-prompt.ts | 2 +- .../api/src/router/assistant-tool-policy.ts | 2 +- .../api/src/router/assistant-tool-registry.ts | 13 +- packages/api/src/router/assistant-tools.ts | 2 +- .../router/assistant-tools/access-control.ts | 2 +- .../assistant-tools/advanced-timeline.ts | 908 +++--- .../assistant-tools/allocation-planning.ts | 316 +- .../router/assistant-tools/audit-history.ts | 93 +- .../assistant-tools/blueprints-rate-cards.ts | 168 +- .../chargeability-computation.ts | 214 +- .../assistant-tools/clients-org-units.ts | 286 +- .../src/router/assistant-tools/comments.ts | 131 +- .../assistant-tools/config-readmodels.ts | 2 +- .../assistant-tools/country-metro-admin.ts | 256 +- .../assistant-tools/country-readmodels.ts | 92 +- .../dashboard-insights-reports.ts | 301 +- .../src/router/assistant-tools/estimates.ts | 862 +++--- .../api/src/router/assistant-tools/helpers.ts | 6 +- .../assistant-tools/import-export-dispo.ts | 735 +++-- .../assistant-tools/notifications-tasks.ts | 898 +++--- .../assistant-tools/planning-navigation.ts | 350 ++- .../src/router/assistant-tools/projects.ts | 132 +- .../src/router/assistant-tools/resources.ts | 253 +- .../router/assistant-tools/roles-analytics.ts | 308 +- .../assistant-tools/scenario-rate-analysis.ts | 157 +- .../router/assistant-tools/settings-admin.ts | 996 +++--- .../api/src/router/assistant-tools/shared.ts | 4 +- .../router/assistant-tools/staffing-demand.ts | 283 +- .../src/router/assistant-tools/user-admin.ts | 448 +-- .../assistant-tools/user-self-service.ts | 352 ++- .../assistant-tools/vacation-entitlements.ts | 361 ++- .../assistant-tools/vacation-holidays.ts | 744 +++-- packages/api/src/router/assistant.ts | 9 +- packages/api/src/router/auth.ts | 10 +- .../src/router/blueprint-procedure-support.ts | 2 +- packages/api/src/router/blueprint-support.ts | 4 +- .../api/src/router/blueprint-validation.ts | 4 +- packages/api/src/router/blueprint.ts | 2 +- .../calculation-rule-procedure-support.ts | 5 +- .../src/router/calculation-rule-support.ts | 19 +- .../chargeability-report-procedure-support.ts | 248 +- .../src/router/client-procedure-support.ts | 2 +- packages/api/src/router/client-support.ts | 28 +- packages/api/src/router/client.ts | 2 +- .../computation-graph-project-estimate.ts | 494 ++- .../src/router/computation-graph-project.ts | 149 +- .../computation-graph-resource-allocation.ts | 32 +- ...computation-graph-resource-availability.ts | 75 +- .../computation-graph-resource-budget.ts | 94 +- .../computation-graph-resource-graph.ts | 578 +++- .../computation-graph-resource-snapshot.ts | 15 +- .../src/router/country-procedure-support.ts | 32 +- packages/api/src/router/country-support.ts | 36 +- packages/api/src/router/country.ts | 4 +- .../api/src/router/custom-field-filters.ts | 2 +- .../src/router/dashboard-detail-support.ts | 34 +- .../src/router/dashboard-procedure-support.ts | 134 +- .../src/router/dispo-management-support.ts | 18 +- packages/api/src/router/dispo-management.ts | 14 +- .../api/src/router/dispo-procedure-support.ts | 4 +- packages/api/src/router/dispo-read.ts | 13 +- .../router/effort-rule-procedure-support.ts | 16 +- .../api/src/router/effort-rule-support.ts | 37 +- packages/api/src/router/effort-rule.ts | 2 +- .../router/entitlement-procedure-support.ts | 13 +- .../api/src/router/estimate-commercial.ts | 8 +- .../api/src/router/estimate-demand-lines.ts | 4 +- packages/api/src/router/estimate-phasing.ts | 18 +- .../src/router/estimate-procedure-support.ts | 43 +- packages/api/src/router/estimate-read.ts | 63 +- .../src/router/estimate-version-workflow.ts | 11 +- packages/api/src/router/estimate.ts | 22 +- ...experience-multiplier-procedure-support.ts | 20 +- .../router/experience-multiplier-support.ts | 33 +- .../api/src/router/experience-multiplier.ts | 2 +- .../holiday-calendar-procedure-support.ts | 35 +- .../holiday-calendar-resolution-read.ts | 112 +- .../src/router/holiday-calendar-support.ts | 21 +- .../router/holiday-calendar-write-support.ts | 20 +- packages/api/src/router/holiday-calendar.ts | 6 +- .../router/import-export-procedure-support.ts | 4 +- .../src/router/insights-procedure-support.ts | 17 +- packages/api/src/router/invite.ts | 14 +- .../management-level-procedure-support.ts | 6 +- .../src/router/management-level-support.ts | 8 +- packages/api/src/router/management-level.ts | 5 +- .../notification-task-procedure-support.ts | 12 +- .../src/router/org-unit-procedure-support.ts | 22 +- packages/api/src/router/org-unit-support.ts | 29 +- packages/api/src/router/org-unit.ts | 2 +- packages/api/src/router/project-cost-read.ts | 4 +- packages/api/src/router/project-cover.ts | 2 +- .../api/src/router/project-identifier-read.ts | 26 +- packages/api/src/router/project-lifecycle.ts | 46 +- packages/api/src/router/project-mutations.ts | 6 +- .../src/router/project-planning-read-model.ts | 15 +- .../src/router/project-procedure-support.ts | 23 +- .../api/src/router/project-read-shared.ts | 32 +- .../api/src/router/project-shoring-ratio.ts | 54 +- .../src/router/rate-card-procedure-support.ts | 41 +- packages/api/src/router/rate-card-read.ts | 62 +- .../api/src/router/rate-card-write-support.ts | 47 +- packages/api/src/router/rate-card.ts | 2 +- .../src/router/report-resource-month-query.ts | 105 +- .../report-template-procedure-support.ts | 16 +- packages/api/src/router/resource-analytics.ts | 29 +- .../api/src/router/resource-capacity-read.ts | 144 +- .../src/router/resource-capacity-shared.ts | 24 +- .../src/router/resource-marketplace-read.ts | 69 +- packages/api/src/router/resource-mutations.ts | 19 +- .../src/router/resource-owned-read-access.ts | 6 +- .../api/src/router/resource-read-shared.ts | 83 +- .../api/src/router/resource-skill-import.ts | 8 +- ...resource-summary-read-procedure-support.ts | 85 +- .../api/src/router/role-procedure-support.ts | 17 +- packages/api/src/router/role-support.ts | 51 +- packages/api/src/router/role.ts | 2 +- .../src/router/scenario-procedure-support.ts | 2 +- packages/api/src/router/scenario-shared.ts | 9 +- packages/api/src/router/settings-support.ts | 211 +- .../router/staffing-best-project-resource.ts | 230 +- .../api/src/router/staffing-capacity-read.ts | 4 +- .../src/router/staffing-capacity-summary.ts | 32 +- packages/api/src/router/staffing-shared.ts | 37 +- .../src/router/staffing-suggestions-read.ts | 8 +- .../src/router/system-role-config-support.ts | 6 +- ...allocation-assignment-procedure-support.ts | 7 +- ...timeline-allocation-batch-shift-support.ts | 2 +- .../timeline-allocation-fragment-support.ts | 26 +- .../timeline-allocation-inline-support.ts | 6 +- ...line-allocation-mutation-schema-support.ts | 2 +- .../router/timeline-allocation-mutations.ts | 4 +- .../timeline-allocation-procedure-support.ts | 2 +- ...imeline-allocation-quick-assign-support.ts | 2 +- .../timeline-allocation-router-support.ts | 7 +- .../timeline-allocation-shift-support.ts | 8 +- .../timeline-allocation-update-support.ts | 6 +- .../src/router/timeline-cost-load-support.ts | 37 +- .../api/src/router/timeline-cost-support.ts | 9 +- .../router/timeline-entry-query-support.ts | 6 +- .../router/timeline-holiday-load-support.ts | 4 +- .../src/router/timeline-holiday-support.ts | 2 +- packages/api/src/router/timeline-mutations.ts | 4 +- .../router/timeline-project-load-support.ts | 2 +- .../timeline-project-procedure-support.ts | 15 +- .../router/timeline-project-query-support.ts | 2 +- .../router/timeline-project-read-support.ts | 11 +- .../api/src/router/timeline-project-read.ts | 6 +- .../api/src/router/timeline-read-shared.ts | 19 +- .../router/timeline-shift-mutation-support.ts | 6 +- .../api/src/router/timeline-shift-planning.ts | 6 +- .../timeline-shift-procedure-support.ts | 7 +- .../router/timeline-shift-router-support.ts | 2 +- .../api/src/router/timeline-shift-support.ts | 13 +- .../api/src/router/user-procedure-support.ts | 10 +- .../user-self-service-procedure-support.ts | 16 +- .../utilization-category-procedure-support.ts | 5 +- .../router/utilization-category-support.ts | 7 +- .../api/src/router/vacation-create-support.ts | 108 +- .../router/vacation-management-procedures.ts | 6 +- .../src/router/vacation-management-support.ts | 8 +- .../src/router/vacation-public-holidays.ts | 9 +- .../api/src/router/vacation-read-support.ts | 21 +- packages/api/src/router/vacation-read.ts | 142 +- .../api/src/router/vacation-side-effects.ts | 17 +- packages/api/src/router/webhook-support.ts | 2 +- packages/api/src/sse/event-bus.ts | 126 +- packages/api/src/sse/subscription-policy.ts | 7 +- packages/api/src/trpc.ts | 12 +- packages/api/tsconfig.json | 2 +- packages/application/package.json | 12 +- .../allocation-entry-resolution.test.ts | 3 +- .../allocation-entry-updates.test.ts | 9 +- .../__tests__/allocation-read-model.test.ts | 2 +- ...-estimate-handoff-planning-entries.test.ts | 2 +- .../__tests__/count-planning-entries.test.ts | 2 +- .../src/__tests__/demand-assignment.test.ts | 22 +- .../__tests__/entitlement-operations.test.ts | 6 +- .../src/__tests__/estimate-operations.test.ts | 2 +- .../src/__tests__/estimate.test.ts | 2 +- .../__tests__/fill-demand-requirement.test.ts | 27 +- .../src/__tests__/fill-open-demand.test.ts | 7 +- .../src/__tests__/read-workbook.test.ts | 2 +- .../split-allocation-read-model.test.ts | 6 +- .../application/src/lib/resource-capacity.ts | 92 +- .../apply-demand-requirement-fill-progress.ts | 4 +- .../allocation/build-allocation-read-model.ts | 2 +- .../build-split-allocation-read-model.ts | 5 +- .../allocation/chargeability-bookings.ts | 2 +- ...count-estimate-handoff-planning-entries.ts | 2 +- .../allocation/count-planning-entries.ts | 7 +- .../use-cases/allocation/create-assignment.ts | 18 +- .../allocation/create-demand-requirement.ts | 4 +- .../allocation/delete-allocation-entry.ts | 2 +- .../use-cases/allocation/delete-assignment.ts | 9 +- .../allocation/delete-demand-requirement.ts | 2 +- ...ill-demand-requirement-with-legacy-sync.ts | 16 +- .../allocation/fill-demand-requirement.ts | 15 +- .../use-cases/allocation/fill-open-demand.ts | 4 +- .../allocation/list-assignment-bookings.ts | 2 +- .../allocation/load-allocation-entry.ts | 4 +- .../allocation/update-allocation-entry.ts | 4 +- .../use-cases/allocation/update-assignment.ts | 9 +- .../allocation/update-demand-requirement.ts | 4 +- .../dashboard/get-budget-forecast.ts | 145 +- .../dashboard/get-chargeability-overview.ts | 73 +- .../src/use-cases/dashboard/get-demand.ts | 155 +- .../src/use-cases/dashboard/get-overview.ts | 61 +- .../src/use-cases/dashboard/get-peak-times.ts | 64 +- .../use-cases/dashboard/get-project-health.ts | 154 +- .../src/use-cases/dashboard/get-skill-gaps.ts | 19 +- .../dashboard/get-top-value-resources.ts | 87 +- .../load-dashboard-planning-read-model.ts | 4 +- .../src/use-cases/dashboard/shared.ts | 4 +- .../dispo-import/assess-import-readiness.ts | 21 +- .../dispo-import/build-dispo-maps.ts | 43 +- .../dispo-import/commit-dispo-batch-types.ts | 6 +- .../dispo-import/commit-dispo-import-batch.ts | 471 +-- .../dispo-import/determine-placement.ts | 53 +- .../parse-chargeability-workbook.ts | 31 +- .../dispo-import/parse-dispo-matrix.ts | 125 +- .../parse-dispo-roster-workbook.ts | 34 +- .../parse-resource-roster-master-workbook.ts | 2 +- .../src/use-cases/dispo-import/shared.ts | 6 +- .../stage-chargeability-resources.ts | 17 +- .../dispo-import/stage-dispo-planning.ts | 21 +- .../dispo-import/stage-dispo-projects.ts | 19 +- .../stage-dispo-roster-resources.ts | 12 +- .../dispo-import/stage-reference-data.ts | 13 +- .../use-cases/dispo-import/tbd-projects.ts | 19 +- .../dispo-import/validate-dispo-batch.ts | 13 +- .../entitlement/read-entitlement-balance.ts | 326 +- .../use-cases/entitlement/set-entitlement.ts | 15 +- .../use-cases/entitlement/sync-entitlement.ts | 46 +- .../src/use-cases/estimate/clone-estimate.ts | 4 +- .../src/use-cases/estimate/create-estimate.ts | 60 +- .../estimate/create-planning-handoff.ts | 13 +- .../src/use-cases/estimate/list-estimates.ts | 7 +- .../src/use-cases/estimate/shared.ts | 11 +- .../estimate/update-estimate-draft.ts | 36 +- .../src/use-cases/estimate/version-actions.ts | 67 +- .../recompute-resource-value-scores.ts | 23 +- .../use-cases/vacation/approve-vacation.ts | 2 +- .../src/use-cases/vacation/cancel-vacation.ts | 5 +- .../src/use-cases/vacation/reject-vacation.ts | 11 +- packages/application/tsconfig.json | 2 +- packages/db/package.json | 6 +- packages/db/src/destructive-db-guard.test.ts | 51 +- packages/db/src/generate-excel.ts | 182 +- packages/db/src/holiday-calendar-seed-data.ts | 51 +- packages/db/src/import-dispo-batch.ts | 39 +- packages/db/src/load-workspace-env.test.ts | 2 +- packages/db/src/reset-dispo-import.ts | 26 +- packages/db/src/safe-destructive-env.ts | 18 +- packages/db/src/seed-dispo-v2.ts | 126 +- packages/db/src/seed-holiday-calendars.ts | 17 +- .../db/src/seed-holiday-demo-resources.ts | 27 +- packages/db/src/seed.ts | 2685 ++++++++++++++--- .../db/src/system-role-config-defaults.ts | 2 +- packages/db/src/update-blueprints.ts | 328 +- packages/db/src/update-excel.mjs | 70 +- packages/db/tsconfig.json | 2 +- packages/engine/package.json | 6 +- .../engine/src/__tests__/availability.test.ts | 58 +- .../blueprint-validator-redos.test.ts | 2 +- packages/engine/src/__tests__/budget.test.ts | 44 +- .../src/__tests__/calculator-rules.test.ts | 4 +- .../src/__tests__/commercial-terms.test.ts | 6 +- .../estimate-export-serializer.test.ts | 6 +- .../engine/src/__tests__/recurrence.test.ts | 4 +- .../engine/src/__tests__/rules-engine.test.ts | 24 +- .../src/__tests__/sah-calculator.test.ts | 2 +- .../src/allocation/availability-validator.ts | 2 +- packages/engine/src/allocation/calculator.ts | 33 +- .../engine/src/allocation/chargeability.ts | 23 +- packages/engine/src/allocation/recurrence.ts | 4 +- packages/engine/src/blueprint/validator.ts | 2 +- packages/engine/src/budget/monitor.ts | 17 +- .../engine/src/estimate/commercial-terms.ts | 21 +- .../engine/src/estimate/export-serializer.ts | 57 +- packages/engine/src/estimate/metrics.ts | 27 +- .../engine/src/estimate/weekly-phasing.ts | 4 +- packages/engine/src/rules/default-rules.ts | 2 +- packages/engine/src/rules/engine.ts | 4 +- packages/engine/src/sah/calculator.ts | 17 +- packages/engine/src/shift/validator.ts | 29 +- packages/engine/tsconfig.json | 2 +- packages/shared/package.json | 4 +- .../src/constants/data-classification.ts | 2 +- .../shared/src/schemas/blueprint.schema.ts | 2 +- packages/shared/tsconfig.json | 2 +- packages/staffing/package.json | 6 +- .../__tests__/capacity-analyzer-perf.test.ts | 2 +- .../capacity-analyzer-vacation.test.ts | 2 +- .../src/__tests__/capacity-analyzer.test.ts | 2 +- packages/staffing/src/capacity-analyzer.ts | 23 +- packages/staffing/src/skill-matcher.ts | 19 +- packages/staffing/src/value-scorer.ts | 13 +- packages/staffing/tsconfig.json | 2 +- packages/ui/package.json | 2 +- plan.md | 21 +- pnpm-lock.yaml | 185 +- .../v2-architecture-proposal-2026-03-11.md | 95 +- samples/Dispov2/plan-chargeability-report.md | 99 +- samples/Dispov2/plan-client-wbs-model.md | 31 +- samples/Dispov2/plan-country-sah-fte.md | 38 +- samples/Dispov2/plan-org-unit-hierarchy.md | 54 +- samples/Dispov2/plan-overview.md | 49 +- samples/Dispov2/plan-resource-extensions.md | 96 +- .../Dispov2/plan-utilization-categories.md | 37 +- samples/generate_skillmatrix.mjs | 2 +- scripts/export-dev-seed.mjs | 2 +- scripts/prisma-with-env.mjs | 4 +- scripts/stop.sh | 6 +- tooling/deploy/.env.production.example | 4 +- tooling/docker/app-dev-start.sh | 14 +- tooling/eslint/package.json | 2 +- tooling/prettier/package.json | 2 +- tooling/typescript/package.json | 6 +- 943 files changed, 24548 insertions(+), 16832 deletions(-) diff --git a/.env.example b/.env.example index d49b85c..ea68c15 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ # ───────────────────────────────────────────────────────────────────────────── -# CapaKraken — environment variable reference +# Nexus — environment variable reference # # Copy this file to .env and fill in the values before running the app. # Lines starting with # are comments. Lines with no value are optional. @@ -12,7 +12,7 @@ # REQUIRED — Public URL of the app (with scheme, no trailing slash). # Used in email links (invites, password reset) and as the Auth.js base URL. # Must use https:// in production. -NEXTAUTH_URL=https://capakraken.example.com +NEXTAUTH_URL=https://nexus.example.com # REQUIRED — Secret used to sign and encrypt JWTs and session cookies. # Generate one with: openssl rand -base64 32 @@ -65,7 +65,7 @@ REDIS_PASSWORD= # SMTP_PORT=587 # SMTP_USER=no-reply@example.com # SMTP_PASSWORD= -# SMTP_FROM=CapaKraken +# SMTP_FROM=Nexus # SMTP_TLS=true # "true" = SMTPS (port 465); "false" = STARTTLS or plain # ─── pgAdmin (dev / Docker Compose only) ───────────────────────────────────── @@ -74,8 +74,8 @@ REDIS_PASSWORD= # Used as the password for the pgAdmin web UI (http://localhost:5050). PGADMIN_PASSWORD= -# Email shown on the pgAdmin login screen (default: admin@capakraken.dev). -# PGADMIN_EMAIL=admin@capakraken.dev +# Email shown on the pgAdmin login screen (default: admin@nexus.dev). +# PGADMIN_EMAIL=admin@nexus.dev # ─── Logging ───────────────────────────────────────────────────────────────── diff --git a/.gitea/gitea_compose_qnap_all_in_one.md b/.gitea/gitea_compose_qnap_all_in_one.md index 91ae662..aed1f27 100644 --- a/.gitea/gitea_compose_qnap_all_in_one.md +++ b/.gitea/gitea_compose_qnap_all_in_one.md @@ -191,7 +191,7 @@ Absolute Pfade unter `/share/Container/gitea/` sind **außerhalb** der Container ## Repo-Secrets für CI/CD -Im capakraken-Repo → **Settings → Actions → Secrets** eintragen: +Im nexus-Repo → **Settings → Actions → Secrets** eintragen: | Secret | Zweck | | ----------------------- | -------------------------------------- | diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 4ae8a61..c3ce660 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -2,7 +2,7 @@ ## Reporting a Vulnerability -If you discover a security vulnerability in CapaKraken, please report it responsibly. +If you discover a security vulnerability in Nexus, please report it responsibly. **Do not** open a public GitHub issue for security vulnerabilities. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f50738c..4c6fd33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,7 +114,7 @@ jobs: run: pnpm db:generate - name: Run assistant split regression - run: pnpm --filter @capakraken/api test:assistant-split + run: pnpm --filter @nexus/api test:assistant-split # ────────────────────────────────────────────── # Lint — ~20s, no services needed @@ -204,13 +204,13 @@ jobs: - name: Run unit tests with coverage run: | - pnpm --filter @capakraken/web test:unit -- --coverage - pnpm --filter @capakraken/engine exec vitest run --coverage - pnpm --filter @capakraken/staffing exec vitest run --coverage - pnpm --filter @capakraken/api exec vitest run --coverage - pnpm --filter @capakraken/application exec vitest run --coverage - pnpm --filter @capakraken/shared exec vitest run --coverage - pnpm --filter @capakraken/db test:unit + pnpm --filter @nexus/web test:unit -- --coverage + pnpm --filter @nexus/engine exec vitest run --coverage + pnpm --filter @nexus/staffing exec vitest run --coverage + pnpm --filter @nexus/api exec vitest run --coverage + pnpm --filter @nexus/application exec vitest run --coverage + pnpm --filter @nexus/shared exec vitest run --coverage + pnpm --filter @nexus/db test:unit - name: Upload coverage reports uses: actions/upload-artifact@v4 @@ -274,7 +274,7 @@ jobs: restore-keys: nextjs-${{ hashFiles('pnpm-lock.yaml') }}- - name: Build - run: pnpm --filter @capakraken/web exec next build + run: pnpm --filter @nexus/web exec next build # ────────────────────────────────────────────── # E2E — depends on build, needs PostgreSQL + Redis @@ -364,11 +364,11 @@ jobs: - name: Install Playwright browsers if: steps.playwright-cache.outputs.cache-hit != 'true' - run: pnpm --filter @capakraken/web exec playwright install --with-deps chromium + run: pnpm --filter @nexus/web exec playwright install --with-deps chromium - name: Install Playwright system deps if: steps.playwright-cache.outputs.cache-hit == 'true' - run: pnpm --filter @capakraken/web exec playwright install-deps chromium + run: pnpm --filter @nexus/web exec playwright install-deps chromium - name: Install psql (debug schema state) run: sudo apt-get update && sudo apt-get install -y --no-install-recommends postgresql-client @@ -416,7 +416,7 @@ jobs: psql -h "$PG_IP" -U capakraken -d capakraken_test -v ON_ERROR_STOP=1 \ -c "DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO capakraken; GRANT ALL ON SCHEMA public TO public;" echo "--- prisma db push ---" - DATABASE_URL="$PINNED_URL" pnpm --filter @capakraken/db exec prisma db push --schema ./prisma/schema.prisma --accept-data-loss --skip-generate + DATABASE_URL="$PINNED_URL" pnpm --filter @nexus/db exec prisma db push --schema ./prisma/schema.prisma --accept-data-loss --skip-generate echo "--- tables in public after push ---" psql -h "$PG_IP" -U capakraken -d capakraken_test -v ON_ERROR_STOP=1 -At \ -c "SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename" \ @@ -438,7 +438,7 @@ jobs: # and restarts mid-run, producing cascading ECONNREFUSED failures # unrelated to test content. Scope CI to smoke.spec.ts; full suite # is run locally / in a dedicated nightly job. - run: pnpm --filter @capakraken/web exec playwright test e2e/smoke.spec.ts + run: pnpm --filter @nexus/web exec playwright test e2e/smoke.spec.ts - name: Upload Playwright report uses: actions/upload-artifact@v4 @@ -576,7 +576,7 @@ jobs: ln -sfn /app/packages/db/node_modules/@prisma /app/scripts/node_modules/@prisma ln -sfn /app/packages/db/node_modules/@node-rs /app/scripts/node_modules/@node-rs ln -sfn /app/packages/db/node_modules/.prisma /app/scripts/node_modules/.prisma - node /app/scripts/setup-admin.mjs --email admin@capakraken.dev --name Admin --password admin123 + node /app/scripts/setup-admin.mjs --email admin@nexus.dev --name Admin --password admin123 ' - name: Set up Node.js 20 diff --git a/CLAUDE.md b/CLAUDE.md index 2b43be5..5100b1b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,8 +1,8 @@ -# CapaKraken +# Nexus ## Ziel -CapaKraken ist ein Ressourcenplanungs- und Projektbesetzungs-Tool fuer eine 3D-Produktionsumgebung. Der aktuelle Produktkern umfasst Timeline-Planung, Kapazitaets- und Budgetsicht, Rollenmanagement, Blueprint-basierte dynamische Felder, Skill-Matrix-Workflows und einen AI-unterstuetzten Staffing-/Profilbereich. +Nexus ist ein Ressourcenplanungs- und Projektbesetzungs-Tool fuer eine 3D-Produktionsumgebung. Der aktuelle Produktkern umfasst Timeline-Planung, Kapazitaets- und Budgetsicht, Rollenmanagement, Blueprint-basierte dynamische Felder, Skill-Matrix-Workflows und einen AI-unterstuetzten Staffing-/Profilbereich. ## Tech Stack @@ -19,7 +19,7 @@ CapaKraken ist ein Ressourcenplanungs- und Projektbesetzungs-Tool fuer eine 3D-P ## Monorepo-Struktur ```text -capakraken/ +nexus/ ├── apps/web ├── packages/shared ├── packages/db @@ -41,7 +41,7 @@ capakraken/ ## Quality Gates - `pnpm test:unit` -- `pnpm --filter @capakraken/web exec tsc --noEmit` +- `pnpm --filter @nexus/web exec tsc --noEmit` - `pnpm lint` ## Dokumente diff --git a/Dockerfile.dev b/Dockerfile.dev index e9799dd..40164dd 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -26,7 +26,7 @@ RUN pnpm install --frozen-lockfile COPY . . # Generate Prisma client -RUN pnpm --filter @capakraken/db db:generate +RUN pnpm --filter @nexus/db db:generate EXPOSE 3100 diff --git a/Dockerfile.prod b/Dockerfile.prod index 9787e2e..088c60f 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -39,7 +39,7 @@ COPY --from=deps /app/ ./ COPY . . # Generate Prisma client -RUN pnpm --filter @capakraken/db db:generate +RUN pnpm --filter @nexus/db db:generate # Build the Next.js application ENV NEXT_TELEMETRY_DISABLED=1 @@ -63,7 +63,7 @@ RUN NEXTAUTH_URL="$NEXTAUTH_URL" \ AUTH_SECRET="$AUTH_SECRET" \ DATABASE_URL="$DATABASE_URL" \ REDIS_URL="$REDIS_URL" \ - pnpm --filter @capakraken/web build + pnpm --filter @nexus/web build # ============================================================ # Stage 3: Migration runner @@ -72,7 +72,7 @@ FROM builder AS migrator ENV NODE_ENV=production -CMD ["pnpm", "--filter", "@capakraken/db", "db:migrate:deploy"] +CMD ["pnpm", "--filter", "@nexus/db", "db:migrate:deploy"] # ============================================================ # Stage 4: Production runtime diff --git a/LEARNINGS.md b/LEARNINGS.md index 0f72426..2da031b 100644 --- a/LEARNINGS.md +++ b/LEARNINGS.md @@ -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/_/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` 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 `` 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(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(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(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(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(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(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(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(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(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(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 `` 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` 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_` localStorage (getrennt von `colvis_` 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. diff --git a/README.md b/README.md index f2dd043..a7a1b75 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@

- CapaKraken Dashboard + Nexus Dashboard

-

CapaKraken

+

Nexus

Resource & Capacity Planning for 3D Production Studios
@@ -25,7 +25,7 @@ ## About -CapaKraken is a full-stack resource planning and project staffing application built for 3D production environments -- VFX studios, animation houses, and automotive visualization teams. It replaces spreadsheet-based capacity planning with a real-time, multi-user web application that provides a single source of truth for who is working on what, when, and at what cost. +Nexus is a full-stack resource planning and project staffing application built for 3D production environments -- VFX studios, animation houses, and automotive visualization teams. It replaces spreadsheet-based capacity planning with a real-time, multi-user web application that provides a single source of truth for who is working on what, when, and at what cost. The application was designed from the ground up for the unique challenges of creative production: fluctuating team sizes, overlapping project phases, mixed chargeability models (client-billable vs. internal vs. BD), complex holiday calendars across multiple countries, and the need to forecast resource availability months in advance. @@ -39,7 +39,7 @@ The application was designed from the ground up for the unique challenges of cre Timeline - Resource View

-The timeline is the centerpiece of CapaKraken. It provides a visual, interactive view of all resource allocations across projects. +The timeline is the centerpiece of Nexus. It provides a visual, interactive view of all resource allocations across projects. - **Resource View** -- see all allocations for each person, with color-coded project bars stacked in sub-lanes when they overlap - **Project View** -- flip the perspective to see all resources assigned to each project @@ -97,18 +97,18 @@ A structured list view of all allocations with: Each user gets a personal dashboard they can customize with drag-and-drop widgets: -| Widget | Description | -|--------|-------------| -| **Overview Stats** | Total resources, active projects, allocations, and budget utilization at a glance | -| **My Projects** | Quick access to projects where the current user is assigned or responsible | -| **Resource Table** | Filterable EID list with utilization percentages and chargeability indicators | -| **Project Overview** | All projects with cost, person days, and status badges | -| **Peak Times** | Bar chart showing booked hours vs. capacity over time, with department breakdown | -| **Demand View** | Staffing demand vs. supply by project, with unfilled headcount tracking | -| **Chargeability Overview** | Leaderboard of resources ranked by chargeability score | -| **Budget Forecast** | Budget burn rate and projected cost per active project | -| **Skill Gap Analysis** | Top skill shortages comparing open demand against available supply | -| **Project Health** | Composite health score per project (budget, staffing, timeline) | +| Widget | Description | +| -------------------------- | --------------------------------------------------------------------------------- | +| **Overview Stats** | Total resources, active projects, allocations, and budget utilization at a glance | +| **My Projects** | Quick access to projects where the current user is assigned or responsible | +| **Resource Table** | Filterable EID list with utilization percentages and chargeability indicators | +| **Project Overview** | All projects with cost, person days, and status badges | +| **Peak Times** | Bar chart showing booked hours vs. capacity over time, with department breakdown | +| **Demand View** | Staffing demand vs. supply by project, with unfilled headcount tracking | +| **Chargeability Overview** | Leaderboard of resources ranked by chargeability score | +| **Budget Forecast** | Budget burn rate and projected cost per active project | +| **Skill Gap Analysis** | Top skill shortages comparing open demand against available supply | +| **Project Health** | Composite health score per project (budget, staffing, timeline) | Widgets are resizable, and the layout persists per user. An **Add Widget** catalog lets users browse available widgets with descriptions and default sizes. @@ -172,23 +172,23 @@ A full project estimation workflow: ## Tech Stack -| Layer | Technology | Purpose | -|-------|-----------|---------| -| **Frontend** | Next.js 15 (App Router), React 19 | Server components, streaming, file-based routing | -| **Styling** | Tailwind CSS v4 | Utility-first CSS with custom design tokens | -| **API** | tRPC v11 | End-to-end type-safe RPC between client and server | -| **Database** | PostgreSQL 16 via Prisma ORM | Relational data with JSONB for dynamic fields | -| **Auth** | Auth.js v5 | Session management, Argon2 passwords, TOTP MFA | -| **Realtime** | SSE + Redis pub/sub | Live updates without WebSocket complexity | -| **AI** | Azure OpenAI / Gemini | Staffing suggestions, skill profile generation | -| **Monorepo** | pnpm workspaces + Turborepo | Incremental builds, shared configs, dependency isolation | -| **Testing** | Vitest + Playwright | Unit tests (engine, shared) and E2E browser tests | -| **Containerization** | Docker Compose | Dev and production stacks with health checks | +| Layer | Technology | Purpose | +| -------------------- | --------------------------------- | -------------------------------------------------------- | +| **Frontend** | Next.js 15 (App Router), React 19 | Server components, streaming, file-based routing | +| **Styling** | Tailwind CSS v4 | Utility-first CSS with custom design tokens | +| **API** | tRPC v11 | End-to-end type-safe RPC between client and server | +| **Database** | PostgreSQL 16 via Prisma ORM | Relational data with JSONB for dynamic fields | +| **Auth** | Auth.js v5 | Session management, Argon2 passwords, TOTP MFA | +| **Realtime** | SSE + Redis pub/sub | Live updates without WebSocket complexity | +| **AI** | Azure OpenAI / Gemini | Staffing suggestions, skill profile generation | +| **Monorepo** | pnpm workspaces + Turborepo | Incremental builds, shared configs, dependency isolation | +| **Testing** | Vitest + Playwright | Unit tests (engine, shared) and E2E browser tests | +| **Containerization** | Docker Compose | Dev and production stacks with health checks | ### Monorepo Structure ``` -capakraken/ +nexus/ | +-- apps/ | +-- web/ Next.js 15 application (frontend + API routes) @@ -263,18 +263,18 @@ capakraken/ ### Prerequisites -| Requirement | Minimum Version | Check | -|-------------|----------------|-------| -| **Node.js** | 20.x | `node --version` | -| **pnpm** | 9.x | `pnpm --version` | -| **Docker** | 24+ | `docker --version` | -| **Docker Compose** | v2 | `docker compose version` | +| Requirement | Minimum Version | Check | +| ------------------ | --------------- | ------------------------ | +| **Node.js** | 20.x | `node --version` | +| **pnpm** | 9.x | `pnpm --version` | +| **Docker** | 24+ | `docker --version` | +| **Docker Compose** | v2 | `docker compose version` | ### 1. Clone and configure ```bash -git clone https://gitea.hartmut-noerenberg.com/Hartmut/plANARCHY.git capakraken -cd capakraken +git clone https://gitea.hartmut-noerenberg.com/Hartmut/plANARCHY.git nexus +cd nexus ``` Create your environment file: @@ -315,13 +315,13 @@ This single command will: You'll see output like: ``` -Starting CapaKraken... +Starting Nexus... Starting PostgreSQL + Redis... Waiting for PostgreSQL... Starting app container on port 3100... Waiting for server (up to 90s)... -CapaKraken is running! +Nexus is running! { "status": "ok", "database": "connected", @@ -372,13 +372,13 @@ This populates the database with sample clients, projects, resources, allocation When running with Docker Compose, the following services are available: -| Service | URL | Purpose | -|---------|-----|---------| -| **CapaKraken App** | [localhost:3100](http://localhost:3100) | Main application | -| **MailHog** | [localhost:8025](http://localhost:8025) | Email testing UI -- catches all outgoing emails (invitations, password resets, notifications) | -| **pgAdmin** | [localhost:5050](http://localhost:5050) | Visual database administration | -| **PostgreSQL** | `localhost:5433` | Direct database access (user: `capakraken`, db: `capakraken`) | -| **Redis** | `localhost:6380` | Cache, rate limiting, and SSE pub/sub | +| Service | URL | Purpose | +| -------------- | --------------------------------------- | --------------------------------------------------------------------------------------------- | +| **Nexus App** | [localhost:3100](http://localhost:3100) | Main application | +| **MailHog** | [localhost:8025](http://localhost:8025) | Email testing UI -- catches all outgoing emails (invitations, password resets, notifications) | +| **pgAdmin** | [localhost:5050](http://localhost:5050) | Visual database administration | +| **PostgreSQL** | `localhost:5433` | Direct database access (user: `nexus`, db: `nexus`) | +| **Redis** | `localhost:6380` | Cache, rate limiting, and SSE pub/sub | --- @@ -386,50 +386,50 @@ When running with Docker Compose, the following services are available: ### Application Lifecycle -| Command | Description | -|---------|-------------| -| `bash scripts/start.sh` | Start all services (PostgreSQL, Redis, app) | -| `bash scripts/stop.sh` | Stop all services gracefully | -| `bash scripts/restart.sh` | Full stop + start cycle | +| Command | Description | +| ------------------------- | ------------------------------------------- | +| `bash scripts/start.sh` | Start all services (PostgreSQL, Redis, app) | +| `bash scripts/stop.sh` | Stop all services gracefully | +| `bash scripts/restart.sh` | Full stop + start cycle | ### Development -| Command | Description | -|---------|-------------| -| `pnpm dev` | Start Next.js dev server with hot reload (host-native) | -| `pnpm build` | Production build (standalone output) | -| `pnpm lint` | Run ESLint across all packages | -| `pnpm format` | Format all files with Prettier | -| `pnpm test:unit` | Run unit tests via Vitest | -| `pnpm test:e2e` | Run end-to-end tests via Playwright | -| `pnpm typecheck` | TypeScript type checking across all packages | +| Command | Description | +| ------------------------- | -------------------------------------------------------- | +| `pnpm dev` | Start Next.js dev server with hot reload (host-native) | +| `pnpm build` | Production build (standalone output) | +| `pnpm lint` | Run ESLint across all packages | +| `pnpm format` | Format all files with Prettier | +| `pnpm test:unit` | Run unit tests via Vitest | +| `pnpm test:e2e` | Run end-to-end tests via Playwright | +| `pnpm typecheck` | TypeScript type checking across all packages | | `pnpm check:architecture` | Verify architecture guardrails (import boundaries, etc.) | ### Database -| Command | Description | -|---------|-------------| -| `pnpm db:generate` | Regenerate Prisma client after schema changes | -| `pnpm db:migrate` | Create and apply new migrations | -| `pnpm db:push` | Push schema changes directly (no migration file) | -| `pnpm db:studio` | Open Prisma Studio (visual data browser) | -| `pnpm db:seed` | Seed the database with demo data | -| `pnpm db:doctor` | Run health checks on database state | -| `pnpm db:seed:export` | Export current DB state as a seed file | -| `pnpm db:seed:import` | Import a previously exported seed file | +| Command | Description | +| --------------------- | ------------------------------------------------ | +| `pnpm db:generate` | Regenerate Prisma client after schema changes | +| `pnpm db:migrate` | Create and apply new migrations | +| `pnpm db:push` | Push schema changes directly (no migration file) | +| `pnpm db:studio` | Open Prisma Studio (visual data browser) | +| `pnpm db:seed` | Seed the database with demo data | +| `pnpm db:doctor` | Run health checks on database state | +| `pnpm db:seed:export` | Export current DB state as a seed file | +| `pnpm db:seed:import` | Import a previously exported seed file | --- ## Production Deployment -CapaKraken ships with a production-ready Docker Compose stack and deployment automation. +Nexus ships with a production-ready Docker Compose stack and deployment automation. ### Quick Deploy ```bash # Configure required secrets -export APP_IMAGE=ghcr.io/your-org/capakraken-app:latest -export MIGRATOR_IMAGE=ghcr.io/your-org/capakraken-migrator:latest +export APP_IMAGE=ghcr.io/your-org/nexus-app:latest +export MIGRATOR_IMAGE=ghcr.io/your-org/nexus-migrator:latest export POSTGRES_PASSWORD=$(openssl rand -hex 32) export REDIS_PASSWORD=$(openssl rand -hex 32) export NEXTAUTH_SECRET=$(openssl rand -base64 32) @@ -454,35 +454,35 @@ bash tooling/deploy/deploy-compose.sh production See [`.env.example`](.env.example) for the complete reference with inline documentation. Summary of key variables: -| Variable | Required | Default | Description | -|----------|----------|---------|-------------| -| `NEXTAUTH_URL` | Yes | -- | Public URL of the application | -| `NEXTAUTH_SECRET` | Yes | -- | Secret for JWT signing and session encryption | -| `DATABASE_URL` | Yes | `localhost:5433` | PostgreSQL connection string | -| `REDIS_PASSWORD` | Prod | -- | Redis authentication password | -| `REDIS_URL` | No | `redis://redis:6379` | Redis connection (auto-configured in Docker) | -| `SMTP_HOST` | No | -- | SMTP server for email delivery | -| `SMTP_PORT` | No | `587` | SMTP port | -| `SMTP_FROM` | No | `noreply@capakraken.dev` | Sender address for outgoing emails | -| `AZURE_OPENAI_API_KEY` | No | -- | Enables AI-assisted staffing features | -| `GEMINI_API_KEY` | No | -- | Alternative AI provider | -| `LOG_LEVEL` | No | `info` | Logging verbosity (trace/debug/info/warn/error) | -| `CRON_SECRET` | No | -- | Authenticates scheduled job endpoints | +| Variable | Required | Default | Description | +| ---------------------- | -------- | -------------------- | ----------------------------------------------- | +| `NEXTAUTH_URL` | Yes | -- | Public URL of the application | +| `NEXTAUTH_SECRET` | Yes | -- | Secret for JWT signing and session encryption | +| `DATABASE_URL` | Yes | `localhost:5433` | PostgreSQL connection string | +| `REDIS_PASSWORD` | Prod | -- | Redis authentication password | +| `REDIS_URL` | No | `redis://redis:6379` | Redis connection (auto-configured in Docker) | +| `SMTP_HOST` | No | -- | SMTP server for email delivery | +| `SMTP_PORT` | No | `587` | SMTP port | +| `SMTP_FROM` | No | `noreply@nexus.dev` | Sender address for outgoing emails | +| `AZURE_OPENAI_API_KEY` | No | -- | Enables AI-assisted staffing features | +| `GEMINI_API_KEY` | No | -- | Alternative AI provider | +| `LOG_LEVEL` | No | `info` | Logging verbosity (trace/debug/info/warn/error) | +| `CRON_SECRET` | No | -- | Authenticates scheduled job endpoints | --- ## Design Principles -| Principle | Implementation | -|-----------|---------------| -| **Money as integer cents** | All monetary values stored and calculated in cents to eliminate floating-point drift | -| **Strict TypeScript** | No `any` types, strict null checks, explicit Prisma casts at package boundaries | -| **Domain-driven packages** | Each bounded context (estimating, chargeability, staffing) lives in its own package with clear exports | -| **Pure engine logic** | Calculation packages have zero I/O dependencies -- they take data in and return results | -| **Real-time by default** | SSE pushes changes to all clients via Redis pub/sub; no polling | -| **Theme-aware UI** | CSS variable-based surface system with configurable accent colors and full dark mode | -| **Defensive data handling** | Nullable foreign keys handled explicitly; Prisma enums and JSONB cast at boundaries | -| **No speculative abstractions** | Build what's needed now; three similar lines beat a premature abstraction | +| Principle | Implementation | +| ------------------------------- | ------------------------------------------------------------------------------------------------------ | +| **Money as integer cents** | All monetary values stored and calculated in cents to eliminate floating-point drift | +| **Strict TypeScript** | No `any` types, strict null checks, explicit Prisma casts at package boundaries | +| **Domain-driven packages** | Each bounded context (estimating, chargeability, staffing) lives in its own package with clear exports | +| **Pure engine logic** | Calculation packages have zero I/O dependencies -- they take data in and return results | +| **Real-time by default** | SSE pushes changes to all clients via Redis pub/sub; no polling | +| **Theme-aware UI** | CSS variable-based surface system with configurable accent colors and full dark mode | +| **Defensive data handling** | Nullable foreign keys handled explicitly; Prisma enums and JSONB cast at boundaries | +| **No speculative abstractions** | Build what's needed now; three similar lines beat a premature abstraction | --- @@ -494,7 +494,7 @@ See [`.env.example`](.env.example) for the complete reference with inline docume 4. Run quality gates before submitting: ```bash pnpm test:unit - pnpm --filter @capakraken/web exec tsc --noEmit + pnpm --filter @nexus/web exec tsc --noEmit pnpm lint pnpm check:architecture ``` diff --git a/apps/web/e2e/a11y.spec.ts b/apps/web/e2e/a11y.spec.ts index d4d37b2..3fbd3d8 100644 --- a/apps/web/e2e/a11y.spec.ts +++ b/apps/web/e2e/a11y.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from "./a11y-fixture.js"; test.describe("Accessibility (axe-core)", () => { test.beforeEach(async ({ page }) => { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="email"]', "admin@nexus.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/, { timeout: 15_000 }); diff --git a/apps/web/e2e/admin.spec.ts b/apps/web/e2e/admin.spec.ts index 8c40945..62c1808 100644 --- a/apps/web/e2e/admin.spec.ts +++ b/apps/web/e2e/admin.spec.ts @@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test"; test.describe("Admin Pages", () => { test.beforeEach(async ({ page }) => { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="email"]', "admin@nexus.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/); @@ -12,39 +12,42 @@ test.describe("Admin Pages", () => { test("settings page loads", async ({ page }) => { await page.goto("/admin/settings"); await page.waitForLoadState("networkidle"); - await expect(page.locator("h1", { hasText: /System Settings/i })).toBeVisible({ timeout: 10000 }); + await expect(page.locator("h1", { hasText: /System Settings/i })).toBeVisible({ + timeout: 10000, + }); }); test("users page loads with user list", async ({ page }) => { await page.goto("/admin/users"); await page.waitForLoadState("networkidle"); - await expect(page.locator("h1", { hasText: /User Management/i })).toBeVisible({ timeout: 10000 }); + await expect(page.locator("h1", { hasText: /User Management/i })).toBeVisible({ + timeout: 10000, + }); // Should show a table with at least the admin user await expect(page.locator("table")).toBeVisible({ timeout: 10000 }); - await expect(page.locator("text=admin@capakraken.dev")).toBeVisible({ timeout: 10000 }); + await expect(page.locator("text=admin@nexus.dev")).toBeVisible({ timeout: 10000 }); }); test("roles page loads", async ({ page }) => { await page.goto("/roles"); await page.waitForLoadState("networkidle"); - await expect( - page.locator("h1").filter({ hasText: /Roles/i }), - ).toBeVisible({ timeout: 10000 }); + await expect(page.locator("h1").filter({ hasText: /Roles/i })).toBeVisible({ timeout: 10000 }); // Should show table or list of roles - await expect( - page.locator("table").or(page.locator("text=No roles")), - ).toBeVisible({ timeout: 10000 }); + await expect(page.locator("table").or(page.locator("text=No roles"))).toBeVisible({ + timeout: 10000, + }); }); test("blueprints page loads", async ({ page }) => { await page.goto("/admin/blueprints"); await page.waitForLoadState("networkidle"); - await expect( - page.locator("h1").filter({ hasText: /Blueprints/i }), - ).toBeVisible({ timeout: 10000 }); + await expect(page.locator("h1").filter({ hasText: /Blueprints/i })).toBeVisible({ + timeout: 10000, + }); // Should show blueprint cards or list from seed data await expect( - page.locator("table") + page + .locator("table") .or(page.locator("text=3D Content Production")) .or(page.locator("text=No blueprints")), ).toBeVisible({ timeout: 10000 }); diff --git a/apps/web/e2e/allocations.spec.ts b/apps/web/e2e/allocations.spec.ts index ee446c6..84f2b35 100644 --- a/apps/web/e2e/allocations.spec.ts +++ b/apps/web/e2e/allocations.spec.ts @@ -40,24 +40,28 @@ async function signIn(page: Page, email: string, password: string) { test.describe("Allocations", () => { test.beforeEach(async ({ page }) => { await freezeBrowserTime(page); - await signIn(page, "admin@capakraken.dev", "admin123"); + await signIn(page, "admin@nexus.dev", "admin123"); await page.goto("/allocations"); }); test("seeded assignments stay visibly rendered on first load", async ({ page }) => { await page.waitForLoadState("networkidle"); - await expect( - page.locator("h1").filter({ hasText: /Allocations|Planning/i }), - ).toBeVisible({ timeout: 10000 }); + await expect(page.locator("h1").filter({ hasText: /Allocations|Planning/i })).toBeVisible({ + timeout: 10000, + }); await expect(page.getByTestId("allocations-table")).toBeVisible({ timeout: 10000 }); await expect(page.getByTestId("allocations-empty-state")).toHaveCount(0); - await expect(page.getByTestId("allocation-group-header").first()).toBeVisible({ timeout: 10000 }); + await expect(page.getByTestId("allocation-group-header").first()).toBeVisible({ + timeout: 10000, + }); await expect(page.getByTestId("allocation-row").first()).toBeVisible({ timeout: 10000 }); expect(await page.getByTestId("allocation-row").count()).toBeGreaterThan(0); }); - test("explicitly restrictive filters show a visible empty state and can be reset", async ({ page }) => { + test("explicitly restrictive filters show a visible empty state and can be reset", async ({ + page, + }) => { await page.waitForLoadState("networkidle"); const projectFilter = page.getByPlaceholder("Filter by project…"); @@ -83,21 +87,23 @@ test.describe("Allocations", () => { await expect(newBtn).toBeVisible({ timeout: 10000 }); await newBtn.click(); await expect(page.getByTestId("allocation-modal")).toBeVisible({ timeout: 5000 }); - await expect(page.getByRole("heading", { name: /New (Assignment|Open Demand)/i })).toBeVisible(); + await expect( + page.getByRole("heading", { name: /New (Assignment|Open Demand)/i }), + ).toBeVisible(); await page.keyboard.press("Escape"); }); test("filter by status works", async ({ page }) => { await page.waitForLoadState("networkidle"); // Look for status filter chips or dropdown - const statusFilter = page.locator("button", { hasText: /Proposed|Confirmed|Active|Status/i }).first(); + const statusFilter = page + .locator("button", { hasText: /Proposed|Confirmed|Active|Status/i }) + .first(); if ((await statusFilter.count()) > 0) { await statusFilter.click(); await page.waitForTimeout(300); // After clicking a status filter, the page should still show the table - await expect( - page.locator("table").or(page.locator("text=No allocations")), - ).toBeVisible(); + await expect(page.locator("table").or(page.locator("text=No allocations"))).toBeVisible(); } }); @@ -108,17 +114,17 @@ test.describe("Allocations", () => { await colToggle.click(); await page.waitForTimeout(300); // A panel or dropdown with column checkboxes should appear - await expect( - page.locator("input[type='checkbox']").first(), - ).toBeVisible({ timeout: 3000 }); + await expect(page.locator("input[type='checkbox']").first()).toBeVisible({ timeout: 3000 }); await page.keyboard.press("Escape"); } }); - test("viewer sees a visible access error instead of an empty allocations page", async ({ browser }) => { + test("viewer sees a visible access error instead of an empty allocations page", async ({ + browser, + }) => { const page = await browser.newPage(); await freezeBrowserTime(page); - await signIn(page, "viewer@capakraken.dev", "viewer123"); + await signIn(page, "viewer@nexus.dev", "viewer123"); await page.goto("/allocations"); await page.waitForLoadState("networkidle"); diff --git a/apps/web/e2e/analytics.spec.ts b/apps/web/e2e/analytics.spec.ts index ae982d1..7949e16 100644 --- a/apps/web/e2e/analytics.spec.ts +++ b/apps/web/e2e/analytics.spec.ts @@ -2,7 +2,7 @@ import { expect, test, type Page } from "@playwright/test"; async function signIn(page: Page) { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="email"]', "admin@nexus.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/); @@ -71,8 +71,8 @@ test.describe("Analytics / Insights", () => { test("insights page loads without errors", async ({ page }) => { await page.waitForLoadState("networkidle"); // Page should render some heading or content area — not a hard 404 - await expect( - page.locator("h1").or(page.locator("main")).first(), - ).toBeVisible({ timeout: 10000 }); + await expect(page.locator("h1").or(page.locator("main")).first()).toBeVisible({ + timeout: 10000, + }); }); }); diff --git a/apps/web/e2e/assistant-approvals.spec.ts b/apps/web/e2e/assistant-approvals.spec.ts index 22ac2c8..5a8263c 100644 --- a/apps/web/e2e/assistant-approvals.spec.ts +++ b/apps/web/e2e/assistant-approvals.spec.ts @@ -3,7 +3,7 @@ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; -const ADMIN_EMAIL = "admin@capakraken.dev"; +const ADMIN_EMAIL = "admin@nexus.dev"; const ADMIN_PASSWORD = "admin123"; const CURRENT_CONVERSATION_ID = "assistant-e2e-current"; const DB_WORKDIR = resolve(process.cwd(), "../../packages/db"); @@ -159,7 +159,9 @@ test.describe("Assistant approvals", () => { `); }); - test("renders the pending approval inbox and handles cross-conversation actions", async ({ page }) => { + test("renders the pending approval inbox and handles cross-conversation actions", async ({ + page, + }) => { const suffix = Date.now(); const currentClientName = `E2E Approval Client Current ${suffix}`; const otherClientName = `E2E Approval Client Other ${suffix}`; @@ -210,14 +212,22 @@ test.describe("Assistant approvals", () => { await expect(page.getByText(currentApproval.summary)).toBeVisible(); await expect(page.getByText(otherApproval.summary)).toBeVisible(); - const currentCard = page.locator('[data-testid="assistant-approval-card"][data-conversation-scope="current"]').first(); - const otherCard = page.locator('[data-testid="assistant-approval-card"][data-conversation-scope="other"]').first(); + const currentCard = page + .locator('[data-testid="assistant-approval-card"][data-conversation-scope="current"]') + .first(); + const otherCard = page + .locator('[data-testid="assistant-approval-card"][data-conversation-scope="other"]') + .first(); await expect(currentCard).toContainText("This chat"); await expect(otherCard).toContainText("Other chat"); await otherCard.getByTestId("assistant-approval-cancel").click(); await expect(page.getByText(`Aktion verworfen: ${otherApproval.summary}`)).toBeVisible(); - await expect(page.locator(`[data-testid="assistant-approval-card"][data-approval-id="${otherApproval.id}"]`)).toHaveCount(0); + await expect( + page.locator( + `[data-testid="assistant-approval-card"][data-approval-id="${otherApproval.id}"]`, + ), + ).toHaveCount(0); await expect .poll(async () => { diff --git a/apps/web/e2e/auth.spec.ts b/apps/web/e2e/auth.spec.ts index a102fa2..6c23a92 100644 --- a/apps/web/e2e/auth.spec.ts +++ b/apps/web/e2e/auth.spec.ts @@ -8,7 +8,7 @@ test.describe("Authentication", () => { test("admin can sign in", async ({ page }) => { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="email"]', "admin@nexus.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/); diff --git a/apps/web/e2e/bench.spec.ts b/apps/web/e2e/bench.spec.ts index 2415dff..2e2fd64 100644 --- a/apps/web/e2e/bench.spec.ts +++ b/apps/web/e2e/bench.spec.ts @@ -2,7 +2,7 @@ import { expect, test, type Page } from "@playwright/test"; async function signIn(page: Page) { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="email"]', "admin@nexus.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/); @@ -16,9 +16,7 @@ test.describe("Bench Board", () => { test("bench board page loads with heading", async ({ page }) => { await page.waitForLoadState("networkidle"); - await expect( - page.locator("h1", { hasText: "Bench Board" }), - ).toBeVisible({ timeout: 10000 }); + await expect(page.locator("h1", { hasText: "Bench Board" })).toBeVisible({ timeout: 10000 }); }); test("date range filter inputs are visible", async ({ page }) => { @@ -32,7 +30,8 @@ test.describe("Bench Board", () => { test("shows bench results or no-resources empty state", async ({ page }) => { await page.waitForLoadState("networkidle"); await expect( - page.locator("table") + page + .locator("table") .or(page.locator("text=No resources on bench")) .or(page.locator("text=No results")) .first(), diff --git a/apps/web/e2e/dashboard.spec.ts b/apps/web/e2e/dashboard.spec.ts index 7471d7f..ab2a356 100644 --- a/apps/web/e2e/dashboard.spec.ts +++ b/apps/web/e2e/dashboard.spec.ts @@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test"; test.describe("Dashboard", () => { test.beforeEach(async ({ page }) => { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="email"]', "admin@nexus.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/); @@ -31,7 +31,9 @@ test.describe("Dashboard", () => { const addBtn = page.locator("button", { hasText: /Add Widget/i }); if ((await addBtn.count()) > 0) { await addBtn.click(); - await expect(page.locator("text=Add Widget").or(page.locator("text=Available Widgets"))).toBeVisible(); + await expect( + page.locator("text=Add Widget").or(page.locator("text=Available Widgets")), + ).toBeVisible(); await page.keyboard.press("Escape"); } }); diff --git a/apps/web/e2e/dev-system/mfa.spec.ts b/apps/web/e2e/dev-system/mfa.spec.ts index c5754ee..e7cf45e 100644 --- a/apps/web/e2e/dev-system/mfa.spec.ts +++ b/apps/web/e2e/dev-system/mfa.spec.ts @@ -21,9 +21,16 @@ import { STORAGE_STATE } from "../../playwright.dev.config.js"; // ─── tRPC helpers ───────────────────────────────────────────────────────────── -type TrpcResult = { result?: { data?: unknown }; error?: { data?: { code?: string }; message?: string } }; +type TrpcResult = { + result?: { data?: unknown }; + error?: { data?: { code?: string }; message?: string }; +}; -async function trpcMutation(page: Page, procedure: string, input: unknown = null): Promise { +async function trpcMutation( + page: Page, + procedure: string, + input: unknown = null, +): Promise { return page.evaluate( async ({ procedure, input }) => { const res = await fetch(`/api/trpc/${procedure}?batch=1`, { @@ -39,7 +46,11 @@ async function trpcMutation(page: Page, procedure: string, input: unknown = null ); } -async function trpcQuery(page: Page, procedure: string, input: unknown = null): Promise { +async function trpcQuery( + page: Page, + procedure: string, + input: unknown = null, +): Promise { return page.evaluate( async ({ procedure, input }) => { const encodedInput = encodeURIComponent(JSON.stringify({ "0": { json: input } })); @@ -60,7 +71,7 @@ async function enableMfaForSession(page: Page): Promise { if (!data?.secret) throw new Error(`generateTotpSecret failed: ${JSON.stringify(genRes)}`); const totp = new TOTP({ - issuer: "CapaKraken", + issuer: "Nexus", algorithm: "SHA1", digits: 6, period: 30, @@ -92,7 +103,9 @@ test.describe("MFA — setup flow (account/security page)", () => { test.afterEach(async ({ page }) => { // Clean up: disable MFA if a test enabled it if (totp) { - await disableMfaForSession(page).catch(() => {/* already disabled or admin override */}); + await disableMfaForSession(page).catch(() => { + /* already disabled or admin override */ + }); totp = null; } }); @@ -106,7 +119,7 @@ test.describe("MFA — setup flow (account/security page)", () => { expect(data?.secret).toBeTruthy(); expect(data?.uri).toMatch(/^otpauth:\/\/totp\//); - expect(data?.uri).toContain("CapaKraken"); + expect(data?.uri).toContain("Nexus"); }); test("verifyAndEnableTotp accepts a valid code and enables MFA", async ({ page }) => { @@ -137,9 +150,9 @@ test.describe("MFA — setup flow (account/security page)", () => { await page.waitForLoadState("networkidle"); // Click the enable/setup button if MFA is not yet enabled - const setupBtn = page.getByRole("button", { name: /set up/i }).or( - page.getByRole("button", { name: /enable.*mfa/i }), - ); + const setupBtn = page + .getByRole("button", { name: /set up/i }) + .or(page.getByRole("button", { name: /enable.*mfa/i })); if (await setupBtn.isVisible({ timeout: 3000 }).catch(() => false)) { await setupBtn.click(); @@ -233,9 +246,10 @@ test.describe("MFA — login flow", () => { // Should show error and remain on TOTP step await expect( - page.getByText(/invalid.*code|incorrect.*token|try again/i).or( - page.locator("[data-error]"), - ).first(), + page + .getByText(/invalid.*code|incorrect.*token|try again/i) + .or(page.locator("[data-error]")) + .first(), ).toBeVisible({ timeout: 5000 }); // Should NOT have navigated away @@ -248,7 +262,9 @@ test.describe("MFA — login flow", () => { test.describe("MFA — users without MFA enabled", () => { test.use({ storageState: { cookies: [], origins: [] } }); - test("login for MFA-less user goes straight to dashboard without TOTP prompt", async ({ page }) => { + test("login for MFA-less user goes straight to dashboard without TOTP prompt", async ({ + page, + }) => { await page.goto("/auth/signin"); await page.fill('input[type="email"]', "manager@planarchy.dev"); await page.fill('input[type="password"]', "manager123"); diff --git a/apps/web/e2e/dev-system/nav-smoke.spec.ts b/apps/web/e2e/dev-system/nav-smoke.spec.ts index 817faae..f5bcc04 100644 --- a/apps/web/e2e/dev-system/nav-smoke.spec.ts +++ b/apps/web/e2e/dev-system/nav-smoke.spec.ts @@ -8,7 +8,7 @@ * Auth: e2e/dev-system/.auth/admin.json (created by global-setup.ts) * * Run: - * pnpm --filter @capakraken/web exec playwright test \ + * pnpm --filter @nexus/web exec playwright test \ * --config playwright.dev.config.ts \ * e2e/dev-system/nav-smoke.spec.ts */ diff --git a/apps/web/e2e/dev-system/rbac-permissions.spec.ts b/apps/web/e2e/dev-system/rbac-permissions.spec.ts index 2e08e1b..4146a77 100644 --- a/apps/web/e2e/dev-system/rbac-permissions.spec.ts +++ b/apps/web/e2e/dev-system/rbac-permissions.spec.ts @@ -27,10 +27,10 @@ test.describe("RBAC — admin routes (admin session)", () => { await page.waitForLoadState("networkidle"); await expect(page.locator("table")).toBeVisible({ timeout: 10000 }); - // Seed users have planarchy.dev or capakraken.dev email domains - await expect( - page.locator("text=/planarchy\\.dev|capakraken\\.dev/").first(), - ).toBeVisible({ timeout: 10000 }); + // Seed users have planarchy.dev or nexus.dev email domains + await expect(page.locator("text=/planarchy\\.dev|capakraken\\.dev/").first()).toBeVisible({ + timeout: 10000, + }); }); test("admin can access /admin/system-roles without errors", async ({ page }) => { @@ -99,9 +99,10 @@ test.describe("RBAC — allocations permitted for admin", () => { await page.goto("/allocations"); await page.waitForLoadState("networkidle"); - await expect( - page.locator("text=/do not have permission to view allocations/i"), - ).toHaveCount(0, { timeout: 8000 }); + await expect(page.locator("text=/do not have permission to view allocations/i")).toHaveCount( + 0, + { timeout: 8000 }, + ); }); }); @@ -112,9 +113,10 @@ test.describe("RBAC — allocations permitted for manager", () => { await page.goto("/allocations"); await page.waitForLoadState("networkidle"); - await expect( - page.locator("text=/do not have permission to view allocations/i"), - ).toHaveCount(0, { timeout: 8000 }); + await expect(page.locator("text=/do not have permission to view allocations/i")).toHaveCount( + 0, + { timeout: 8000 }, + ); }); }); diff --git a/apps/web/e2e/estimates.spec.ts b/apps/web/e2e/estimates.spec.ts index 32eb609..818d833 100644 --- a/apps/web/e2e/estimates.spec.ts +++ b/apps/web/e2e/estimates.spec.ts @@ -10,22 +10,26 @@ async function signIn(page: Page, email: string, password: string) { test.describe("Estimates", () => { test.beforeEach(async ({ page }) => { - await signIn(page, "admin@capakraken.dev", "admin123"); + await signIn(page, "admin@nexus.dev", "admin123"); await page.goto("/estimates"); }); test("estimate list loads", async ({ page }) => { await page.waitForLoadState("networkidle"); + await expect(page.getByRole("heading", { name: /estimate workspace/i })).toBeVisible({ + timeout: 10000, + }); + await expect(page.getByPlaceholder("Search by estimate or opportunity")).toBeVisible({ + timeout: 10000, + }); await expect( - page.getByRole("heading", { name: /estimate workspace/i }), - ).toBeVisible({ timeout: 10000 }); - await expect( - page.getByPlaceholder("Search by estimate or opportunity"), - ).toBeVisible({ timeout: 10000 }); - await expect( - page.locator("text=No estimates yet").or( - page.locator("text=Select an estimate to inspect the current version, demand lines, and summary metrics."), - ), + page + .locator("text=No estimates yet") + .or( + page.locator( + "text=Select an estimate to inspect the current version, demand lines, and summary metrics.", + ), + ), ).toBeVisible({ timeout: 10000 }); }); @@ -44,8 +48,13 @@ test.describe("Estimates", () => { await page.locator("button", { hasText: /New Estimate/i }).click(); // Step 1: Setup — fill a name - await expect(page.getByRole("button", { name: /Step 1 Setup/i })).toBeVisible({ timeout: 5000 }); - const nameInput = page.locator('input[placeholder*="name"]').or(page.locator('input[name="name"]')).first(); + await expect(page.getByRole("button", { name: /Step 1 Setup/i })).toBeVisible({ + timeout: 5000, + }); + const nameInput = page + .locator('input[placeholder*="name"]') + .or(page.locator('input[name="name"]')) + .first(); if ((await nameInput.count()) > 0) { await nameInput.fill(`E2E Estimate ${Date.now()}`); } @@ -90,9 +99,7 @@ test.describe("Estimates", () => { test("shows the empty-state fallback when no estimates exist", async ({ page }) => { await page.waitForLoadState("networkidle"); - await expect( - page.locator("text=No estimates yet"), - ).toBeVisible({ timeout: 10000 }); + await expect(page.locator("text=No estimates yet")).toBeVisible({ timeout: 10000 }); }); test("shows an estimate-not-found fallback for unknown workspaces", async ({ page }) => { @@ -103,12 +110,14 @@ test.describe("Estimates", () => { test("shows the restricted workspace fallback for viewers", async ({ browser }) => { const page = await browser.newPage(); - await signIn(page, "viewer@capakraken.dev", "viewer123"); + await signIn(page, "viewer@nexus.dev", "viewer123"); await page.goto("/estimates/missing-estimate"); await page.waitForLoadState("networkidle"); await expect( - page.locator("text=Your role can access the estimate list, but not the detailed financial workspace."), + page.locator( + "text=Your role can access the estimate list, but not the detailed financial workspace.", + ), ).toBeVisible({ timeout: 10000 }); await page.close(); diff --git a/apps/web/e2e/holiday-calendar.spec.ts b/apps/web/e2e/holiday-calendar.spec.ts index 3f8bd8c..a13e3fc 100644 --- a/apps/web/e2e/holiday-calendar.spec.ts +++ b/apps/web/e2e/holiday-calendar.spec.ts @@ -2,14 +2,16 @@ import { expect, test, type Page } from "@playwright/test"; async function signInAsAdmin(page: Page) { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="email"]', "admin@nexus.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/); } test.describe("Holiday Calendar Editor", () => { - test("creates a city calendar, previews a holiday, blocks duplicates and confirms deletion", async ({ page }) => { + test("creates a city calendar, previews a holiday, blocks duplicates and confirms deletion", async ({ + page, + }) => { const suffix = Date.now().toString(); const calendarName = `E2E City Calendar ${suffix}`; const holidayName = `E2E Local Holiday ${suffix}`; @@ -21,11 +23,18 @@ test.describe("Holiday Calendar Editor", () => { await page.getByTestId("holiday-calendar-name-input").fill(calendarName); await page.getByTestId("holiday-calendar-scope-select").selectOption("CITY"); - await page.getByTestId("holiday-calendar-country-select").selectOption({ label: "Germany (DE)" }); + await page + .getByTestId("holiday-calendar-country-select") + .selectOption({ label: "Germany (DE)" }); await page.getByTestId("holiday-calendar-city-select").selectOption({ label: "Muenchen" }); await page.getByTestId("holiday-calendar-create-button").click(); - await expect(page.getByTestId(/holiday-calendar-row-/).filter({ hasText: calendarName }).first()).toBeVisible(); + await expect( + page + .getByTestId(/holiday-calendar-row-/) + .filter({ hasText: calendarName }) + .first(), + ).toBeVisible(); await expect(page.getByRole("heading", { name: calendarName })).toBeVisible(); await expect(page.getByTestId("holiday-entry-create-button")).toBeVisible(); @@ -44,10 +53,15 @@ test.describe("Holiday Calendar Editor", () => { await page.getByTestId("holiday-entry-name-input").fill(`${holidayName} Duplicate`); await page.getByTestId("holiday-entry-create-button").click(); - await expect(page.getByText("A holiday entry for this calendar and date already exists")).toBeVisible(); + await expect( + page.getByText("A holiday entry for this calendar and date already exists"), + ).toBeVisible(); page.once("dialog", (dialog) => dialog.accept()); - await page.getByTestId(/holiday-entry-delete-/).first().click(); + await page + .getByTestId(/holiday-entry-delete-/) + .first() + .click(); await expect(page.getByText(holidayName).first()).not.toBeVisible(); page.once("dialog", (dialog) => dialog.accept()); diff --git a/apps/web/e2e/navigation.spec.ts b/apps/web/e2e/navigation.spec.ts index d379263..dc7c5b8 100644 --- a/apps/web/e2e/navigation.spec.ts +++ b/apps/web/e2e/navigation.spec.ts @@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test"; test.describe("Navigation", () => { test.beforeEach(async ({ page }) => { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="email"]', "admin@nexus.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/); @@ -28,7 +28,7 @@ test.describe("Navigation", () => { test("all nav routes resolve — no 404 (smoke)", async ({ page }) => { // Complements the click-based test above with a direct-navigation check - // covering every sidebar destination. Uses admin@capakraken.dev (ADMIN role). + // covering every sidebar destination. Uses admin@nexus.dev (ADMIN role). const routes = [ // Already covered by click test but included for completeness "/dashboard", @@ -79,7 +79,10 @@ test.describe("Navigation", () => { } // Expand again — the button should still be visible as an icon - const expandBtn = page.locator("nav button").filter({ has: page.locator("svg") }).last(); + const expandBtn = page + .locator("nav button") + .filter({ has: page.locator("svg") }) + .last(); await expandBtn.click(); await page.waitForTimeout(300); const boxExpanded = await nav.boundingBox(); @@ -113,7 +116,10 @@ test.describe("Navigation", () => { await page.waitForLoadState("networkidle"); // The hamburger button should be visible on mobile - const hamburgerBtn = page.locator("button").filter({ has: page.locator("svg") }).first(); + const hamburgerBtn = page + .locator("button") + .filter({ has: page.locator("svg") }) + .first(); await expect(hamburgerBtn).toBeVisible({ timeout: 5000 }); await hamburgerBtn.click(); diff --git a/apps/web/e2e/project-detail.spec.ts b/apps/web/e2e/project-detail.spec.ts index 8ed666d..0e4b403 100644 --- a/apps/web/e2e/project-detail.spec.ts +++ b/apps/web/e2e/project-detail.spec.ts @@ -2,7 +2,7 @@ import { expect, test, type Page } from "@playwright/test"; async function signIn(page: Page) { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="email"]', "admin@nexus.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/); @@ -74,9 +74,9 @@ test.describe("Project Detail Page", () => { await page.waitForLoadState("networkidle"); // BudgetStatusCard renders budget-related content - await expect( - page.locator("text=Budget").or(page.locator("text=budget")).first(), - ).toBeVisible({ timeout: 10000 }); + await expect(page.locator("text=Budget").or(page.locator("text=budget")).first()).toBeVisible({ + timeout: 10000, + }); }); test("unknown project id shows not-found state", async ({ page }) => { @@ -85,7 +85,11 @@ test.describe("Project Detail Page", () => { // Server-side notFound() triggers the Next.js 404 page await expect( - page.locator("text=404").or(page.locator("text=Not Found")).or(page.locator("text=not found")).first(), + page + .locator("text=404") + .or(page.locator("text=Not Found")) + .or(page.locator("text=not found")) + .first(), ).toBeVisible({ timeout: 10000 }); }); diff --git a/apps/web/e2e/projects.spec.ts b/apps/web/e2e/projects.spec.ts index 65d854e..055c946 100644 --- a/apps/web/e2e/projects.spec.ts +++ b/apps/web/e2e/projects.spec.ts @@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test"; test.describe("Projects", () => { test.beforeEach(async ({ page }) => { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "manager@capakraken.dev"); + await page.fill('input[type="email"]', "manager@nexus.dev"); await page.fill('input[type="password"]', "manager123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/resources/); @@ -26,9 +26,16 @@ test.describe("Projects", () => { // Step 1: Blueprint selection await expect(page.locator("text=Select Blueprint")).toBeVisible(); // Select the first available blueprint - const blueprintCard = page.locator("[data-blueprint-id]").first() - .or(page.locator("button").filter({ hasText: /Blueprint|Production/ }).first()); - if (await blueprintCard.count() > 0) { + const blueprintCard = page + .locator("[data-blueprint-id]") + .first() + .or( + page + .locator("button") + .filter({ hasText: /Blueprint|Production/ }) + .first(), + ); + if ((await blueprintCard.count()) > 0) { await blueprintCard.click(); } else { // Click next without blueprint if none shown @@ -37,16 +44,21 @@ test.describe("Projects", () => { } // Step 2: Timeline — set project dates - await expect(page.locator("text=Timeline").or(page.locator("text=Project Dates"))).toBeVisible({ timeout: 5000 }); - const projectNameInput = page.locator('input[placeholder*="name"]').or(page.locator('input[name="name"]')).first(); - if (await projectNameInput.count() > 0) { + await expect(page.locator("text=Timeline").or(page.locator("text=Project Dates"))).toBeVisible({ + timeout: 5000, + }); + const projectNameInput = page + .locator('input[placeholder*="name"]') + .or(page.locator('input[name="name"]')) + .first(); + if ((await projectNameInput.count()) > 0) { await projectNameInput.fill(`E2E Test Project ${Date.now()}`); } await page.locator("button", { hasText: "Next" }).click(); // Step 3: Staffing demand await expect( - page.locator("text=Staffing").or(page.locator("text=Demand").or(page.locator("text=Roles"))) + page.locator("text=Staffing").or(page.locator("text=Demand").or(page.locator("text=Roles"))), ).toBeVisible({ timeout: 5000 }); await page.locator("button", { hasText: "Next" }).click(); @@ -56,11 +68,13 @@ test.describe("Projects", () => { // Step 5: Review await page.waitForTimeout(500); - const reviewOrFinish = page.locator("text=Review").or(page.locator("button", { hasText: /Create|Finish|Submit/ })); + const reviewOrFinish = page + .locator("text=Review") + .or(page.locator("button", { hasText: /Create|Finish|Submit/ })); await expect(reviewOrFinish).toBeVisible({ timeout: 5000 }); // Don't actually submit — just close const cancelBtn = page.locator("button", { hasText: /Cancel|Close/ }).first(); - if (await cancelBtn.count() > 0) { + if ((await cancelBtn.count()) > 0) { await cancelBtn.click(); } }); diff --git a/apps/web/e2e/reports.spec.ts b/apps/web/e2e/reports.spec.ts index 255fc84..7e5239e 100644 --- a/apps/web/e2e/reports.spec.ts +++ b/apps/web/e2e/reports.spec.ts @@ -2,7 +2,7 @@ import { expect, test, type Page } from "@playwright/test"; async function signIn(page: Page) { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="email"]', "admin@nexus.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/); @@ -16,16 +16,17 @@ test.describe("Chargeability Report", () => { test("chargeability forecast page loads with heading", async ({ page }) => { await page.waitForLoadState("networkidle"); - await expect( - page.locator("h1", { hasText: "Chargeability Forecast" }), - ).toBeVisible({ timeout: 10000 }); + await expect(page.locator("h1", { hasText: "Chargeability Forecast" })).toBeVisible({ + timeout: 10000, + }); }); test("filter controls are present", async ({ page }) => { await page.waitForLoadState("networkidle"); // Should have at least one filter (e.g., chapter, period, resource search) await expect( - page.locator('input[type="text"]') + page + .locator('input[type="text"]') .or(page.locator('input[type="search"]')) .or(page.locator("select")) .first(), @@ -64,9 +65,9 @@ test.describe("Report Builder", () => { test("report builder page loads with heading", async ({ page }) => { await page.waitForLoadState("networkidle"); - await expect( - page.getByRole("heading", { name: "Report Builder" }), - ).toBeVisible({ timeout: 10000 }); + await expect(page.getByRole("heading", { name: "Report Builder" })).toBeVisible({ + timeout: 10000, + }); }); test("entity selector is present with expected options", async ({ page }) => { @@ -78,9 +79,9 @@ test.describe("Report Builder", () => { test("run report button is visible", async ({ page }) => { await page.waitForLoadState("networkidle"); - await expect( - page.locator("button", { hasText: /Run|Export|Generate/i }).first(), - ).toBeVisible({ timeout: 10000 }); + await expect(page.locator("button", { hasText: /Run|Export|Generate/i }).first()).toBeVisible({ + timeout: 10000, + }); }); test("running a default report produces output or empty state", async ({ page }) => { @@ -90,7 +91,11 @@ test.describe("Report Builder", () => { await runBtn.click(); await page.waitForTimeout(1500); await expect( - page.locator("table").or(page.locator("text=No rows")).or(page.locator("text=0 rows")).first(), + page + .locator("table") + .or(page.locator("text=No rows")) + .or(page.locator("text=0 rows")) + .first(), ).toBeVisible({ timeout: 15000 }); } }); diff --git a/apps/web/e2e/resources.spec.ts b/apps/web/e2e/resources.spec.ts index 8317fa1..1429869 100644 --- a/apps/web/e2e/resources.spec.ts +++ b/apps/web/e2e/resources.spec.ts @@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test"; test.describe("Resources", () => { test.beforeEach(async ({ page }) => { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "manager@capakraken.dev"); + await page.fill('input[type="email"]', "manager@nexus.dev"); await page.fill('input[type="password"]', "manager123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/); @@ -21,10 +21,11 @@ test.describe("Resources", () => { await expect(rows.first()).toBeVisible(); const firstRowText = (await rows.first().textContent()) ?? ""; - const searchTerm = firstRowText - .split(/\s+/) - .map((token) => token.replace(/[^A-Za-z0-9@._-]/g, "").trim()) - .find((token) => token.length >= 3) ?? "EMP"; + const searchTerm = + firstRowText + .split(/\s+/) + .map((token) => token.replace(/[^A-Za-z0-9@._-]/g, "").trim()) + .find((token) => token.length >= 3) ?? "EMP"; const searchInput = page.locator('input[type="search"]'); await searchInput.fill(searchTerm); diff --git a/apps/web/e2e/scenarios.spec.ts b/apps/web/e2e/scenarios.spec.ts index 4d34026..3b3dc7e 100644 --- a/apps/web/e2e/scenarios.spec.ts +++ b/apps/web/e2e/scenarios.spec.ts @@ -2,7 +2,7 @@ import { expect, test, type Page } from "@playwright/test"; async function signIn(page: Page) { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="email"]', "admin@nexus.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/); @@ -16,15 +16,16 @@ test.describe("Scenario Planning", () => { test("scenarios page loads with heading", async ({ page }) => { await page.waitForLoadState("networkidle"); - await expect( - page.locator("h1", { hasText: /Scenario Planning/i }), - ).toBeVisible({ timeout: 10000 }); + await expect(page.locator("h1", { hasText: /Scenario Planning/i })).toBeVisible({ + timeout: 10000, + }); }); test("shows scenarios list or empty state", async ({ page }) => { await page.waitForLoadState("networkidle"); await expect( - page.locator("table") + page + .locator("table") .or(page.locator("text=No scenarios")) .or(page.locator("text=Create a project first")) .or(page.locator("[data-testid]")) diff --git a/apps/web/e2e/smoke.spec.ts b/apps/web/e2e/smoke.spec.ts index 93a39b3..8b5a027 100644 --- a/apps/web/e2e/smoke.spec.ts +++ b/apps/web/e2e/smoke.spec.ts @@ -29,7 +29,7 @@ test("signin page renders credential inputs and submit button", async ({ page }) test("admin login succeeds and redirects away from signin", async ({ page }) => { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="email"]', "admin@nexus.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).not.toHaveURL(/\/auth\/signin/, { timeout: 15_000 }); @@ -37,7 +37,7 @@ test("admin login succeeds and redirects away from signin", async ({ page }) => test("authenticated user sees app shell nav", async ({ page }) => { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="email"]', "admin@nexus.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).not.toHaveURL(/\/auth\/signin/, { timeout: 15_000 }); diff --git a/apps/web/e2e/staffing.spec.ts b/apps/web/e2e/staffing.spec.ts index a6bcd78..3ae4acb 100644 --- a/apps/web/e2e/staffing.spec.ts +++ b/apps/web/e2e/staffing.spec.ts @@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test"; test.describe("Staffing", () => { test.beforeEach(async ({ page }) => { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="email"]', "admin@nexus.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/); @@ -12,7 +12,9 @@ test.describe("Staffing", () => { test("staffing page loads with search form", async ({ page }) => { await page.waitForLoadState("networkidle"); - await expect(page.locator("h1", { hasText: /Staffing Suggestions/i })).toBeVisible({ timeout: 10000 }); + await expect(page.locator("h1", { hasText: /Staffing Suggestions/i })).toBeVisible({ + timeout: 10000, + }); // Search form should have skill input, date fields, and a search button await expect(page.locator("text=How scoring works")).toBeVisible({ timeout: 10000 }); }); @@ -20,9 +22,9 @@ test.describe("Staffing", () => { test("search form has default skill tags", async ({ page }) => { await page.waitForLoadState("networkidle"); // The StaffingPanel pre-populates with TypeScript and React skill tags - await expect( - page.locator("text=TypeScript").or(page.locator("text=React")), - ).toBeVisible({ timeout: 10000 }); + await expect(page.locator("text=TypeScript").or(page.locator("text=React"))).toBeVisible({ + timeout: 10000, + }); }); test("submitting search returns suggestions or empty state", async ({ page }) => { @@ -34,7 +36,9 @@ test.describe("Staffing", () => { await page.waitForTimeout(1000); // After search, should show either suggestion cards or a "no suggestions" message await expect( - page.locator("text=/Score|Availability|No suggestions|No matching/i").first() + page + .locator("text=/Score|Availability|No suggestions|No matching/i") + .first() .or(page.locator("[data-suggestion]").first()) .or(page.locator("table").first()), ).toBeVisible({ timeout: 15000 }); diff --git a/apps/web/e2e/test-server.mjs b/apps/web/e2e/test-server.mjs index d4114bc..b1fc9d9 100644 --- a/apps/web/e2e/test-server.mjs +++ b/apps/web/e2e/test-server.mjs @@ -393,9 +393,9 @@ try { await cleanupStaleE2eArtifacts(); await ensureE2eDatabaseContainer(); } - await run("pnpm", ["--filter", "@capakraken/db", "db:push"], workspaceRoot); - await run("pnpm", ["--filter", "@capakraken/db", "db:seed"], workspaceRoot); - await run("pnpm", ["--filter", "@capakraken/db", "db:seed:holidays"], workspaceRoot); + await run("pnpm", ["--filter", "@nexus/db", "db:push"], workspaceRoot); + await run("pnpm", ["--filter", "@nexus/db", "db:seed"], workspaceRoot); + await run("pnpm", ["--filter", "@nexus/db", "db:seed:holidays"], workspaceRoot); rmSync(webDistDirPath, { recursive: true, force: true }); const server = spawn("pnpm", ["exec", "next", "dev", "-p", e2ePort], { diff --git a/apps/web/e2e/timeline.spec.ts b/apps/web/e2e/timeline.spec.ts index 04c12f2..7da2f5c 100644 --- a/apps/web/e2e/timeline.spec.ts +++ b/apps/web/e2e/timeline.spec.ts @@ -133,7 +133,7 @@ function createTimelineSegmentScenario(suffix: string): TimelineSegmentScenario data: { eid: ${JSON.stringify(`e2e.timeline.${suffix}`)}, displayName: ${JSON.stringify(`E2E Timeline ${suffix}`)}, - email: ${JSON.stringify(`e2e.timeline.${suffix}@capakraken.dev`)}, + email: ${JSON.stringify(`e2e.timeline.${suffix}@nexus.dev`)}, chapter: "E2E", lcrCents: 5000, ucrCents: 9000, @@ -208,7 +208,7 @@ function createTimelineDemandScenario(suffix: string): TimelineDemandScenario { data: { eid: ${JSON.stringify(`e2e.timeline.demand.${suffix}`)}, displayName: ${JSON.stringify(`E2E Timeline Demand Resource ${suffix}`)}, - email: ${JSON.stringify(`e2e.timeline.demand.${suffix}@capakraken.dev`)}, + email: ${JSON.stringify(`e2e.timeline.demand.${suffix}@nexus.dev`)}, chapter: "E2E", lcrCents: 5000, ucrCents: 9000, @@ -341,7 +341,9 @@ function listScenarioAssignments(projectId: string) { } function listScenarioDemands(projectId: string) { - return runDbJson>(` + return runDbJson< + Array<{ id: string; startDate: string; endDate: string; headcount: number; status: string }> + >(` const demands = await prisma.demandRequirement.findMany({ where: { projectId: ${JSON.stringify(projectId)} }, orderBy: [{ startDate: "asc" }, { endDate: "asc" }], @@ -448,10 +450,7 @@ async function openAllocationContextMenuAtOffset( ); } -async function openContextMenuAtCenter( - page: Page, - locator: ReturnType, -) { +async function openContextMenuAtCenter(page: Page, locator: ReturnType) { const target = await resolveAllocationContextMenuTarget(locator); const box = await readBoundingBox(target); await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2, { button: "right" }); @@ -511,9 +510,7 @@ async function listRenderedAllocationSegments( row: ReturnType, allocationId?: string, ) { - const selector = allocationId - ? `[data-allocation-id="${allocationId}"]` - : "[data-allocation-id]"; + const selector = allocationId ? `[data-allocation-id="${allocationId}"]` : "[data-allocation-id]"; return row.locator(selector).evaluateAll((elements) => elements.map((element) => { const htmlElement = element as HTMLElement; @@ -536,17 +533,13 @@ function escapeRegex(value: string) { async function signInAsAdmin(page: Page) { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="email"]', "admin@nexus.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/); } -async function findVisibleTimelineEntryId( - page: Page, - selector: string, - minimumWidth = 24, -) { +async function findVisibleTimelineEntryId(page: Page, selector: string, minimumWidth = 24) { return page.locator(selector).evaluateAll((elements, minimum) => { for (const element of elements) { if (!(element instanceof HTMLElement)) continue; @@ -600,9 +593,9 @@ async function findVisibleAllocationSegmentForResize( ); const stickyHeaderBottom = scrollContainer ? Array.from(scrollContainer.querySelectorAll(".sticky.top-0")).reduce( - (maxBottom, element) => Math.max(maxBottom, element.getBoundingClientRect().bottom), - 0, - ) + (maxBottom, element) => Math.max(maxBottom, element.getBoundingClientRect().bottom), + 0, + ) : 0; const safeTop = stickyHeaderBottom > 0 ? stickyHeaderBottom + 8 : 48; const candidates: Array<{ @@ -611,8 +604,11 @@ async function findVisibleAllocationSegmentForResize( segmentEnd: string | null; score: number; }> = []; - let fallback: { allocationId: string; segmentStart: string | null; segmentEnd: string | null } | null = - null; + let fallback: { + allocationId: string; + segmentStart: string | null; + segmentEnd: string | null; + } | null = null; for (const element of elements) { if (!(element instanceof HTMLElement)) continue; @@ -829,13 +825,20 @@ async function switchToProjectView(page: Page, readySelector?: string) { await expect(page.locator(readySelector).first()).toBeVisible(); } else { await expect - .poll(async () => { - const projectRows = await page.getByTestId("timeline-project-resource-row-canvas").count(); - const projectBars = await page.locator("[data-timeline-entry-type='project-bar']").count(); - const demandBars = await page.locator("[data-timeline-entry-type='demand']").count(); - const emptyStates = await page.getByText(/No projects in this time range/).count(); - return projectRows + projectBars + demandBars + emptyStates; - }, { timeout: 10_000 }) + .poll( + async () => { + const projectRows = await page + .getByTestId("timeline-project-resource-row-canvas") + .count(); + const projectBars = await page + .locator("[data-timeline-entry-type='project-bar']") + .count(); + const demandBars = await page.locator("[data-timeline-entry-type='demand']").count(); + const emptyStates = await page.getByText(/No projects in this time range/).count(); + return projectRows + projectBars + demandBars + emptyStates; + }, + { timeout: 10_000 }, + ) .not.toBe(0); } await expect(page.getByTestId("timeline-resource-row-canvas")).toHaveCount(0); @@ -906,22 +909,21 @@ test.describe("Timeline", () => { await expect(page.locator("text=/\\d+ resources/")).toBeVisible(); }); - test("view toggle stays disabled until the initial timeline load becomes interactive", async ({ page }) => { + test("view toggle stays disabled until the initial timeline load becomes interactive", async ({ + page, + }) => { const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`; const scenario = createTimelineSegmentScenario(suffix); try { - await page.goto( - `/timeline?startDate=2026-04-01&days=31&eids=${scenario.resourceEid}`, - { waitUntil: "domcontentloaded" }, - ); + await page.goto(`/timeline?startDate=2026-04-01&days=31&eids=${scenario.resourceEid}`, { + waitUntil: "domcontentloaded", + }); const projectButton = page.getByRole("button", { name: "Project view" }); const resourceButton = page.getByRole("button", { name: "Resource view" }); - const resourceRowSelector = - `[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`; - const projectRowSelector = - `[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`; + const resourceRowSelector = `[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`; + const projectRowSelector = `[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`; await expect(projectButton).toBeDisabled(); await expect(resourceButton).toBeDisabled(); @@ -951,9 +953,9 @@ test.describe("Timeline", () => { test("keeps timeline data populated after navigating from allocations", async ({ page }) => { await page.goto("/allocations"); - await expect( - page.locator("h1").filter({ hasText: /Allocations|Planning/i }), - ).toBeVisible({ timeout: 10000 }); + await expect(page.locator("h1").filter({ hasText: /Allocations|Planning/i })).toBeVisible({ + timeout: 10000, + }); await page.locator('nav a >> text="Timeline"').first().click(); await expect(page).toHaveURL(/\/timeline/); @@ -1046,7 +1048,10 @@ test.describe("Timeline", () => { if (!projectAllocationBox) { throw new Error("Expected a project allocation block to be available"); } - await page.mouse.move(projectAllocationBox.x + (projectAllocationBox.width / 2), projectHoverBox.y + 20); + await page.mouse.move( + projectAllocationBox.x + projectAllocationBox.width / 2, + projectHoverBox.y + 20, + ); await expect(heatmapTooltip).toBeVisible(); await expect .poll(async () => { @@ -1071,7 +1076,9 @@ test.describe("Timeline", () => { .first(); await allocation.click({ button: "right" }); - await expect(page.getByTestId("timeline-allocation-popover-loading")).toHaveCount(0, { timeout: 2_000 }); + await expect(page.getByTestId("timeline-allocation-popover-loading")).toHaveCount(0, { + timeout: 2_000, + }); const popover = page.getByTestId("timeline-allocation-popover"); await expect(popover).toBeVisible(); await expect(page.getByTestId("timeline-allocation-popover-error")).toHaveCount(0); @@ -1103,12 +1110,16 @@ test.describe("Timeline", () => { waitUntil: "domcontentloaded", }); - const row = page.locator('[data-testid="timeline-resource-row-canvas"][data-resource-eid="bruce.banner"]').first(); + const row = page + .locator('[data-testid="timeline-resource-row-canvas"][data-resource-eid="bruce.banner"]') + .first(); await expect(row).toBeVisible(); - const holidayBlock = row.locator( - '[data-testid="timeline-vacation-block"][data-vacation-type="PUBLIC_HOLIDAY"][data-vacation-note="Karfreitag"]', - ).first(); + const holidayBlock = row + .locator( + '[data-testid="timeline-vacation-block"][data-vacation-type="PUBLIC_HOLIDAY"][data-vacation-note="Karfreitag"]', + ) + .first(); await expect(holidayBlock).toBeVisible(); const rowBox = await row.boundingBox(); @@ -1129,7 +1140,9 @@ test.describe("Timeline", () => { const holidayTooltip = page .locator("div.fixed.pointer-events-none.rounded-xl.border.border-amber-700\\/50") - .or(page.locator("div.fixed.pointer-events-none.rounded-xl").filter({ hasText: "Karfreitag" })) + .or( + page.locator("div.fixed.pointer-events-none.rounded-xl").filter({ hasText: "Karfreitag" }), + ) .first(); await expect(holidayTooltip).toBeVisible(); @@ -1278,9 +1291,7 @@ test.describe("Timeline", () => { expect(result.maxGap).toBeLessThan(24); }); - test("allocation resize shows a live preview before mouseup", async ({ - page, - }) => { + test("allocation resize shows a live preview before mouseup", async ({ page }) => { await page.goto("/timeline?startDate=2026-04-01&days=31", { waitUntil: "domcontentloaded", }); @@ -1358,9 +1369,7 @@ test.describe("Timeline", () => { expect(secondResize.rightEdgeGain).toBeGreaterThan(48); }); - test("allocation start resize shows a live preview before mouseup", async ({ - page, - }) => { + test("allocation start resize shows a live preview before mouseup", async ({ page }) => { await page.goto("/timeline?startDate=2026-04-01&days=31", { waitUntil: "domcontentloaded", }); @@ -1394,18 +1403,17 @@ test.describe("Timeline", () => { await page.goto(`/timeline?startDate=2026-04-01&days=30&eids=${scenario.resourceEid}`, { waitUntil: "domcontentloaded", }); - const resourceRowSelector = - `[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`; - const projectRowSelector = - `[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`; - const projectAllocationSelector = - `${projectRowSelector} [data-timeline-entry-type="allocation"][data-allocation-id="${scenario.assignmentId}"]`; + const resourceRowSelector = `[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`; + const projectRowSelector = `[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`; + const projectAllocationSelector = `${projectRowSelector} [data-timeline-entry-type="allocation"][data-allocation-id="${scenario.assignmentId}"]`; await expect(page.locator(resourceRowSelector)).toBeVisible(); await expect( - page.locator( - `[data-timeline-entry-type="allocation"][data-allocation-id="${scenario.assignmentId}"]`, - ).first(), + page + .locator( + `[data-timeline-entry-type="allocation"][data-allocation-id="${scenario.assignmentId}"]`, + ) + .first(), ).toBeVisible(); await switchToProjectView(page, projectRowSelector); @@ -1427,19 +1435,22 @@ test.describe("Timeline", () => { expect(resizeEnd.rightEdgeGain).toBeGreaterThan(48); let rightResizeAssignments: Array<{ id: string; startDate: string; endDate: string }> = []; await expect - .poll(() => { - rightResizeAssignments = listScenarioAssignments(scenario.projectId); - if (rightResizeAssignments.length !== 1) { - return null; - } + .poll( + () => { + rightResizeAssignments = listScenarioAssignments(scenario.projectId); + if (rightResizeAssignments.length !== 1) { + return null; + } - const [assignment] = rightResizeAssignments; - if (!assignment || assignment.id !== scenario.assignmentId) { - return null; - } + const [assignment] = rightResizeAssignments; + if (!assignment || assignment.id !== scenario.assignmentId) { + return null; + } - return assignment.endDate; - }, { timeout: 15_000 }) + return assignment.endDate; + }, + { timeout: 15_000 }, + ) .not.toBe("2026-04-17"); expect(rightResizeAssignments).toHaveLength(1); expect(rightResizeAssignments[0]?.id).toBe(scenario.assignmentId); @@ -1451,19 +1462,22 @@ test.describe("Timeline", () => { expect(resizeStart.leftEdgeGain).toBeGreaterThan(36); let leftResizeAssignments: Array<{ id: string; startDate: string; endDate: string }> = []; await expect - .poll(() => { - leftResizeAssignments = listScenarioAssignments(scenario.projectId); - if (leftResizeAssignments.length !== 1) { - return null; - } + .poll( + () => { + leftResizeAssignments = listScenarioAssignments(scenario.projectId); + if (leftResizeAssignments.length !== 1) { + return null; + } - const [assignment] = leftResizeAssignments; - if (!assignment || assignment.id !== scenario.assignmentId) { - return null; - } + const [assignment] = leftResizeAssignments; + if (!assignment || assignment.id !== scenario.assignmentId) { + return null; + } - return assignment.startDate; - }, { timeout: 15_000 }) + return assignment.startDate; + }, + { timeout: 15_000 }, + ) .not.toBe("2026-04-06"); expect(leftResizeAssignments).toHaveLength(1); expect(leftResizeAssignments[0]?.id).toBe(scenario.assignmentId); @@ -1479,15 +1493,12 @@ test.describe("Timeline", () => { const scenario = createTimelineDemandScenario(suffix); try { - await page.goto( - `/timeline?startDate=2026-04-01&days=31&projectIds=${scenario.projectId}`, - { waitUntil: "domcontentloaded" }, - ); + await page.goto(`/timeline?startDate=2026-04-01&days=31&projectIds=${scenario.projectId}`, { + waitUntil: "domcontentloaded", + }); await ensureOpenDemandVisibilityEnabled(page); - const demandRowSelector = - `[data-project-demand-row="true"][data-project-id="${scenario.projectId}"]`; - const demandSelector = - `${demandRowSelector} [data-timeline-entry-type="demand"][data-allocation-id="${scenario.demandId}"]`; + const demandRowSelector = `[data-project-demand-row="true"][data-project-id="${scenario.projectId}"]`; + const demandSelector = `${demandRowSelector} [data-timeline-entry-type="demand"][data-allocation-id="${scenario.demandId}"]`; await switchToProjectView(page, demandRowSelector); await expect(page.getByText(scenario.projectName).first()).toBeVisible(); @@ -1505,19 +1516,22 @@ test.describe("Timeline", () => { status: string; }> = []; await expect - .poll(() => { - rightResizeDemands = listScenarioDemands(scenario.projectId); - if (rightResizeDemands.length !== 1) { - return null; - } + .poll( + () => { + rightResizeDemands = listScenarioDemands(scenario.projectId); + if (rightResizeDemands.length !== 1) { + return null; + } - const [demand] = rightResizeDemands; - if (!demand || demand.id !== scenario.demandId) { - return null; - } + const [demand] = rightResizeDemands; + if (!demand || demand.id !== scenario.demandId) { + return null; + } - return demand.endDate; - }, { timeout: 15_000 }) + return demand.endDate; + }, + { timeout: 15_000 }, + ) .not.toBe("2026-04-16"); expect(rightResizeDemands).toHaveLength(1); expect(rightResizeDemands[0]?.id).toBe(scenario.demandId); @@ -1538,19 +1552,22 @@ test.describe("Timeline", () => { status: string; }> = []; await expect - .poll(() => { - leftResizeDemands = listScenarioDemands(scenario.projectId); - if (leftResizeDemands.length !== 1) { - return null; - } + .poll( + () => { + leftResizeDemands = listScenarioDemands(scenario.projectId); + if (leftResizeDemands.length !== 1) { + return null; + } - const [demand] = leftResizeDemands; - if (!demand || demand.id !== scenario.demandId) { - return null; - } + const [demand] = leftResizeDemands; + if (!demand || demand.id !== scenario.demandId) { + return null; + } - return demand.startDate; - }, { timeout: 15_000 }) + return demand.startDate; + }, + { timeout: 15_000 }, + ) .not.toBe("2026-04-07"); expect(leftResizeDemands).toHaveLength(1); expect(leftResizeDemands[0]?.id).toBe(scenario.demandId); @@ -1630,7 +1647,11 @@ test.describe("Timeline", () => { ); await expect(resizedSegment).toBeVisible(); - await dragLocatorBy(page, resizedSegment.locator('[data-allocation-interaction="body"]'), -dayWidth); + await dragLocatorBy( + page, + resizedSegment.locator('[data-allocation-interaction="body"]'), + -dayWidth, + ); await releaseMouse(page); await waitForScenarioAssignments(scenario.projectId, [ @@ -1674,9 +1695,21 @@ test.describe("Timeline", () => { { startDate: "2026-04-11", endDate: "2026-04-17" }, ]); - const leftSplit = row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first(); - const rightSplit = row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first(); - const nextWeekSegment = row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first(); + const leftSplit = row + .locator( + '[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]', + ) + .first(); + const rightSplit = row + .locator( + '[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]', + ) + .first(); + const nextWeekSegment = row + .locator( + '[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]', + ) + .first(); await expect(leftSplit).toBeVisible(); await expect(rightSplit).toBeVisible(); await expect(nextWeekSegment).toBeVisible(); @@ -1704,22 +1737,42 @@ test.describe("Timeline", () => { ]); await expect( - row.locator('[data-allocation-segment-start="2026-04-08"][data-allocation-segment-end="2026-04-08"]').first(), + row + .locator( + '[data-allocation-segment-start="2026-04-08"][data-allocation-segment-end="2026-04-08"]', + ) + .first(), ).toBeVisible(); await page.reload({ waitUntil: "domcontentloaded" }); await expect(row).toBeVisible(); await expect( - row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first(), + row + .locator( + '[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]', + ) + .first(), ).toBeVisible(); await expect( - row.locator('[data-allocation-segment-start="2026-04-08"][data-allocation-segment-end="2026-04-08"]').first(), + row + .locator( + '[data-allocation-segment-start="2026-04-08"][data-allocation-segment-end="2026-04-08"]', + ) + .first(), ).toBeVisible(); await expect( - row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first(), + row + .locator( + '[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]', + ) + .first(), ).toBeVisible(); await expect( - row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first(), + row + .locator( + '[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]', + ) + .first(), ).toBeVisible(); } finally { cleanupTimelineSegmentScenario(scenario.projectId, scenario.resourceId); @@ -1769,9 +1822,21 @@ test.describe("Timeline", () => { await page.reload({ waitUntil: "domcontentloaded" }); await expect(row).toBeVisible(); - const leftSplit = row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first(); - const fridayBridge = row.locator('[data-allocation-segment-start="2026-04-10"][data-allocation-segment-end="2026-04-10"]').first(); - const mondaySegment = row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first(); + const leftSplit = row + .locator( + '[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]', + ) + .first(); + const fridayBridge = row + .locator( + '[data-allocation-segment-start="2026-04-10"][data-allocation-segment-end="2026-04-10"]', + ) + .first(); + const mondaySegment = row + .locator( + '[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]', + ) + .first(); await expect(leftSplit).toBeVisible(); await expect(fridayBridge).toBeVisible(); await expect(mondaySegment).toBeVisible(); @@ -1797,13 +1862,25 @@ test.describe("Timeline", () => { await page.reload({ waitUntil: "domcontentloaded" }); await expect(row).toBeVisible(); await expect( - row.locator('[data-allocation-segment-start="2026-04-08"][data-allocation-segment-end="2026-04-09"]').first(), + row + .locator( + '[data-allocation-segment-start="2026-04-08"][data-allocation-segment-end="2026-04-09"]', + ) + .first(), ).toBeVisible(); await expect( - row.locator('[data-allocation-segment-start="2026-04-10"][data-allocation-segment-end="2026-04-10"]').first(), + row + .locator( + '[data-allocation-segment-start="2026-04-10"][data-allocation-segment-end="2026-04-10"]', + ) + .first(), ).toBeVisible(); await expect( - row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first(), + row + .locator( + '[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]', + ) + .first(), ).toBeVisible(); } finally { cleanupTimelineSegmentScenario(scenario.projectId, scenario.resourceId); @@ -1850,9 +1927,21 @@ test.describe("Timeline", () => { { startDate: "2026-04-09", endDate: "2026-04-17" }, ]); - const leftSplit = row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first(); - const rightSplit = row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first(); - const mondaySegment = row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first(); + const leftSplit = row + .locator( + '[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]', + ) + .first(); + const rightSplit = row + .locator( + '[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]', + ) + .first(); + const mondaySegment = row + .locator( + '[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]', + ) + .first(); await expect(leftSplit).toBeVisible(); await expect(rightSplit).toBeVisible(); await expect(mondaySegment).toBeVisible(); @@ -1870,8 +1959,16 @@ test.describe("Timeline", () => { { startDate: "2026-04-09", endDate: "2026-04-17" }, ]); - const resizedRightSplit = row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first(); - await dragLocatorBy(page, resizedRightSplit.locator('[data-allocation-handle="end"]'), -dayWidth); + const resizedRightSplit = row + .locator( + '[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]', + ) + .first(); + await dragLocatorBy( + page, + resizedRightSplit.locator('[data-allocation-handle="end"]'), + -dayWidth, + ); await releaseMouse(page); await waitForScenarioAssignments(scenario.projectId, [ @@ -1883,9 +1980,11 @@ test.describe("Timeline", () => { await page.reload({ waitUntil: "domcontentloaded" }); await expect(row).toBeVisible(); - const mondaySegmentAfterReload = row.locator( - '[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]', - ).first(); + const mondaySegmentAfterReload = row + .locator( + '[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]', + ) + .first(); await expect(mondaySegmentAfterReload).toBeVisible(); const mondayCarveDateInputs = page.locator('input[placeholder="dd/mm/yyyy"]'); @@ -1951,9 +2050,21 @@ test.describe("Timeline", () => { { startDate: "2026-04-09", endDate: "2026-04-17" }, ]); - const leftSplit = row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first(); - const rightSplit = row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first(); - const mondaySegment = row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first(); + const leftSplit = row + .locator( + '[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]', + ) + .first(); + const rightSplit = row + .locator( + '[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]', + ) + .first(); + const mondaySegment = row + .locator( + '[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]', + ) + .first(); await expect(leftSplit).toBeVisible(); await expect(rightSplit).toBeVisible(); await expect(mondaySegment).toBeVisible(); @@ -1968,7 +2079,11 @@ test.describe("Timeline", () => { ]); await expect( - row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first(), + row + .locator( + '[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]', + ) + .first(), ).toHaveCount(0); await expect(rightSplit).toBeVisible(); await expect(mondaySegment).toBeVisible(); @@ -1976,13 +2091,25 @@ test.describe("Timeline", () => { await page.reload({ waitUntil: "domcontentloaded" }); await expect(row).toBeVisible(); await expect( - row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first(), + row + .locator( + '[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]', + ) + .first(), ).toHaveCount(0); await expect( - row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first(), + row + .locator( + '[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]', + ) + .first(), ).toBeVisible(); await expect( - row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first(), + row + .locator( + '[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]', + ) + .first(), ).toBeVisible(); } finally { cleanupTimelineSegmentScenario(scenario.projectId, scenario.resourceId); @@ -2029,13 +2156,14 @@ test.describe("Timeline", () => { { startDate: "2026-04-09", endDate: "2026-04-17" }, ]); - const mondaySegment = resourceRow.locator( - '[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]', - ).first(); + const mondaySegment = resourceRow + .locator( + '[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]', + ) + .first(); await expect(mondaySegment).toBeVisible(); - const projectRowSelector = - `[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`; + const projectRowSelector = `[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`; await switchToProjectView(page, projectRowSelector); let mondayAssignment: { id: string; startDate: string; endDate: string } | null = null; @@ -2072,7 +2200,9 @@ test.describe("Timeline", () => { await expect(page.getByText(scenario.projectName).first()).toBeVisible(); const projectAllocationAfterReload = page - .locator(`[data-timeline-entry-type="allocation"][data-allocation-id="${mondayAssignment!.id}"]`) + .locator( + `[data-timeline-entry-type="allocation"][data-allocation-id="${mondayAssignment!.id}"]`, + ) .first(); await expect(projectAllocationAfterReload).toBeVisible(); await openContextMenuAtCenter(page, projectAllocationAfterReload); @@ -2093,15 +2223,12 @@ test.describe("Timeline", () => { const scenario = createTimelineDemandScenario(suffix); try { - await page.goto( - `/timeline?startDate=2026-04-01&days=31&projectIds=${scenario.projectId}`, - { waitUntil: "domcontentloaded" }, - ); + await page.goto(`/timeline?startDate=2026-04-01&days=31&projectIds=${scenario.projectId}`, { + waitUntil: "domcontentloaded", + }); await ensureOpenDemandVisibilityEnabled(page); - const demandRowSelector = - `[data-project-demand-row="true"][data-project-id="${scenario.projectId}"]`; - const demandSelector = - `${demandRowSelector} [data-timeline-entry-type="demand"][data-allocation-id="${scenario.demandId}"]`; + const demandRowSelector = `[data-project-demand-row="true"][data-project-id="${scenario.projectId}"]`; + const demandSelector = `${demandRowSelector} [data-timeline-entry-type="demand"][data-allocation-id="${scenario.demandId}"]`; await switchToProjectView(page, demandRowSelector); await expect(page.locator(demandSelector)).toBeVisible(); diff --git a/apps/web/e2e/vacations.spec.ts b/apps/web/e2e/vacations.spec.ts index 1aeef6f..59918bd 100644 --- a/apps/web/e2e/vacations.spec.ts +++ b/apps/web/e2e/vacations.spec.ts @@ -2,7 +2,7 @@ import { expect, test, type Page } from "@playwright/test"; async function signInAsAdmin(page: Page) { await page.goto("/auth/signin"); - await page.fill('input[type="email"]', "admin@capakraken.dev"); + await page.fill('input[type="email"]', "admin@nexus.dev"); await page.fill('input[type="password"]', "admin123"); await page.click('button[type="submit"]'); await expect(page).toHaveURL(/\/(dashboard|resources)/); @@ -27,9 +27,9 @@ test.describe("Vacations", () => { test("request vacation button is visible", async ({ page }) => { await page.waitForLoadState("networkidle"); - await expect( - page.locator("button", { hasText: /Request Vacation/i }), - ).toBeVisible({ timeout: 10000 }); + await expect(page.locator("button", { hasText: /Request Vacation/i })).toBeVisible({ + timeout: 10000, + }); }); test("request vacation is blocked without linked resource", async ({ page }) => { @@ -37,7 +37,9 @@ test.describe("Vacations", () => { const reqBtn = page.locator("button", { hasText: /Request Vacation/i }); await expect(reqBtn).toBeDisabled(); await expect( - page.getByText("Your account is not linked to a resource. Please contact an administrator."), + page.getByText( + "Your account is not linked to a resource. Please contact an administrator.", + ), ).toBeVisible({ timeout: 5000 }); }); }); @@ -57,11 +59,18 @@ test.describe("Vacations", () => { test("team calendar tab renders", async ({ page }) => { await page.waitForLoadState("networkidle"); - await page.locator("button", { hasText: "Team Calendar" }).or(page.locator("text=Team Calendar")).first().click(); + await page + .locator("button", { hasText: "Team Calendar" }) + .or(page.locator("text=Team Calendar")) + .first() + .click(); await page.waitForTimeout(500); // Calendar view should appear await expect( - page.locator("table").or(page.locator("[data-calendar]")).or(page.locator("text=Mon").or(page.locator("text=Week"))), + page + .locator("table") + .or(page.locator("[data-calendar]")) + .or(page.locator("text=Mon").or(page.locator("text=Week"))), ).toBeVisible({ timeout: 10000 }); }); @@ -75,11 +84,15 @@ test.describe("Vacations", () => { await expect(filters.nth(2)).toHaveValue(""); }); - test("vacation request preview excludes regional public holidays from deducted days", async ({ page }) => { + test("vacation request preview excludes regional public holidays from deducted days", async ({ + page, + }) => { await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: /request vacation/i }).click(); - await expect(page.getByLabel(/^type/i).locator("option", { hasText: /Public Holiday/i })).toHaveCount(0); + await expect( + page.getByLabel(/^type/i).locator("option", { hasText: /Public Holiday/i }), + ).toHaveCount(0); await page.getByLabel(/resource/i).selectOption({ label: "Bruce Banner (bruce.banner)" }); await page.getByLabel(/^type/i).selectOption("ANNUAL"); await fillDisplayDate(page, /start date/i, "2026-01-06"); @@ -89,9 +102,13 @@ test.describe("Vacations", () => { await expect(page.getByTestId("vacation-preview-requested-days")).toHaveText("1"); await expect(page.getByTestId("vacation-preview-effective-days")).toHaveText("0"); await expect(page.getByTestId("vacation-preview-deducted-days")).toHaveText("0"); - await expect(page.getByTestId("vacation-preview-public-holidays")).toContainText("2026-01-06"); + await expect(page.getByTestId("vacation-preview-public-holidays")).toContainText( + "2026-01-06", + ); await expect(page.getByTestId("vacation-preview-holiday-basis")).toContainText("Germany"); - await expect(page.getByTestId("vacation-preview-holiday-sources")).toContainText("Holiday Calendar"); + await expect(page.getByTestId("vacation-preview-holiday-sources")).toContainText( + "Holiday Calendar", + ); }); }); diff --git a/apps/web/eslint.config.mjs b/apps/web/eslint.config.mjs index 0579d6e..d711200 100644 --- a/apps/web/eslint.config.mjs +++ b/apps/web/eslint.config.mjs @@ -1,4 +1,4 @@ -import nextjsConfig from "@capakraken/eslint-config/nextjs"; +import nextjsConfig from "@nexus/eslint-config/nextjs"; /** @type {import("eslint").Linter.FlatConfig[]} */ export default [ diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 1adf2d8..3369527 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -11,16 +11,16 @@ const nextConfig: NextConfig = { "recharts", "date-fns", "framer-motion", - "@capakraken/shared", + "@nexus/shared", "@react-pdf/renderer", ], }, transpilePackages: [ - "@capakraken/api", - "@capakraken/db", - "@capakraken/engine", - "@capakraken/shared", - "@capakraken/staffing", + "@nexus/api", + "@nexus/db", + "@nexus/engine", + "@nexus/shared", + "@nexus/staffing", ], typedRoutes: true, eslint: { diff --git a/apps/web/package.json b/apps/web/package.json index b98fa23..f0b6232 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,5 +1,5 @@ { - "name": "@capakraken/web", + "name": "@nexus/web", "version": "0.1.0", "private": true, "scripts": { @@ -13,11 +13,11 @@ "test:e2e:email": "playwright test --config playwright.dev.config.ts e2e/dev-system/invite-flow.spec.ts e2e/dev-system/password-reset.spec.ts" }, "dependencies": { - "@capakraken/api": "workspace:*", - "@capakraken/application": "workspace:*", - "@capakraken/db": "workspace:*", - "@capakraken/engine": "workspace:*", - "@capakraken/shared": "workspace:*", + "@nexus/api": "workspace:*", + "@nexus/application": "workspace:*", + "@nexus/db": "workspace:*", + "@nexus/engine": "workspace:*", + "@nexus/shared": "workspace:*", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -34,7 +34,7 @@ "dompurify": "^3.4.0", "exceljs": "^4.4.0", "framer-motion": "^12.38.0", - "next": "^15.5.15", + "next": "^15.5.16", "next-auth": "^5.0.0-beta.25", "otpauth": "^9.5.0", "qrcode": "^1.5.4", @@ -51,8 +51,8 @@ "devDependencies": { "@next/bundle-analyzer": "^16.2.3", "@axe-core/playwright": "^4.11.1", - "@capakraken/eslint-config": "workspace:*", - "@capakraken/tsconfig": "workspace:*", + "@nexus/eslint-config": "workspace:*", + "@nexus/tsconfig": "workspace:*", "@playwright/test": "^1.49.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", diff --git a/apps/web/playwright.dev.config.ts b/apps/web/playwright.dev.config.ts index bf44a41..686f2f0 100644 --- a/apps/web/playwright.dev.config.ts +++ b/apps/web/playwright.dev.config.ts @@ -6,7 +6,7 @@ * dev server at localhost:3100 and exercises real dev-DB data. * * Usage: - * pnpm --filter @capakraken/web exec playwright test --config playwright.dev.config.ts + * pnpm --filter @nexus/web exec playwright test --config playwright.dev.config.ts * * Prerequisites: * - Dev server running: pnpm run dev (or docker compose up) diff --git a/apps/web/public/manifest.json b/apps/web/public/manifest.json index e72a0f0..cbb9d9b 100644 --- a/apps/web/public/manifest.json +++ b/apps/web/public/manifest.json @@ -1,6 +1,6 @@ { - "name": "CapaKraken — Resource & Capacity Planning", - "short_name": "CapaKraken", + "name": "Nexus — Resource & Capacity Planning", + "short_name": "Nexus", "description": "Resource planning and project staffing for 3D production", "start_url": "/dashboard", "display": "standalone", diff --git a/apps/web/public/sw.js b/apps/web/public/sw.js index a64ed96..2d15339 100644 --- a/apps/web/public/sw.js +++ b/apps/web/public/sw.js @@ -1,6 +1,6 @@ /// -const CACHE_NAME = "capakraken-v2"; +const CACHE_NAME = "nexus-v2"; const STATIC_EXTENSIONS = /\.(js|css|png|jpg|jpeg|svg|gif|ico|woff2?|ttf|eot)$/; // Offline fallback page (simple inline HTML) @@ -9,7 +9,7 @@ const OFFLINE_HTML = ` - CapaKraken - Offline + Nexus - Offline