Commit Graph

442 Commits

Author SHA1 Message Date
Hartmut 78d50b78d3 fix: script portability and npm security updates
Scripts:
- stop.sh: replace Linux-only fuser with cross-platform lsof fallback
- start.sh: parameterize port (APP_PORT) and container name (dynamic lookup)
- app-dev-start.sh: cross-platform stat (GNU -c / BSD -f) and setpriv/su fallback
- deploy-compose.sh: parameterize Docker registry via DOCKER_REGISTRY env var
- harden-postgres.sh: make DB_USER and DB_NAME configurable via env vars

NPM security:
- next: 15.5.12 → 15.5.15 (fixes HTTP request smuggling CVE)
- nodemailer: 8.0.1 → 8.0.5 (fixes SMTP command injection CVEs)
- lodash-es: add pnpm override to force >=4.18.0 (fixes code injection + prototype pollution)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 14:06:58 +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 4eac7b1888 fix(infra): apply missing migrations, fix Dockerfile.dev ui package reference 2026-04-09 20:29:21 +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 b573a1d554 Merge branch 'worktree-agent-a74dc5bc' 2026-04-09 14:18:50 +02:00
Hartmut a2613f6e93 Merge branch 'worktree-agent-a0063a64' 2026-04-09 14:18:47 +02:00
Hartmut 96679f5fec Merge branch 'worktree-agent-acdb74a3' 2026-04-09 14:18:27 +02:00
Hartmut b9fd7fdb03 perf(staffing): replace day-by-day capacity loop with range arithmetic
Eliminates O(resource × allocation × days) iteration in findCapacityWindows
by pre-computing vacation date sets and using direct range overlap math.
Adds performance regression test (50 resources × 20 allocs × 365 days < 500ms).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:17:26 +02:00
Hartmut fbca017eaa test(application): add unit tests for demand fill logic and capacity vacation overlap
Covers fill-demand-requirement status validation, duplicate detection,
fill-open-demand happy path, and vacation overlap edge cases in
capacity analyzer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:15:00 +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 aebe5bc57d fix(db): add SetNull cascade on Assignment→DemandRequirement + composite indexes
Prevents orphaned Assignment rows when a DemandRequirement is deleted.
Adds (resourceId, status, endDate) and (projectId, status, endDate)
indexes to support capacity range queries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:03:11 +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 b944a17572 feat(db,shared): add overbookingAcknowledged field and conflict check types
- Assignment.overbookingAcknowledged Boolean @default(false) — audit trail
  for intentional overbookings
- CreateAssignmentBaseSchema gets allowOverbooking?: boolean flag so callers
  can explicitly opt in to overbooking
- Export AllocationConflictCheckResult, AllocationConflictDay,
  AllocationVacationOverlap types from @capakraken/shared for use in the
  new conflict-check API procedure and AllocationModal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 10:11:02 +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
Hartmut fba65387fe feat(resources): add hard-delete action to resource list (per-row and batch)
- Add batchHardDelete adminProcedure to resource-mutations router
- Per-row Delete button visible to ADMIN role only
- Delete Selected button in BatchActionBar for ADMIN role only
- Two-step confirmation dialogs with permanent-action warnings
- Audit log written for each deleted resource

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-03 23:18:30 +02:00
Hartmut 0d0707264d feat(admin): hard-delete resources (admin-only)
Adds a transactional hard-delete procedure behind adminProcedure that
removes a resource's assignments and vacations first, then the record
itself, and writes an audit log entry.  The ResourceModal exposes a
"Delete Resource" button (edit mode, ADMIN role only) with an inline
confirm step before the mutation fires.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-03 15:23:30 +02:00
Hartmut ed4d4e4640 test(api): fill router auth and security coverage gaps
Four new test files — 27 tests total:
- role-router-auth.test.ts (8): UNAUTHORIZED/FORBIDDEN on all mutations for
  unauthenticated/USER callers; MANAGER and ADMIN happy paths
- webhook-router-auth.test.ts (6): adminProcedure guard verified for all
  six webhook procedures across USER/MANAGER/ADMIN roles
- comment-sanitization-router.test.ts (4): proves stripHtml runs before
  db.comment.create — script tags stripped, plain text and @mentions preserved
- auth-anomaly-check/route.test.ts (+5 unit tests): detectAuthAnomalies()
  unit coverage — empty window, global threshold, per-entity threshold, null
  entityId, and both anomaly types firing simultaneously

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-02 23:31:26 +02:00
Hartmut e3c585a403 test(scenario): add unit regression coverage for all four scenario modules
Previously untested business logic — no direct tests existed for the
scenario domain beyond auth guards and delegation stubs.

scenario-shared.test.ts (13 tests)
  - roundToTenths: rounding edge cases
  - getScenarioAvailability: null/undefined fall through to DEFAULT_AVAILABILITY
  - collectScenarioSkillSet: null, empty, lowercase, dedup, whitespace filter
  - calculateScenarioEntryHours: null resourceId → calculateAllocation,
    non-null → calculateEffectiveBookedHours with context map lookup

scenario-apply.test.ts (6 tests)
  - NOT_FOUND guard
  - remove:true → CANCELLED status, not counted in appliedCount
  - assignmentId without remove → update branch, appliedCount 1
  - no assignmentId / no resourceId → skipped, appliedCount 0
  - resourceId only → create with computed dailyCostCents (lcrCents × hours)
  - mixed changes → correct aggregate appliedCount

scenario-baseline.test.ts (6 tests)
  - NOT_FOUND guard
  - empty project → zeroed totals
  - costCents computed from lcrCents × effective hours
  - CANCELLED assignments excluded via findMany WHERE filter
  - demands mapped with headcount and roleName from roleEntity
  - totalCostCents is sum of all assignment costCents

scenario-simulation.test.ts (6 tests)
  - NOT_FOUND guard
  - unchanged carry-through → delta.headcount 0
  - remove change → delta.headcount -1
  - new resource → delta.headcount +1
  - budget exceeded → warnings includes /exceeds budget/i
  - skill coverage → delta.skillCoveragePct > 0 when scenario adds skills

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-02 21:30:46 +02:00
Hartmut e7e525df49 fix(timeline): clear multi-select on drag start and lock in SSE edge-case coverage
- useTimelineDrag: onProjectBarMouseDown and single-alloc drag path now reset
  multiSelectRef + multiSelectState before starting a new drag, so the
  FloatingActionBar is dismissed immediately when an unrelated drag begins
- FloatingActionBar.test.tsx: 4 regression tests for the null-render guard
  (count=0) and all three label variants
- useTimelineSSE.test.ts: 2 new tests — tab hides during pending reconnect
  timer (clears timer, resyncs on next open) and first-ever connection fails
  before any open (retry open still resyncs correctly)
- assistant-tools-user-admin-inventory-read.test.ts: add isActive to expected
  findMany select shape (already in production, test was stale)

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-02 21:16:10 +02:00
Hartmut 41eb722369 feat: user invite flow, deactivate/delete, favicon, dashboard loading fix, admin full-width
- Invite flow: admin can invite users by email with role selection; accept-invite page
  sets password and creates the account; 72-hour token expiry; E2E tests
- User deactivate/reactivate/delete: new tRPC procedures + UI buttons; deactivation
  revokes all active sessions immediately; delete cascades vacation/broadcast records;
  isActive field added via migration 20260402000000_user_isactive
- Auth: block login for inactive users with audit entry
- Favicon: SVG favicon + ICO/PNG fallbacks (16, 32, 180, 192, 512px); manifest updated
- Dashboard: GridLayout dynamic-import loading skeleton prevents blank dark area
  on first login before react-grid-layout chunk is cached
- Admin users: remove max-w-5xl constraint so table uses full page width
- Dev: docker container restart workflow documented in LEARNINGS.md; Prisma generate
  must run inside the container after schema changes (named node_modules volume)

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-02 20:19:26 +02:00