useLayoutEffect([]) fired before isInitialLoading resolved, so the
scroll container had no canvas yet — scrollLeft was clipped to 0.
Now the scroll-to-today fires on the first render where totalCanvasWidth
becomes non-zero. The cleanup effect resets the guard on unmount so
React Strict Mode's fake-unmount+remount also scrolls correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The guard-ref approach broke in React Strict Mode (dev): the ref
persisted as `true` across the simulated remount, so the second
invocation skipped the scroll — leaving scrollLeft=0 (today-90
at the left edge, not today). An empty-deps useLayoutEffect runs
twice in Strict Mode but both executions fire against the same
initial `toLeft` and produce the correct result.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
viewStart=today left no canvas to the left of scrollLeft=0, making
left-scroll physically impossible. Now viewStart defaults to today-90
so the canvas always has 90 days to scroll into, and a mount-time
useLayoutEffect positions the viewport with today at the left edge.
The Today button restores this view: scrolls in-range, or resets
viewStart and schedules a post-layout scroll if today has scrolled
out of the visible window.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously viewStart defaulted to today-30 and the scroll container had
no left-edge expansion logic, so users hit a hard wall when scrolling
left. This change:
- Sets viewStart default to today so the viewport opens with today at
the left edge (URL ?startDate= override still respected).
- Adds left-edge auto-expansion in handleContainerScroll: when the user
scrolls within 40 cells of the left boundary, 120 days are prepended
and a useLayoutEffect applies the matching scrollLeft compensation in
the same paint frame to prevent a visual jump.
- Floors backward navigation at 5 years (minDate) to prevent unbounded
viewDays growth.
- Updates handleNavigateToday to match: resets to today rather than
today-30.
Both resource view and project view use the same TimelineContext /
TimelineView, so both are fixed by this change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a one-time-use backup code set so users with a lost authenticator are not
locked out. Codes are Crockford base32 (XXXXX-XXXXX), hashed with argon2id, and
redeemed under a WHERE-guarded delete so a concurrent replay race fails closed.
- New MfaBackupCode model + migration
- Issue 10 codes inside the enable transaction; show plaintext exactly once
- Sign-in page accepts TOTP or backup code, reporting remaining count
- regenerateBackupCodes tRPC mutation wipes + reissues atomically
- Unit coverage for generator, normalizer, verify, redeem, and race path
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Client-side validators (reset-password, invite-accept, first-admin setup,
user-create modal) previously checked password.length < 8 while every
server-side Zod schema required .min(12). External API consumers (or a
confused browser UI) could get past the client check but fail at the tRPC
boundary — or worse, quietly under-enforce policy compared to what
admins expect.
Fix: introduce PASSWORD_MIN_LENGTH (12) and PASSWORD_MAX_LENGTH (128) in
@capakraken/shared and import them from every pre-submit client validator
and every server Zod schema. Single source of truth; drift becomes a
compile error rather than a security finding.
Also hardens the AUTH_SECRET runtime check: in addition to the existing
placeholder-blacklist, production startup now rejects secrets shorter
than 32 chars OR with Shannon entropy below 3.5 bits/char. That covers
low-entropy-but-long values like "aaaa..." (38 chars, entropy 0) which
would have passed the previous checks.
Documented the rotation process for AUTH_SECRET + POSTGRES_PASSWORD in
docs/security-architecture.md §3.
Verified:
- pnpm test:unit — 396 files / 1922 tests passed
- pnpm --filter @capakraken/web exec tsc --noEmit — clean
- pnpm --filter @capakraken/api exec tsc --noEmit — clean
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The useQuery type cast was using `as any` behind a blanket eslint-disable.
Using an explicit function-shape cast is both safer and removes the lint
error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BenchResourceCard, MobileProjectCard, MobileCapacityCard, DynamicFieldRenderer,
BudgetStatusBar, and TimelineHeader use no hooks, event handlers, or browser APIs —
they can be server components, reducing client bundle size.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- KeyboardShortcutOverlay: add role="dialog", aria-modal, aria-labelledby, close button aria-label
- Timeline popovers (5 files): add aria-label="Close" to symbol-only close buttons
- TimelineToolbar: add aria-label to navigation and undo/redo icon buttons
- ComputationGraphClient: add aria-pressed to 2D/3D and view mode toggle buttons
- BulkEditModal: fix type mismatch from jsonb field hardening
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- MfaPromptBanner: silently hide on query error (non-critical advisory banner)
- Step1Identity: show skeleton placeholders while blueprint list loads
- MobileSummaryClient: add error state with retry button for dashboard queries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SmtpSettingsPanel now owns its form state, save/test mutations, and feedback state
internally. Props reduced from 17 to 2 (initialSettings + onSettingsSaved callback).
Removes 7 useState declarations, 2 mutation definitions, and 1 handler from the parent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract overlay/popover JSX from TimelineView (1268→1037 lines) into TimelineDragOverlays and
TimelinePopovers. Extract ResourceMonthConfigSection from ReportBuilder (1132→1018 lines).
Extract ResourceSkillsEditor and ResourceOrgClassification from ResourceModal (1035→714 lines).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move renderOpenDemandRow, renderProjectUtilOverlay, and renderProjectDragHandles
(534 lines) to timelineProjectRenderers.tsx. TimelineProjectPanel: 1230 -> 687 lines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract ReportResultsPanel (293 lines) from ReportBuilder (1231→1044 lines)
and move 38 inline icon components from AppShell (937→833 lines) to nav-icons.tsx.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract each wizard step into its own file under project-wizard/:
StepBar, DynamicFieldInput, Step1Identity, ResourcePersonPicker,
Step2Timeline, Step3Staffing, Step4Suggestions, Step5Review.
Main file reduced from 1,385 to 112 lines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- useInvalidateTimeline and useInvalidatePlanningViews now return
Promise.all instead of fire-and-forget void calls
- Timeline mutations now use useInvalidatePlanningViews to also
invalidate allocation list views, preventing stale data
- AllocationsClient sequential awaits replaced with single
invalidatePlanningViews() call (parallel invalidation)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reduces unnecessary re-renders by separating the monolithic 20+ property
context into TimelineDataContext, TimelineViewContext, and
TimelineDisplayContext. Panel components now subscribe only to the
slices they need.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers: aria-sort/aria-labelledby attributes, non-Error throws in
ErrorBoundary, NaN/MAX_SAFE_INTEGER in formatCents, invalid dates,
carriage returns in CSV, self-closing HTML tags in sanitize, non-digit
input in DateInput, panel-click-not-dismissing in ConfirmDialog,
role="search" on FilterBar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move renderAllocBlocksFromData, renderLoadGraph, renderHeatmapOverlay,
renderDailyBars into timelineResourceRender.tsx (707 lines).
TimelineResourcePanel reduced from 1,270 to 589 lines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract types.ts, FilterDropdown.tsx, BooleanBadge.tsx from
ResourcesClient.tsx into resource-client/ subdirectory.
ResourcesClient reduced from 1,613 to 1,507 lines.
Fix TypeScript strict mode errors across 8 test files:
- Add id/order to BlueprintFieldDefinition test objects
- Use FieldType enum instead of string literals in useFilters
- Add non-null assertions for mock.calls array access
- Type ScrollDiv for jsdom scrollLeft workaround
- Fix exactOptionalPropertyTypes violations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
- ProjectHealthWidget: row already typed as ProjectHealthRow with id field
- ResourceDetail: use narrowed unknown cast instead of any for error code
- provider.tsx: same pattern for TRPCClientError data access
- ChatPanel: use intersection type for Next.js typed route push
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Install husky v9 + lint-staged: pre-commit runs eslint --fix and prettier on staged files
- Tighten ESLint base config: no-console→error, ban-ts-comment (ts-ignore banned, ts-expect-error with description allowed), reportUnusedDisableDirectives→error
- Migrate web app from deprecated `next lint` to `eslint src/` with flat config and react-hooks plugin
- Convert all 5 @ts-ignore to @ts-expect-error with descriptions, remove stale disable comments
- Add NEXT_PUBLIC_SENTRY_DSN to docker-compose.prod.yml and .env.example
- Add coverage artifact upload step to CI test job
- Pre-existing violations (102 warnings) downgraded to warn in web config for Phase 2 cleanup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Unify Saturday+Sunday into single isWeekend flag (header + grid lines)
- Replace hardcoded amber vacation bar/tooltip colors with brand-* classes
- Add global accent-color for checkboxes and radio buttons via CSS variable
- Update VACATION_TIMELINE_COLORS/BORDER to use brand palette (SICK stays red)
- Vacation-only tooltip uses neutral dark surface with brand accent border
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the colored background tint for 50-100% utilized rows entirely.
Only over-utilized rows (>100%) keep the red warning tint.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace hardcoded blue-shifted rgba values and slate-* classes with neutral
CSS variable references in timeline resource/project panels, tooltips,
constants, and heatmap mono palette. Change utilization row tint from blue
to green. Replace slate-950 open demand backgrounds with --surface-card.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The widget wrapper had a hardcoded dark gradient using rgba(22,23,26) and
rgba(16,17,19) which are blue-shifted. Replace with CSS variable references
--surface-elevated and --surface-card for neutral dark backgrounds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove duplicated Tailwind class string constants from 15 component files.
Use app-input, app-select, app-label, app-action-danger-btn, and
app-action-delete CSS component classes from globals.css instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add missing dark: class variants for backgrounds, borders, and text across
dashboard widgets, AppShell sidebar, notification cards, and the chargeability
report table. Replace hardcoded slate/gray hex values with CSS variable
references. Fix chargeability hover tint and remove ineffective sticky thead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace sidebar #0d0e22 hardcoded hex with .sidebar-panel class backed by
--surface-card CSS variable so all three sidebar elements (desktop, mobile,
mobile header) share the same neutral-dark color as the main content
- Remove purple logo gradient (dark:from-[#0d0e22] dark:to-[#13162a]) — now uses
--surface-elevated for a neutral, unified look
- Add .dark slate-*/gray-900 overrides: bg-slate-700/800/900, border-slate-800,
hover:bg-slate-800 all map to --surface-elevated/--surface-card/--border-subtle
- Remove dead hardcoded rgb(45 51 71) rule for dark bg-gray-100 (was overridden
further down anyway; now consistently uses --surface-elevated)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Runtime errors in components now show a friendly "Something went wrong"
screen instead of a white page. Timeline and staffing panel are
individually wrapped. Route-level error.tsx handles server component errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>