Commit Graph

435 Commits

Author SHA1 Message Date
Hartmut 85c064ba32 fix(api): harden raw SQL jsonb field validation in batchUpdateCustomFields
Replace z.unknown() with z.union([z.string(), z.number(), z.boolean(), z.null()])
to constrain what values can be written into the dynamicFields jsonb column via
the $executeRaw path. Prevents arbitrary nested structures from being serialized.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 23:23:43 +02:00
Hartmut 5a4836d292 perf(api): eliminate 3 N+1 query patterns
- timeline-holiday-load-support: deduplicate getResolvedCalendarHolidays
  by location key so resources sharing the same country/state/city resolve
  holidays once instead of per-resource
- rate-card-lookup: add lookupRatesBatch that loads rate card lines once
  and scores locally per demand line, replacing per-line DB round-trips
  in estimate-demand-lines autoFillDemandLineRates
- config-readmodels: include _count in utilization-category list query
  instead of calling getById per category for project counts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 22:59:45 +02:00
Hartmut dd2c9c0f88 perf(api,web,db): refactor and optimize for enterprise readiness
- Add missing @@index([userId]) on Account and Session models (auth query perf)
- Batch holiday-auto-import to eliminate N+1 query pattern (O(n) → O(1))
- Reduce SessionProvider refetchInterval from 5min to 15min
- Fix Cache-Control catch-all to stop blocking static asset caching
- Decompose assistant-tools.ts (2,562 → 809 lines) into callers, helpers, access-control modules
- Add @next/bundle-analyzer for data-driven bundle optimization
- Add @react-pdf/renderer to optimizePackageImports
- Add safety caps (take limits) on unbounded findMany queries

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 22:34:41 +02:00
Hartmut e3551fb78f fix(api): validate rolePresets with RolePresetsSchema before DB cast
Replace z.array(z.unknown()) with RolePresetsSchema for blueprint
role presets mutation input, ensuring structural validation before
Prisma JSON cast. Also adds SECURITY.md for vulnerability disclosure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 08:35:02 +02:00
Hartmut c098cedf06 perf(db): add missing indexes, fix N+1 batch delete, add pagination limits
- Add indexes on Resource(blueprintId, roleId), DemandRequirement(roleId),
  Assignment(roleId) — commonly filtered FK columns that were missing indexes
- Replace N+1 batch delete pattern (2N queries) with findAllocationEntries()
  that does 2 total queries via findMany({ id: { in: ids } })
- Add take/skip pagination with default limit of 500 to listDemands and
  listAssignments to prevent unbounded result sets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 08:09:39 +02:00
Hartmut 110e4ff1aa fix(security): harden auth reset, rate limiter fallback, and CI secrets
- Move CI_AUTH_SECRET from plaintext to ${{ secrets.CI_AUTH_SECRET }}
- Wrap password reset (update + session kill + token mark) in $transaction
  to prevent stale sessions on partial failure (CWE-613)
- Rate limiter Redis fallback now uses stricter degraded limits
  (maxRequests/10) and logs at error level instead of warn

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 08:03:42 +02:00
Hartmut 2f2fe2631f test(api): add 38 tests for project read, project cost, and staffing shared utils
Project identifier: 4-step fallback lookup, search summaries with fuzzy notes.
Project cost: pagination, cost/person-day calculations, utilization percent.
Staffing shared: createDateRange, ACTIVE_STATUSES, createLocationLabel,
calculateAllocatedHoursForDay with absence fractions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 16:49:23 +02:00
Hartmut 45cf7b8c29 test(api): add 36 tests for insights anomalies and resource identifier read
Insights: budget burn rate, staffing gaps, timeline overruns, utilization thresholds,
summary counts, sorting. Resource: resolveByIdentifier, getHoverCard, getById,
getByEid with alias fallback, getByIdentifierDetail mapping.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 16:45:26 +02:00
Hartmut 378ed61002 test(api): add 34 router tests for estimate read/workflow and vacation read
Covers estimate list, getById, version snapshot aggregation, rethrowEstimateRouterError,
submit/approve/createRevision workflow procedures. Vacation read covers isSameUtcDay,
list, getById, getForResource, team overlap, and team overlap detail.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 16:41:18 +02:00
Hartmut a0de69a520 test(api): add 68 router tests for comment, project-lifecycle, dispo, holiday-calendar
Covers comment CRUD/resolve/delete, project status transitions and cascade
deletes, dispo import batch read/cancel/commit/resolve, and holiday calendar
catalog read with identifier fallback lookup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 16:37:02 +02:00
Hartmut 2484eb9b9d test(api): add 50 router tests for settings, webhook, and calculation rules
Phase 3c continued: covers admin settings CRUD with secret handling,
webhook lifecycle with SSRF validation, and calculation rules with
controller/manager authorization boundaries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 16:29:10 +02:00
Hartmut efe3b96676 test(api): add 48 router tests for client, role, and blueprint CRUD
Phase 3c: covers list/getById/create/update for all three routers
including authorization guards, conflict detection, NOT_FOUND errors,
and audit logging verification.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 16:26:12 +02:00
Hartmut dfeb4d361e fix(tests): align 20 drifted tests with current source behavior
Tests fell behind source changes: lastTotpAt replay-attack prevention,
activeSession invalidation on password reset, select clauses in
permission updates, UNAUTHORIZED (anti-enumeration) for disabled TOTP,
and password minimum raised from 8 to 12 characters.

Also fix root eslint.config.mjs to ignore packages/ (linted via turbo)
and add --no-warn-ignored to lint-staged to suppress warnings for
ignored files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 15:41:42 +02:00
Hartmut 9bd3781c03 fix(types): flatten tRPC Zod schema types to resolve TS2589 inference depth errors
Cast Zod schemas with .refine()/.superRefine() to z.ZodType<InferredType> at the
procedure level. This short-circuits TypeScript's deep type recursion through
tRPC's middleware chain, eliminating 4 of 5 @ts-expect-error TS2589 suppressions
in web components (VacationModal, ProjectModal, UsersClient, CountriesClient).

Applied same pattern to allocation, timeline, staffing, dashboard, project, and
resource query/mutation procedures to reduce client-side type depth.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 15:28:12 +02:00
Hartmut 9051ff73d0 fix(types): replace structural DB types with Pick<PrismaClient> and remove Prisma boundary as any casts
Replace ~440 lines of hand-written structural DB client types across 7 lib files
with `Pick<PrismaClient, ...>` from @capakraken/db. This eliminates all `as any`
casts at Prisma boundaries (cron routes, allocation effects, vacation procedures)
and surfaces two pre-existing bugs:
- weekly-digest.ts: `db.allocation.count()` called non-existent model (fixed → demandRequirement)
- estimate-reminders.ts: `submittedAt` field doesn't exist on EstimateVersion (fixed → updatedAt)

Also adds root eslint.config.mjs so lint-staged can lint package files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 15:09:16 +02:00
Hartmut 97cfd0ed90 fix(security): raise password minimum to 12 chars, hide raw error messages, add audit script
- Password validation: min(8) → min(12) across auth.ts, user-procedure-support.ts,
  and invite.ts (aligns with NIST SP 800-63B modern recommendations)
- Error boundary: stop rendering raw error.message which could leak internal
  details; always show the generic fallback text
- Add `pnpm audit` script (--audit-level=high) for dependency vulnerability scanning

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 21:48:51 +02:00
Hartmut afabaa0b7a fix(security): prevent TOTP replay attacks and fix user enumeration in verifyTotp
Adds lastTotpAt timestamp to User model. After a successful TOTP validation,
the timestamp is recorded. Any reuse of the same code within the 30-second
window is rejected as a replay attack.

verifyTotp now returns a single generic UNAUTHORIZED error regardless of
whether the user ID is invalid or TOTP is not enabled, preventing enumeration
of user IDs and MFA status.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 21:41:09 +02:00
Hartmut 1833182e90 fix(security): harden input validation schemas and fix SSR sanitize bypass
- blueprint rolePresets: cap array at 100 items to prevent storage abuse
- notification CreateManagedNotification: add .max() on title (500),
  body (2000), type (100), entityType/entityId (200), link (1000),
  taskAction (200)
- settings: add .max() on all string config fields; add regex allowlist
  (/^[a-zA-Z0-9._-]+$/) on model name fields (geminiModel,
  azureDalleDeployment, azureOpenAiDeployment) to prevent path manipulation
- sanitizeHtml: fix SSR bypass — server-side branch now strips HTML tags
  instead of returning the raw string unchanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 21:38:16 +02:00
Hartmut df191d1e03 fix(security): rate-limit public invite and password-reset endpoints
- requestPasswordReset: rate-limited by email (authRateLimiter, 5/15 min)
  to prevent email bombing
- resetPassword: rate-limited by token to add explicit brute-force defence
- getInvite + acceptInvite: rate-limited by invite token (authRateLimiter)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 21:38:08 +02:00
Hartmut 3452464809 fix(security): invalidate sessions on password change and remove hash from permission API responses
- setUserPassword and resetPassword now call activeSession.deleteMany after
  updating the passwordHash, so any pre-change sessions are immediately revoked
  (CWE-613 session fixation after credential change)
- setUserPermissions and resetUserPermissions now use explicit Prisma select to
  exclude passwordHash and totpSecret from the returned user object

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 21:37:56 +02:00
Hartmut 43de66e982 feat(api): add audit helpers, tool registry, shared tool manifest types, and UI primitives
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 21:14:26 +02:00
Hartmut 5ad1048519 feat(dashboard): expand grid to 16 columns with auto-migration for saved 12-col layouts 2026-04-09 20:50:40 +02:00
Hartmut c83bd5f97f Merge branch 'worktree-agent-a8de1898' - Phase 2A entitlement use-cases 2026-04-09 20:16:38 +02:00
Hartmut 999626cf70 feat(application): extract entitlement use-cases from API router layer
Move core entitlement business logic (syncEntitlement, balance reading,
year summary, set/bulk-set) into packages/application/src/use-cases/entitlement/
using the deps-injection pattern. Audit logging stays in the router support
file; authorization check for getBalance/getBalanceDetail stays in the router
layer. The router support file becomes a thin wiring adapter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 20:14:35 +02:00
Hartmut 1a8ea11331 feat(db): add deletedAt audit timestamp to soft-deletable models
Add deletedAt DateTime? to User, Client, Role, Resource, and Blueprint
models for GDPR-compliant deactivation audit trail. Soft-delete mutations
now stamp deletedAt: new Date() on deactivation and clear it on
reactivation. Migration and test assertions updated accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 20:03:38 +02:00
Hartmut f7407bd882 fix(api): add centralized Prisma → TRPCError middleware on protectedProcedure
P2002/P2025/P2003 now map to CONFLICT/NOT_FOUND/BAD_REQUEST with generic
messages. Raw Prisma error details no longer reach the client.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:58:00 +02:00
Hartmut 245b59723a fix(api): update resource-router tests for select-based listStaff queries
Tests expected include: { resourceRoles } but the Prisma select audit
changed the query to select: { ...RESOURCE_LIST_SELECT, resourceRoles }.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:33:58 +02:00
Hartmut db83933ea1 Merge branch 'worktree-agent-aed43cff' 2026-04-09 19:31:50 +02:00
Hartmut 6f3bdd81e8 perf(api): add explicit Prisma selects on hot read paths
Replaces full model includes with field-scoped selects on the resource
list (listStaff) query. Avoids fetching large JSONB columns
(availability, valueScoreBreakdown) and unused scalar fields (aiSummary,
portfolioUrl, fte, resourceType, postalCode, etc.) when only
identity/rate fields are needed.

Adds RESOURCE_LIST_SELECT constant to packages/api/src/db/selects.ts
covering all fields actually consumed by ResourcesClient, FillOpenDemandModal,
EstimateWizard, EstimateWorkspaceDraftEditor, and ScenarioPlanner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:24:55 +02:00
Hartmut d737a251b2 fix(api): replace ctx.session.user lookups with ctx.dbUser
Procedures were re-fetching the acting user from DB using the session
email, which breaks if email changes between session creation and request.
ctx.dbUser is populated by protectedProcedure and is always current.

Also removed the now-unused findVacationActor helper function.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:18:39 +02:00
Hartmut 070be70848 refactor(application): extract vacation management into application use-cases
Moves approve, reject, cancel, and request vacation business logic
out of the tRPC procedure layer into packages/application, matching
the pattern used by allocation use-cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 17:11:37 +02:00
Hartmut dda049075f refactor(application): extract vacation management into application use-cases
Moves approve, reject, cancel, and request vacation business logic
out of the tRPC procedure layer into packages/application, matching
the pattern used by allocation use-cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:49:45 +02:00
Hartmut 485e220c49 fix(api,web): env startup validation, QueryClient defaults, warn on missing REDIS_URL
- Throw at startup in production if REDIS_URL/DATABASE_URL/NEXTAUTH_SECRET missing
- Warn in development when REDIS_URL falls back to localhost
- QueryClient: add gcTime, disable refetchOnWindowFocus, skip retry on 4xx

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:42:34 +02:00
Hartmut 3c0179fcec fix(api): wrap audit log writes inside their parent transactions
Prevents mutations from committing without an audit trail if the
auditLog.create call fails after the main write already succeeded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:40:10 +02:00
Hartmut a01f99561d fix(api): fix import paths missed by router reorganisation
- allocation-conflict-procedures: allocation-shared.js → allocation/shared.js
- allocation/index.ts: add missing allocationConflictProcedures spread
- allocation-conflict-check.test.ts: router/allocation.js → allocation/index.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:47:16 +02:00
Hartmut 1a8ed97d5e Merge branch 'worktree-agent-a2939317' 2026-04-09 14:44:51 +02:00
Hartmut b2c8d98b25 refactor(api): reorganise allocation router into allocation/ subdirectory
Moves read, assignment-procedures, assignment-mutations, and demand
procedures into allocation/ so the domain boundary is discoverable
without grep.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:44:17 +02:00
Hartmut 75167d6129 fix(merge): resolve post-merge type errors from batch-1 agents
- ScenarioPlanner.Baseline.shortCode: string → string | null (matches Prisma)
- ScenarioPlanner.SimulationResult.chargeabilityTarget: number → number | null
- Remove runtime Zod parse from scenario procedures (typed by Prisma already)
- Float64Array index access: add non-null assertions for noUncheckedIndexedAccess

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:38:32 +02:00
Hartmut 0fcb481350 Merge branch 'worktree-agent-a90e1bc2' 2026-04-09 14:19:18 +02:00
Hartmut 9a42615a21 fix(api): add Zod bounds on financial fields, type vacation router, type scenarioData
- dailyCostCents, hoursPerDay, percentage now validated at API boundary
- vacation router no longer uses ctx.db as any
- scenarioData reads through typed Zod schema

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:08:16 +02:00
Hartmut ab4ec91e02 feat(digest): add weekly capacity digest email cron
Sends a Monday digest to all ADMIN + MANAGER users with:
- Team utilization % for the next 4 weeks
- Overbooked resource count
- Open demand count
- Upcoming vacation count
- Top 5 most utilized resources

Route: GET /api/cron/weekly-digest (secured by CRON_SECRET).
HTML template and plain-text fallback included.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 13:33:12 +02:00
Hartmut 1df208dbcc feat(timeline): add pulse animation for in-flight drag mutations
Allocation bars that have active optimistic overrides (post-drag,
awaiting server confirmation) now pulse subtly via animate-pulse.
The pending set is derived from the existing optimisticAllocations
map keys, requiring no additional state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 13:28:46 +02:00
Hartmut 24435a1824 test(allocation): add conflict check tests for checkConflicts query
Covers: no-conflict baseline, overbooking detection with per-day breakdown,
vacation overlap reporting, edit-mode excludeAssignmentId exclusion,
NOT_FOUND guard, and fallback country-hours capacity path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 10:20:37 +02:00
Hartmut 61e52e9995 feat(api,application): add checkConflicts query and soften overbooking block
- New allocation.checkConflicts managerProcedure: returns per-day overbooking
  breakdown (availableHours, existingHours, requestedHours, overageHours,
  maxOverbookPercent) plus vacation overlap list for the requested period.
  Read-only — used by AllocationModal for pre-submission warnings.
- createAssignment(): replace the hard >5-day overbooking block with a soft
  CONFLICT error. When allowOverbooking: true is passed the assignment is
  created and overbookingAcknowledged is set to true on the record.
- allowOverbooking field added to CreateAssignmentBaseSchema (optional)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 10:13:18 +02:00
Hartmut 60d267fa0a feat(api): add SSE subscriber isolation, token pruning and E2E rate-limit guard
- event-bus: wrap each subscriber.fn call in try/catch so one throwing subscriber cannot kill delivery to all others
- event-bus: log Redis parse errors instead of swallowing them silently; add .catch() on Redis publish promise for async fallback to local delivery
- pruning.ts: new runPruning() deletes expired invite tokens, expired password-reset tokens, and read notifications older than 90 days
- settings.runPruning: expose pruning as adminProcedure mutation
- trpc.ts: E2E_TEST_MODE rate-limit bypass is now a no-op in production (NODE_ENV=production); logs a startup warning if misconfigured

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 08:35:39 +02:00
Hartmut 1204c186ef perf(api): eliminate N+1 queries, add query guards and missing indexes
- Notification fan-out: replace sequential for loops with Promise.all (allocation-effects, notification-broadcast, create-notification)
- Public holiday batch: group resources by location combo, resolve holidays once per group, replace per-holiday delete/findFirst/create with 3 batched queries (~18K → ~5 queries)
- Add take guards to unbounded findMany calls (resource-analytics: 5000, resource-marketplace: 2000, resource-capacity: 1000, chargeability-report: 2000)
- auto-staffing: add select with only needed fields + take: 5000
- schema.prisma: add 5 missing indexes (ManagementLevel.groupId, Blueprint.isActive/target, Comment.parentId, Vacation.requestedById, Resource.managementLevelGroupId)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 08:35:13 +02:00
Hartmut 1d6d75ecf6 fix(api): wrap critical mutations in transactions and fix TOCTOU race conditions
- applyProjectScenario: wrap assignment loop in db.$transaction to prevent partial updates
- vacation approve/reject: fix TOCTOU race via updateMany with status-guard in WHERE + CONFLICT on count=0
- vacation cancel: wrap vacation.update + entitlement.updateMany in $transaction
- batchApprove: collect mutations, wrap in $transaction, dispatch SSE/notifications after commit
- Fix dead-code bug in createHappyPathDb where $transaction was assigned after return
- Add atomicity and concurrency tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 08:34:59 +02:00
Hartmut d2caba8d7c fix(test): use relative dates in insights summary test
Hardcoded dates (2026-03-20 / 2026-04-05) were now in the past, causing
the demand window filter (endDate >= now) to exclude the mock demand
requirement and miss the expected staffing anomaly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 09:40:14 +02:00
Hartmut a9ad1ed8b6 feat(G-08): chapter field uses live datalist from resource.chapters
All chapter text inputs now show autocomplete suggestions from the
database (distinct chapter values from active resources) via HTML
<datalist> wired to trpc.resource.chapters:

- ResourceModal: chapter input
- RateCardsClient: rate card line chapter input
- EffortRulesClient: effort rule chapter input
- ExperienceMultipliersClient: replaces hardcoded CHAPTER_PRESETS
  with live data, falls back to presets when no data available

Also revert blueprintRolePresetsInputSchema to z.array(z.unknown())
to restore compatibility with StaffingRequirement[] call sites.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 08:10:36 +02:00
Hartmut 4a49ec4f05 fix(sanity): resolve 15 gaps from sanity check audit (G-01 through G-15)
- G-01: ProjectWizard renders blueprint fieldDefs with DynamicFieldInput component
- G-02: Blueprint rolePresets validated via RolePresetsSchema in wizard; API keeps loose schema
- G-03: ProjectWizard step 2/3 validation (role, hoursPerDay, headcount required)
- G-04: EstimateWizard validates baseCurrency and demand line cost rates
- G-05: Project lifecycle transition guards with ALLOWED_TRANSITIONS map
- G-06: Blueprint validator extended for minLength/maxLength/pattern and DATE range checks
- G-07: assertBlueprintDynamicFields merges global blueprint fieldDefs into validation
- G-08: (tracked — chapter managed dropdown; deferred to backend ticket)
- G-09: JSDoc added to lcrCents/ucrCents clarifying LCR/UCR terminology
- G-10: Dispo route redirect already in place — closed as done
- G-11: packages/ui empty by design — closed as documented
- G-12: @deprecated JSDoc added to CreateAllocationSchema and UpdateAllocationSchema
- G-13: ProjectWizard review step enhanced with blueprint name, field values, skills, assignments
- G-14: ProjectWizard handleSubmit collects per-item warnings instead of silent swallowing
- G-15: Vacation cancel reverses usedDays entitlement for APPROVED ANNUAL/OTHER vacations

Tests: all 1575 passing (1 pre-existing failure in insights-summary unrelated to these changes)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 00:11:12 +02:00