Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
📜 Recent review details⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
🧰 Additional context used📓 Path-based instructions (1)web/src/**/*.{ts,tsx}📄 CodeRabbit inference engine (CLAUDE.md)
Files:
🧠 Learnings (1)📓 Common learnings🔇 Additional comments (6)
WalkthroughAdds Playwright visual tests, fixtures, and a Playwright config plus related npm scripts and test output paths. Adds Lighthouse CI config and 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
Dependency ReviewThe following issues were found:
|
There was a problem hiding this comment.
Code Review
This pull request implements a robust responsive layout system, including a tablet-specific overlay sidebar and a mobile-unsupported overlay, while introducing comprehensive testing infrastructure with Playwright and Lighthouse. It adds several utility hooks for auto-scrolling, breakpoint detection, and accessibility, alongside production bundle splitting in Vite. Feedback focuses on maintaining consistency with the new FOCUS_RING standard in the task creation dialog and completing the configuration for the bundle analysis plugin.
| const INPUT_CLASSES = 'w-full h-8 rounded-md border border-border bg-surface px-2 text-[13px] text-foreground outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 focus:ring-offset-background' | ||
| const TEXTAREA_CLASSES = 'w-full rounded-md border border-border bg-surface px-2 py-1.5 text-[13px] text-foreground outline-none resize-y focus:ring-2 focus:ring-accent focus:ring-offset-2 focus:ring-offset-background' |
There was a problem hiding this comment.
For consistency with the new FOCUS_RING standard and for better accessibility, it's recommended to use focus-visible: instead of focus:. This avoids showing focus rings on mouse clicks for a better user experience.
While you're at it, consider using the FOCUS_RING constant from lib/utils.ts for better maintainability, which would require importing it.
| const INPUT_CLASSES = 'w-full h-8 rounded-md border border-border bg-surface px-2 text-[13px] text-foreground outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 focus:ring-offset-background' | |
| const TEXTAREA_CLASSES = 'w-full rounded-md border border-border bg-surface px-2 py-1.5 text-[13px] text-foreground outline-none resize-y focus:ring-2 focus:ring-accent focus:ring-offset-2 focus:ring-offset-background' | |
| const INPUT_CLASSES = 'w-full h-8 rounded-md border border-border bg-surface px-2 text-[13px] text-foreground outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background' | |
| const TEXTAREA_CLASSES = 'w-full rounded-md border border-border bg-surface px-2 py-1.5 text-[13px] text-foreground outline-none resize-y focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background' |
web/vite.config.ts
Outdated
| } | ||
|
|
||
| export default defineConfig({ | ||
| plugins: [react(), tailwindcss()], |
There was a problem hiding this comment.
The package.json adds an analyze script and the rollup-plugin-visualizer dependency, but the plugin isn't configured here. To enable the bundle analysis as described in CLAUDE.md, you need to import visualizer and add it to the plugins array, conditionally based on the VITE_ANALYZE environment variable.
First, add this import at the top of the file:
import { visualizer } from 'rollup-plugin-visualizer'Then, update the plugins array:
plugins: [
react(),
tailwindcss(),
process.env.VITE_ANALYZE && visualizer({ open: true, filename: 'stats.html' }),
],There was a problem hiding this comment.
Actionable comments posted: 30
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
web/src/pages/budget/PeriodSelector.tsx (1)
16-38:⚠️ Potential issue | 🟠 MajorThis radiogroup still needs real keyboard navigation.
With
role="radiogroup"/role="radio", ArrowLeft/ArrowRight and roving tabindex are expected. Right now every option stays in the Tab order, so this control still misses the keyboard behavior the PR is aiming for. Please replace it with the sharedSegmentedControlor wireuseRovingTabIndexinto these buttons.As per coding guidelines,
web/src/**/*.{ts,tsx}should “ALWAYS reuse existing components fromweb/src/components/ui/before creating new ones” and reuseSegmentedControl.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/budget/PeriodSelector.tsx` around lines 16 - 38, Replace the custom radiogroup buttons with the shared SegmentedControl component to get correct keyboard behaviour: import SegmentedControl from web/src/components/ui/, pass the current value and onChange, map PERIODS to SegmentedControl.Item (or the component's item API) using period.value as key and period.label as content, and move any styling (e.g. FOCUS_RING, cn-based classes) into the SegmentedControl/className props; remove the manual role="radiogroup"/role="radio" and per-button tabindex logic since SegmentedControl provides proper ArrowLeft/ArrowRight navigation and roving tabindex out of the box.web/src/pages/budget/CostBreakdownChart.tsx (1)
71-99:⚠️ Potential issue | 🟠 MajorThis segmented control should be shared, not reimplemented.
Like
PeriodSelector, this bespoke radiogroup still has no roving tabindex or arrow-key handling, so it isn't keyboard-correct. Please switch it to the sharedSegmentedControlor the new roving-tab hook instead of maintaining another custom button group.As per coding guidelines,
web/src/**/*.{ts,tsx}should “ALWAYS reuse existing components fromweb/src/components/ui/before creating new ones” and reuseSegmentedControl.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/budget/CostBreakdownChart.tsx` around lines 71 - 99, The custom radiogroup (using DIMENSION_OPTIONS, dimension, onDimensionChange and deptDisabled) lacks proper roving-tab/arrow-key handling and should be replaced with the shared SegmentedControl from web/src/components/ui; switch the <div>+<button> map to a SegmentedControl, pass the current value and onChange (use onDimensionChange), map DIMENSION_OPTIONS to its items/labels, mark the 'department' option disabled when deptDisabled, and preserve visual classes like FOCUS_RING and cn for styling and accessible aria-label. Ensure the SegmentedControl receives the same aria-label ("Breakdown dimension") and that disabled state and active styling mirror the previous behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/e2e/fixtures/mock-api.ts`:
- Around line 57-60: The catch-all Playwright route handler (page.route)
currently fulfills requests with an incompatible body ({ data: [], total: 0 });
update the route.fulfill payload to match ApiResponse/ PaginatedResponse
envelopes by returning at minimum { data: [], error: null, error_detail: null,
success: true } for non-paginated endpoints and include a pagination object {
total: 0, offset: 0, limit: 10 } for paginated endpoints so tests receive the
expected fields; adjust the single catch-all in mock-api.ts (the page.route
callback that calls route.fulfill) to send the proper envelope based on the
request path or always return the full paginated envelope if that covers test
expectations.
In `@web/e2e/visual/dashboard.spec.ts`:
- Around line 9-15: The test's page.addInitScript sets auth_token and auth_user
but omits auth_token_expires_at which the auth store (init logic reading
auth_token and auth_token_expires_at) requires — add an auth_token_expires_at
key with a future timestamp so the token isn't cleared; update the addInitScript
block in dashboard.spec.ts (the page.addInitScript callback) to set
localStorage.setItem('auth_token_expires_at', new Date(Date.now() +
60*60*1000).toISOString()) and remove or ignore setting auth_user since the auth
store doesn't read it.
In `@web/package.json`:
- Line 14: The "analyze" npm script in package.json sets VITE_ANALYZE using
Unix-style assignment which breaks on Windows; add cross-env to devDependencies
and update the "analyze" script to use cross-env (referencing the "analyze"
script name in package.json) so the environment variable is set cross-platform,
then run npm install --save-dev cross-env to ensure it works on all platforms.
In `@web/playwright.config.ts`:
- Around line 49-53: Playwright's webServer is binding to port 4173 implicitly
(webServer.port) which can lead to cryptic failures if Vite's default or that
port changes; to fix, explicitly declare the preview port and strictPort in your
Vite config by adding a preview block (e.g., preview.port = 4173 and
preview.strictPort = true) in the project’s defineConfig export (vite.config.ts)
so the server fails fast when the port is unavailable and matches Playwright's
webServer.port.
In `@web/src/__tests__/hooks/useAutoScroll.test.ts`:
- Around line 23-30: The test currently asserts that scrollTop equals
scrollHeight but omits verifying that calling scrollToBottom() re-enables
auto-scroll; update the test for useAutoScroll to simulate a user scroll away
(e.g., create container with non-zero scrollTop and dispatch a 'scroll' event),
assert result.current.isAutoScrolling is false, then call
result.current.scrollToBottom() and assert result.current.isAutoScrolling is
true; reference the hook/useAutoScroll, its scrollToBottom method and
isAutoScrolling property (and helper makeContainer) when adding the assertion
and the pre-scroll simulation.
In `@web/src/__tests__/hooks/useBreakpoint.test.ts`:
- Around line 105-111: The test name is misleading: it either should be renamed
to reflect that it asserts the case where all media queries match (e.g.,
"returns desktop when all breakpoints match") when calling mockMatchMedia(true,
true, true) and rendering useBreakpoint via renderHook, or it should be changed
to exercise the SSR fallback by stubbing global window to undefined (use
vi.stubGlobal('window', undefined)) before rendering useBreakpoint and
remove/adjust mockMatchMedia; update the test title and assertions accordingly
so the name matches the actual behavior of useBreakpoint.
In `@web/src/__tests__/hooks/useCountAnimation.test.ts`:
- Around line 89-92: The test name is misleading: in useCountAnimation.test the
spec labeled "handles string values by parsing and formatting" actually passes a
number (42) and only asserts the return type; update the test to make behavior
and name consistent — either rename the it() description to something like
"returns a number when initialized with numeric value" (referencing the test
using useCountAnimation and result.current) or change the input to a string
(e.g., useCountAnimation('42')) and add assertions that the hook
parses/formatting occurs (assert parsed numeric value and any formatted output
the hook provides); modify the test body accordingly around renderHook(() =>
useCountAnimation(...)) and expect(...) to reflect the chosen fix.
- Around line 15-17: The cancelAnimationFrame mock currently calls
clearTimeout(id) but the requestAnimationFrame mock returns a different rafId,
so clearTimeout doesn't cancel the scheduled callback; update the mocks so the
id returned by window.requestAnimationFrame is the actual timeout handle used by
setTimeout (or keep a map of rafId->timeoutHandle) and make
window.cancelAnimationFrame clear the corresponding timeout; adjust the vi.spyOn
mocks for requestAnimationFrame and cancelAnimationFrame to return and clear the
same handle (or use an internal map) so cancelling actually prevents the
callback from running.
In `@web/src/__tests__/integration/ws-dashboard.test.tsx`:
- Around line 43-55: The test currently only checks activities.length; update it
to assert the actual activity content produced by
useAnalyticsStore.getState().updateFromWsEvent(event): retrieve activities from
useAnalyticsStore.getState().activities and find the activity corresponding to
task-99, then assert its fields (e.g., event_type === 'task.created', channel
=== 'tasks', payload.task_id === 'task-99', payload.title === 'New integration
task') and that the timestamp is present/parsable; use equality or partial
matching on those properties instead of only checking length.
- Around line 77-79: Replace the hardcoded "50" assertion with the shared
constant to avoid a magic number: import and use MAX_ACTIVITIES (the cap defined
in analytics.ts) in the test assertion that reads
useAnalyticsStore.getState().activities and assert activities.length <=
MAX_ACTIVITIES; if MAX_ACTIVITIES is not exported, either export it from
analytics.ts or add a short explanatory comment next to the expect line
referencing the analytics.ts cap to make the coupling explicit.
In `@web/src/__tests__/integration/ws-tasks.test.tsx`:
- Around line 58-86: The test uses a module-scoped Set named pendingTransitions
(defined in tasks.ts) which can leak state between tests if a failing test never
calls rollback(); ensure tests always clean up by either adding an afterEach
that clears pendingTransitions via a new helper (expose
clearPendingTransitions() on the useTasksStore or export a test-only
clearPendingTransitions function) or by calling that exposed clear method in the
test teardown; update tests (ws-tasks.test.tsx) to call the new
clearPendingTransitions() in an afterEach to guarantee isolation and reference
the existing optimisticTransition/rollback() flow
(useTasksStore.getState().optimisticTransition and rollback()) when locating
where to clear.
In `@web/src/components/layout/Sidebar.tsx`:
- Around line 146-156: Wrap calls to openCommandPalette so the tablet drawer's
FocusScope is unmounted first: when invoking openCommandPalette from
SidebarFooter (and the other call sites around the same component), first close
the drawer/overlay (via the existing toggleCollapse or the drawer-close helper
used by the Sidebar) and wait for the drawer to fully close/unmount (or
synchronously ensure the FocusScope is torn down) before calling
openCommandPalette; update the prop passed to SidebarFooter (openCommandPalette)
to be this wrapper function so the drawer trap cannot remain active when the
command palette opens.
- Around line 300-339: Replace the inline colored dot and text in Sidebar with
the shared StatusBadge component and add a polite live region for announcements:
determine status from wsConnected and wsReconnectExhausted, render <StatusBadge
...> (instead of the size-2 span) with the same title/aria-label logic, render
the textual label only when not collapsed, and add a visually-hidden element
(e.g., sr-only span) with aria-live="polite" (or role="status") that outputs the
same status string so changes are announced to screen readers; remove the inline
dot markup and ensure StatusBadge and the live-region use the
wsConnected/wsReconnectExhausted state values.
- Around line 119-159: The overlay/backdrop/focus logic in Sidebar is duplicated
instead of reusing the shared Drawer UI; replace the manual backdrop, aside and
FocusScope block with the shared Drawer component (imported from
web/src/components/ui/Drawer) and move the existing children into Drawer: pass
onOverlayClose as the Drawer close handler, set Drawer open state to the same
condition that renders this overlay, and render <SidebarNav collapsed={false} />
and <SidebarFooter ... /> as Drawer content so focus trapping and backdrop
behavior come from Drawer rather than the custom aside/FocusScope; ensure you
keep the close button handler (onOverlayClose), aria-labels, and props passed to
SidebarFooter (toggleCollapse, openCommandPalette, shortcutKey, wsConnected,
wsReconnectExhausted, user, logout).
In `@web/src/components/layout/StatusBar.tsx`:
- Around line 146-159: The WebSocket status span lacks an aria-live attribute;
update the span inside the StatusItem that renders the
wsConnected/wsReconnectExhausted text to include aria-live="polite" so screen
readers announce status changes (locate the StatusItem containing Dot and the
span that uses wsConnected and wsReconnectExhausted and add the attribute to
that span).
In `@web/src/components/ui/agent-card.tsx`:
- Around line 13-15: AgentCardProps is currently declared but not exported;
export the interface so consumers can import the prop types — update the
AgentCardProps declaration to "export interface AgentCardProps" and ensure any
files using these props (stories, tests, consumers) import { AgentCardProps }
from the component module; keep the AgentCard component and its flashStyle prop
unchanged, only change the interface declaration visibility.
In `@web/src/components/ui/empty-state.tsx`:
- Line 37: The EmptyState component currently always sets role="status" causing
unwanted screen-reader announcements; add an optional boolean prop announce
(default false) to the EmptyState props and change the JSX to only include
role="status" (and related aria-live attributes if present) when announce is
true, update the component's TypeScript prop type/interface (e.g.,
EmptyStateProps) and any default prop assignment, and audit usages to opt-in
only where live-region announcements are desired.
In `@web/src/components/ui/live-region.tsx`:
- Around line 3-10: The LiveRegionProps interface is currently unexported;
export it so external consumers (stories, tests, wrappers) can import the prop
contract directly. Update the declaration of LiveRegionProps to be exported
(export interface LiveRegionProps { ... }) and ensure any local references
(e.g., component props typing for the LiveRegion component) continue to use the
exported symbol.
In `@web/src/components/ui/sparkline.tsx`:
- Around line 11-12: The SparklineProps interface is currently defined but not
exported; update the declaration of SparklineProps to be exported (export
interface SparklineProps { ... }) so downstream code, stories and docs can
import and reuse the type; ensure any existing references to SparklineProps in
the same file (e.g., the Sparkline component signature) continue to compile
after making the interface exported.
- Around line 64-67: The Sparkline component currently falls back to aria-hidden
when ariaLabel is omitted, hiding meaningful charts from assistive tech; change
the prop typing to a discriminated union (e.g., make props either { decorative:
true; ariaLabel?: never } or { decorative?: false; ariaLabel: string }) in the
SparklineProps/interface so callers must provide ariaLabel unless explicitly
decorative, and update the JSX that spreads ARIA attributes (the existing
conditional using ariaLabel) to use the new decorative flag (render role="img"
and aria-label when ariaLabel is present, otherwise render aria-hidden when
decorative is true) so omissions are caught at compile time (refer to the
Sparkline component and SparklineProps type).
In `@web/src/components/ui/status-badge.tsx`:
- Around line 40-58: The animated branch in StatusBadge (the motion.span when
animated is true) omits the base color class from DOT_COLOR_CLASSES[color], so
initial paint can be transparent; update the className on the motion.span inside
the StatusBadge component to include DOT_COLOR_CLASSES[color] (alongside
'size-1.5 shrink-0 rounded-full' and pulse && 'animate-pulse') and keep
spreading motionProps as before so the animated background overlays the same
base color.
In `@web/src/components/ui/task-status-indicator.tsx`:
- Line 32: TaskStatusIndicator currently always sets role="status" (and should
also set aria-live="polite") which causes excessive screen-reader announcements;
add an optional announce boolean prop (default false) to the TaskStatusIndicator
props and only include role="status" and aria-live="polite" on the rendered
element when announce is true. Update the component signature/type to include
announce?: boolean = false, use conditional attributes (e.g., announce && {
role: "status", "aria-live": "polite" }) or conditional JSX props so existing
callers are unaffected, and ensure any places that rely on announcements pass
announce={true} where appropriate.
In `@web/src/hooks/useAutoScroll.ts`:
- Around line 26-43: The effect currently captures containerRef.current once and
may leave a listener on a stale element if the ref changes; update the useEffect
to read and bind to the current element each run and ensure cleanup removes the
listener from that same element by: inside the effect assign const current =
containerRef?.current; if (!current) return; add the listener to current via
current.addEventListener('scroll', onScroll, { passive: true }) and in the
cleanup call current.removeEventListener('scroll', onScroll); also include the
element identity in the dependency list (e.g. containerRef.current or a derived
stable refVal) along with thresholdPx so the effect re-runs when the container
element changes; keep onScroll, isAutoScrollingRef and setIsAutoScrolling logic
unchanged.
In `@web/src/hooks/useCountAnimation.ts`:
- Around line 22-25: When starting a new RAF tween in useCountAnimation, don't
use previousValueRef.current as the tween "from" value because that can differ
from what's actually rendered; instead compute the start from the current
displayed value (parse Number(display) or use display state) and cancel any
pending animationFrameRef before restarting. Update previousValueRef at the end
of the animation (or when target settles) rather than as the immediate source of
the next tween so the next tween begins from the onscreen number; ensure the
same change is applied to the code paths around the other tween starts
referenced (the blocks around the current target-change handling and the main
RAF loop).
- Line 21: Clamp and validate the public durationMs input in useCountAnimation:
ensure durationMs is coerced to a finite positive number (e.g., check
Number.isFinite and > 0) and fall back to DEFAULT_DURATION_MS if invalid, and
similarly guard any progress computations so the denominator is never zero/NaN;
update the declaration of durationMs and the progress/display calculations
(where progress is computed in the RAF loop) to use the validated/clamped value
so the RAF loop can terminate and display cannot overshoot or become NaN.
In `@web/src/hooks/useRovingTabIndex.ts`:
- Around line 59-90: The arrow-key handler in useRovingTabIndex derives
nextIndex from the stored focusedIndex which can be stale; update the handler to
compute the origin index from the actual DOM focus (e.g., const origin =
items.findIndex(el => el === document.activeElement) and fall back to
focusedIndex if origin === -1) and use that origin when calculating nextIndex,
then continue to clamp/loop and call setFocusedIndex and
items[nextIndex]?.focus(); reference the handler, focusedIndex, items,
setFocusedIndex and nextIndex when making the change.
In `@web/src/pages/dashboard/ActivityFeed.tsx`:
- Around line 22-28: The auto-scroll check uses visible.length which is capped
by MAX_VISIBLE so it never increases after the cap; change the effect to compare
the total activities count (e.g., activities.length or totalActivitiesLength)
against prevCountRef.current instead of visible.length, update
prevCountRef.current to that total count, and keep isAutoScrolling and
scrollToBottom in the deps so scrollToBottom is called when new activities
arrive even when visible is capped; reference the variables visible,
prevCountRef, isAutoScrolling, scrollToBottom, and MAX_VISIBLE when making the
change.
In `@web/src/pages/TaskDetailPage.tsx`:
- Around line 35-42: TaskDetailPage currently calls useWebSocket(...) and
ignores its return value, so setup failures are swallowed; change the call to
capture the returned state (e.g., const { connected, setupError } =
useWebSocket({ bindings: wsBindings })) and pass connected where needed, then
render the same warning banner pattern used on other pages when setupError is
non-null (show a dismissed/inline warning near the top of the TaskDetailPage
component) so users see WebSocket setup errors; locate the useMemo that defines
wsBindings and the useWebSocket call in TaskDetailPage and update them
accordingly, and use the same banner/component style as
OrgEditPage/TaskBoardPage for consistency.
In `@web/src/pages/tasks/TaskCreateDialog.tsx`:
- Around line 271-272: INPUT_CLASSES and TEXTAREA_CLASSES duplicate the same
focus-ring utility string; extract that shared string into a single constant
(e.g., FOCUS_RING_CLASSES or FOCUS_RING_TOKEN) and compose INPUT_CLASSES and
TEXTAREA_CLASSES by concatenating the original base class strings with that
shared token so both use the same focus-ring source and avoid duplication/drift.
In `@web/vite.config.ts`:
- Around line 45-51: The Vite config currently never uses VITE_ANALYZE so the
rollup visualizer never runs; update web/vite.config.ts to conditionally import
and add the visualizer plugin when process.env.VITE_ANALYZE (or
import.meta.env.VITE_ANALYZE) is truthy: dynamically import
'rollup-plugin-visualizer' (e.g., const { visualizer } = await
import('rollup-plugin-visualizer')) and push visualizer({ filename:
'stats.html', open: true }) into the plugins array in the exported config
(alongside existing plugins and build.rollupOptions.manualChunks) only when the
env flag is set. Ensure the import is dynamic to avoid requiring the dependency
in non-analyze runs and use the same env variable name used by the analyze
script.
---
Outside diff comments:
In `@web/src/pages/budget/CostBreakdownChart.tsx`:
- Around line 71-99: The custom radiogroup (using DIMENSION_OPTIONS, dimension,
onDimensionChange and deptDisabled) lacks proper roving-tab/arrow-key handling
and should be replaced with the shared SegmentedControl from
web/src/components/ui; switch the <div>+<button> map to a SegmentedControl, pass
the current value and onChange (use onDimensionChange), map DIMENSION_OPTIONS to
its items/labels, mark the 'department' option disabled when deptDisabled, and
preserve visual classes like FOCUS_RING and cn for styling and accessible
aria-label. Ensure the SegmentedControl receives the same aria-label ("Breakdown
dimension") and that disabled state and active styling mirror the previous
behavior.
In `@web/src/pages/budget/PeriodSelector.tsx`:
- Around line 16-38: Replace the custom radiogroup buttons with the shared
SegmentedControl component to get correct keyboard behaviour: import
SegmentedControl from web/src/components/ui/, pass the current value and
onChange, map PERIODS to SegmentedControl.Item (or the component's item API)
using period.value as key and period.label as content, and move any styling
(e.g. FOCUS_RING, cn-based classes) into the SegmentedControl/className props;
remove the manual role="radiogroup"/role="radio" and per-button tabindex logic
since SegmentedControl provides proper ArrowLeft/ArrowRight navigation and
roving tabindex out of the box.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: fb8468df-afac-4023-aafd-acbc58fd8bb9
⛔ Files ignored due to path filters (1)
web/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (60)
.gitignoreCLAUDE.mddocs/design/page-structure.mddocs/design/ux-guidelines.mdweb/e2e/fixtures/mock-api.tsweb/e2e/visual/dashboard.spec.tsweb/e2e/visual/login.spec.tsweb/lighthouserc.jsweb/package.jsonweb/playwright.config.tsweb/src/__tests__/hooks/useAutoScroll.test.tsweb/src/__tests__/hooks/useBreakpoint.test.tsweb/src/__tests__/hooks/useCountAnimation.test.tsweb/src/__tests__/hooks/useJustUpdated.test.tsweb/src/__tests__/hooks/useRovingTabIndex.test.tsweb/src/__tests__/integration/ws-dashboard.test.tsxweb/src/__tests__/integration/ws-tasks.test.tsxweb/src/__tests__/pages/DashboardPage.test.tsxweb/src/components/layout/AppLayout.tsxweb/src/components/layout/Sidebar.tsxweb/src/components/layout/SidebarNavItem.tsxweb/src/components/layout/StatusBar.tsxweb/src/components/ui/agent-card.tsxweb/src/components/ui/command-palette.tsxweb/src/components/ui/empty-state.tsxweb/src/components/ui/inline-edit.tsxweb/src/components/ui/lazy-code-mirror-editor.stories.tsxweb/src/components/ui/lazy-code-mirror-editor.tsxweb/src/components/ui/live-region.stories.tsxweb/src/components/ui/live-region.tsxweb/src/components/ui/metric-card.tsxweb/src/components/ui/mobile-unsupported.stories.tsxweb/src/components/ui/mobile-unsupported.tsxweb/src/components/ui/section-card.tsxweb/src/components/ui/sparkline.tsxweb/src/components/ui/status-badge.tsxweb/src/components/ui/task-status-indicator.tsxweb/src/components/ui/toggle-field.tsxweb/src/hooks/useAutoScroll.tsweb/src/hooks/useBreakpoint.tsweb/src/hooks/useCountAnimation.tsweb/src/hooks/useJustUpdated.tsweb/src/hooks/useRovingTabIndex.tsweb/src/lib/utils.tsweb/src/pages/DashboardPage.tsxweb/src/pages/OrgChartPage.tsxweb/src/pages/TaskDetailPage.tsxweb/src/pages/budget/AgentSpendingTable.tsxweb/src/pages/budget/CostBreakdownChart.tsxweb/src/pages/budget/PeriodSelector.tsxweb/src/pages/budget/SpendBurnChart.tsxweb/src/pages/dashboard/ActivityFeed.tsxweb/src/pages/dashboard/BudgetBurnChart.tsxweb/src/pages/setup/WizardProgress.tsxweb/src/pages/tasks/TaskCard.tsxweb/src/pages/tasks/TaskCreateDialog.tsxweb/src/pages/tasks/TaskDependencyGraph.tsxweb/src/pages/tasks/TaskListView.tsxweb/vite.config.tsweb/vitest.config.ts
a81971c to
4151935
Compare
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
web/src/components/ui/status-badge.tsx (1)
20-27: 🛠️ Refactor suggestion | 🟠 MajorExport the shared component props interface.
StatusBadgeis a reusable UI primitive and its prop contract changed in this PR. ExportStatusBadgePropsso stories, tests, and consumers can reuse the type instead of re-declaring it.♻️ Proposed fix
-interface StatusBadgeProps { +export interface StatusBadgeProps {As per coding guidelines:
web/src/components/ui/**/*.{ts,tsx}— “Export props as TypeScript interface.”🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/components/ui/status-badge.tsx` around lines 20 - 27, The StatusBadgeProps interface is currently internal but should be exported for reuse; update the interface declaration for StatusBadgeProps (used by the StatusBadge component) to be exported so stories, tests, and other consumers can import the type instead of re-declaring it. Ensure the exported name remains StatusBadgeProps and keep all existing fields (status, label, pulse, className, animated) unchanged to preserve the component contract.web/src/pages/tasks/TaskListView.tsx (1)
79-96: 🧹 Nitpick | 🔵 TrivialHeader sort buttons missing FOCUS_RING for consistency.
The sortable column header buttons lack focus indicators, unlike
AgentSpendingTable.tsx'sColumnHeaderwhich appliesFOCUS_RING. For consistent keyboard accessibility, applyFOCUS_RINGhere as well.♻️ Proposed fix
<button key={col.key} type="button" onClick={() => col.sortable && handleSort(col.key)} className={cn( 'flex items-center gap-1 text-[11px] font-semibold uppercase tracking-wider text-text-muted transition-colors', + FOCUS_RING, col.sortable && 'cursor-pointer hover:text-foreground', col.width, )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/tasks/TaskListView.tsx` around lines 79 - 96, The sortable column header buttons (rendered in the mapping over col) are missing the shared focus-ring utility used elsewhere; update the button className expression in TaskListView's header rendering so that when col.sortable is true it includes the FOCUS_RING class (alongside the existing 'cursor-pointer hover:text-foreground'), e.g. add FOCUS_RING to the cn(...) call for the button that uses col.key, handleSort, sortKey and sortDir so the ArrowUp/ArrowDown icons remain unchanged but keyboard focus shows the same ring as AgentSpendingTable's ColumnHeader.web/src/components/layout/SidebarNavItem.tsx (1)
42-54: 🧹 Nitpick | 🔵 TrivialAccessibility improvement looks good, with a minor consideration.
The
aria-live="polite"and descriptivearia-labelimprove screen reader support. However, placingaria-liveon a conditionally rendered element means removals (badge going to 0) won't be announced. If that's important, consider movingaria-liveto a parent element that persists, or using aLiveRegioncomponent if one exists in this PR.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/components/layout/SidebarNavItem.tsx` around lines 42 - 54, The badge element in SidebarNavItem currently applies aria-live="polite" to a conditionally rendered span so removals (badge -> 0) won't be announced; update the component to place the live region on a persistent parent (e.g., the wrapping element in SidebarNavItem) or use the project's LiveRegion component if available, and move the aria-live and aria-label logic there (use the badge and label props to construct the message and still render the visual badge as-is) so both additions and removals of the badge are reliably announced.web/src/components/ui/inline-edit.tsx (1)
128-142: 🧹 Nitpick | 🔵 TrivialMissing
FOCUS_RINGon the display button.The display-mode button lacks focus styling while the edit-mode input has
FOCUS_RING. For consistent keyboard navigation experience, applyFOCUS_RINGto the display button as well.♻️ Proposed fix
<button type="button" onClick={startEditing} disabled={disabled} data-inline-display="" aria-label={`Edit: ${value || placeholder || 'empty'}`} className={cn( - 'cursor-pointer border-b border-dashed border-transparent text-left transition-colors', + 'cursor-pointer border-b border-dashed border-transparent text-left transition-colors rounded-sm', + FOCUS_RING, !disabled && 'hover:border-border-bright', disabled && 'cursor-default opacity-60', flashClassName, )} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/components/ui/inline-edit.tsx` around lines 128 - 142, The display-mode button in inline-edit.tsx is missing the shared focus styling constant FOCUS_RING; update the button's className (the one using cn(...) where startEditing is wired) to include FOCUS_RING (only when not disabled) alongside the existing hover and disabled classes so the display button matches the edit-mode input's focus ring for keyboard users; reference the button that uses props/locals startEditing, renderDisplay, value, placeholder, disabled, flashClassName and the FOCUS_RING constant when making the change.
♻️ Duplicate comments (3)
web/src/hooks/useAutoScroll.ts (1)
26-43:⚠️ Potential issue | 🟡 MinorScroll listener can drift to a stale element if
containerRef.currentchanges.The handler is attached to one element but reads from
containerRef.current, which may later point elsewhere.🔧 Proposed stabilization by binding to a single element identity
+ const currentEl = containerRef?.current + // Track scroll position to determine if user has scrolled away useEffect(() => { - const el = containerRef?.current - if (!el) return + if (!currentEl) return function onScroll() { - const container = containerRef?.current - if (!container) return const nearBottom = - container.scrollTop >= container.scrollHeight - container.clientHeight - thresholdPx + currentEl.scrollTop >= currentEl.scrollHeight - currentEl.clientHeight - thresholdPx if (nearBottom !== isAutoScrollingRef.current) { isAutoScrollingRef.current = nearBottom setIsAutoScrolling(nearBottom) } } - el.addEventListener('scroll', onScroll, { passive: true }) - return () => el.removeEventListener('scroll', onScroll) - }, [containerRef, thresholdPx]) + currentEl.addEventListener('scroll', onScroll, { passive: true }) + return () => currentEl.removeEventListener('scroll', onScroll) + }, [currentEl, thresholdPx])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/hooks/useAutoScroll.ts` around lines 26 - 43, The scroll handler currently reads containerRef.current inside onScroll while the listener is attached to the original element, which can drift if containerRef.current changes; to fix, capture the current element into a local const (el) when setting up the effect and use that captured element inside onScroll (referencing el instead of containerRef.current), attach the listener to that same captured el, and ensure the cleanup removes the listener from that same captured el; update references to isAutoScrollingRef and setIsAutoScrolling/thresholdPx as needed inside the handler so they operate on the bound element identity in useEffect.web/src/__tests__/hooks/useAutoScroll.test.ts (1)
23-45:⚠️ Potential issue | 🟡 Minor
scrollToBottom()re-enable behavior is still unasserted.The suite verifies
scrollTopupdates, but not that auto-scroll state is restored after the user scrolls away.🧪 Proposed additional assertion path
+ it('scrollToBottom re-enables auto-scrolling after user scrolls away', () => { + const container = makeContainer({ scrollTop: 100 }) + const ref = { current: container } + const { result } = renderHook(() => useAutoScroll(ref)) + + act(() => { + container.dispatchEvent(new Event('scroll')) + }) + expect(result.current.isAutoScrolling).toBe(false) + + act(() => { + result.current.scrollToBottom() + }) + expect(result.current.isAutoScrolling).toBe(true) + })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/__tests__/hooks/useAutoScroll.test.ts` around lines 23 - 45, The test misses asserting that invoking scrollToBottom re-enables auto-scroll after the user scrolled away; update the second test for useAutoScroll to, after dispatching the scroll event and asserting isAutoScrolling is false, call result.current.scrollToBottom() (inside act if needed) and then assert result.current.isAutoScrolling is true and container.scrollTop equals container.scrollHeight to confirm scrollToBottom restored auto-scrolling; reference the useAutoScroll hook, the scrollToBottom method, and the isAutoScrolling property when adding the assertions.web/src/components/layout/Sidebar.tsx (1)
117-162: 🛠️ Refactor suggestion | 🟠 MajorTablet overlay still uses custom backdrop/dialog instead of shared
Drawer.The tablet overlay mode implements its own backdrop, aside with
role="dialog", andFocusScope. The design system already provides aDrawercomponent with these behaviors centralized.Consider refactoring to use
<Drawer>for consistency and reduced maintenance burden. The current implementation duplicates modal semantics thatDraweralready handles.As per coding guidelines, "ALWAYS reuse existing components from
web/src/components/ui/before creating new ones. Reuse ... Drawer ...".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/components/layout/Sidebar.tsx` around lines 117 - 162, Replace the custom tablet overlay block (the if (isOverlayMode) branch that checks overlayOpen and renders the backdrop div, aside role="dialog", FocusScope, SidebarNav and SidebarFooter) with the shared Drawer component from web/src/components/ui/, letting Drawer handle backdrop, focus trap and dialog semantics; render <SidebarNav collapsed={false} /> and <SidebarFooter ... /> as Drawer children, wire Drawer open state to overlayOpen and close to onOverlayClose (or Drawer onOpenChange), and keep behavior for toggleCollapse, openCommandPalette (call onOverlayClose?.() before openCommandPalette), shortcutKey, wsConnected, wsReconnectExhausted, user and logout so existing props are forwarded unchanged. Ensure you remove the backdrop div, the aside element with role="dialog" and the FocusScope usage when switching to Drawer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/lighthouserc.js`:
- Around line 14-18: The current lighthouserc.js assertions under the assert
object use 'warn' severity for 'categories:performance',
'categories:accessibility', and 'categories:best-practices', which does not fail
the run when scores drop below 0.9; change the severity from 'warn' to 'error'
for those assertion entries so the 0.9 minScore is enforced as a failing
condition (update the assertions for 'categories:performance',
'categories:accessibility', and 'categories:best-practices' accordingly) and
re-run the Lighthouse check to verify the enforcement.
- Line 8: The configured Lighthouse test URL currently points at '/login' which
won't exercise the dashboard lazy bundles or WebSocket-driven views; update the
url array in lighthouserc.js (the url property) to include a route that actually
loads the new dashboard code (for example the dashboard entry route used by the
app such as '/dashboard' or the main app shell) so the Lighthouse run will load
CodeMirror, WebSocket layout, and task view bundles introduced in this PR.
In `@web/src/__tests__/hooks/useBreakpoint.test.ts`:
- Around line 105-109: Remove the duplicate test case named "returns desktop
when all breakpoints match" in useBreakpoint.test.ts: locate the redundant
it(...) block that calls mockMatchMedia(true, true, true) and renderHook(() =>
useBreakpoint()) and delete that entire test (the original test at lines ~58-66
should remain), ensuring no other tests or helper calls (mockMatchMedia,
renderHook, useBreakpoint) are altered.
In `@web/src/__tests__/hooks/useCountAnimation.test.ts`:
- Around line 73-81: The mid-animation assertion is too permissive because it
allows result.current === 100 which won't catch instant-jump regressions; update
the assertions in useCountAnimation.test.ts (the block that advances timers by
100ms and checks result.current) to assert the value is strictly between the
start and target (e.g., use toBeGreaterThan(0) and toBeLessThan(100) or
equivalent) so the test fails if the animation jumps immediately to the target.
In `@web/src/components/ui/command-palette.tsx`:
- Line 155: The component currently hardcodes responsive width with
"max-[1023px]:max-w-[calc(100vw-2rem)]"; update the wrapper in CommandPalette
(the element using that class string) to remove the calc and instead use design
token-based spacing and responsive width (e.g., use max-w-full or the tokenized
max-width plus the layout padding token such as the app's horizontal padding
token) so the component relies on layout padding tokens rather than hardcoded
calc values; ensure you replace the "max-[1023px]:max-w-[calc(100vw-2rem)]"
fragment with the appropriate token-based classes from the design system and
adjust the wrapper padding token to preserve the same visual gap at small
viewports.
In `@web/src/components/ui/status-badge.tsx`:
- Around line 35-39: The StatusBadge currently always includes role="status",
making it a polite live region; change the component to make live-region
behavior opt-in by adding a boolean prop (e.g., live or announce, default false)
and only include role="status" (and any aria-live if used) when that prop is
true; update the JSX that renders aria-label={statusLabel} and role="status" so
role is conditional (e.g., role={live ? 'status' : undefined}) and adjust the
component's props/type definition and usages (references: StatusBadge component,
statusLabel) accordingly.
In `@web/src/pages/budget/CostBreakdownChart.tsx`:
- Around line 73-74: The options prop mapping recreates a new array each render
when deptDisabled is true causing unnecessary re-renders of SegmentedControl;
wrap the transformed options in a useMemo that depends on deptDisabled and
DIMENSION_OPTIONS (e.g., const memoizedOptions = useMemo(() => deptDisabled ?
DIMENSION_OPTIONS.map(o => o.value === 'department' ? { ...o, disabled: true } :
o) : DIMENSION_OPTIONS, [deptDisabled, DIMENSION_OPTIONS]) and pass
memoizedOptions to SegmentedControl to avoid allocating a new array on every
render.
In `@web/src/pages/TaskDetailPage.tsx`:
- Around line 38-39: Extract the inline type import to a top-level type import
and simplify the handler signature: add a top import like "import type { WsEvent
} from '@/api/types'" at the top of TaskDetailPage.tsx, then change the handler
signature to use that type (handler: (event: WsEvent) => {
useTasksStore.getState().handleWsEvent(event) }) so the inline import is removed
and the code matches the file's existing import style; keep the call to
useTasksStore.getState().handleWsEvent(event) unchanged.
In `@web/vite.config.ts`:
- Around line 7-16: The vendor chunk for Radix in the VENDOR_CHUNKS constant
currently uses the literal 'radix-ui' which won't match scoped packages like
`@radix-ui/react-`*; update the 'vendor-ui' array in VENDOR_CHUNKS (the entry for
'vendor-ui') to replace 'radix-ui' with the scoped prefix '@radix-ui/' so scoped
Radix modules are included in that chunk (keep the rest of the array unchanged).
In `@web/vitest.config.ts`:
- Line 16: The test.exclude setting currently replaces Vitest's built-in
excludes; change it to extend configDefaults.exclude instead of replacing it:
import configDefaults from Vitest's config export (e.g., import { configDefaults
} from 'vitest/config') and set test.exclude to [...configDefaults.exclude,
'**/e2e/**'] so built-in patterns are preserved while adding the e2e exclusion;
update the variable used in web/vitest.config.ts where test.exclude is defined.
---
Outside diff comments:
In `@web/src/components/layout/SidebarNavItem.tsx`:
- Around line 42-54: The badge element in SidebarNavItem currently applies
aria-live="polite" to a conditionally rendered span so removals (badge -> 0)
won't be announced; update the component to place the live region on a
persistent parent (e.g., the wrapping element in SidebarNavItem) or use the
project's LiveRegion component if available, and move the aria-live and
aria-label logic there (use the badge and label props to construct the message
and still render the visual badge as-is) so both additions and removals of the
badge are reliably announced.
In `@web/src/components/ui/inline-edit.tsx`:
- Around line 128-142: The display-mode button in inline-edit.tsx is missing the
shared focus styling constant FOCUS_RING; update the button's className (the one
using cn(...) where startEditing is wired) to include FOCUS_RING (only when not
disabled) alongside the existing hover and disabled classes so the display
button matches the edit-mode input's focus ring for keyboard users; reference
the button that uses props/locals startEditing, renderDisplay, value,
placeholder, disabled, flashClassName and the FOCUS_RING constant when making
the change.
In `@web/src/components/ui/status-badge.tsx`:
- Around line 20-27: The StatusBadgeProps interface is currently internal but
should be exported for reuse; update the interface declaration for
StatusBadgeProps (used by the StatusBadge component) to be exported so stories,
tests, and other consumers can import the type instead of re-declaring it.
Ensure the exported name remains StatusBadgeProps and keep all existing fields
(status, label, pulse, className, animated) unchanged to preserve the component
contract.
In `@web/src/pages/tasks/TaskListView.tsx`:
- Around line 79-96: The sortable column header buttons (rendered in the mapping
over col) are missing the shared focus-ring utility used elsewhere; update the
button className expression in TaskListView's header rendering so that when
col.sortable is true it includes the FOCUS_RING class (alongside the existing
'cursor-pointer hover:text-foreground'), e.g. add FOCUS_RING to the cn(...) call
for the button that uses col.key, handleSort, sortKey and sortDir so the
ArrowUp/ArrowDown icons remain unchanged but keyboard focus shows the same ring
as AgentSpendingTable's ColumnHeader.
---
Duplicate comments:
In `@web/src/__tests__/hooks/useAutoScroll.test.ts`:
- Around line 23-45: The test misses asserting that invoking scrollToBottom
re-enables auto-scroll after the user scrolled away; update the second test for
useAutoScroll to, after dispatching the scroll event and asserting
isAutoScrolling is false, call result.current.scrollToBottom() (inside act if
needed) and then assert result.current.isAutoScrolling is true and
container.scrollTop equals container.scrollHeight to confirm scrollToBottom
restored auto-scrolling; reference the useAutoScroll hook, the scrollToBottom
method, and the isAutoScrolling property when adding the assertions.
In `@web/src/components/layout/Sidebar.tsx`:
- Around line 117-162: Replace the custom tablet overlay block (the if
(isOverlayMode) branch that checks overlayOpen and renders the backdrop div,
aside role="dialog", FocusScope, SidebarNav and SidebarFooter) with the shared
Drawer component from web/src/components/ui/, letting Drawer handle backdrop,
focus trap and dialog semantics; render <SidebarNav collapsed={false} /> and
<SidebarFooter ... /> as Drawer children, wire Drawer open state to overlayOpen
and close to onOverlayClose (or Drawer onOpenChange), and keep behavior for
toggleCollapse, openCommandPalette (call onOverlayClose?.() before
openCommandPalette), shortcutKey, wsConnected, wsReconnectExhausted, user and
logout so existing props are forwarded unchanged. Ensure you remove the backdrop
div, the aside element with role="dialog" and the FocusScope usage when
switching to Drawer.
In `@web/src/hooks/useAutoScroll.ts`:
- Around line 26-43: The scroll handler currently reads containerRef.current
inside onScroll while the listener is attached to the original element, which
can drift if containerRef.current changes; to fix, capture the current element
into a local const (el) when setting up the effect and use that captured element
inside onScroll (referencing el instead of containerRef.current), attach the
listener to that same captured el, and ensure the cleanup removes the listener
from that same captured el; update references to isAutoScrollingRef and
setIsAutoScrolling/thresholdPx as needed inside the handler so they operate on
the bound element identity in useEffect.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 882cfd3a-b81e-4b8b-8f74-68c583e60576
⛔ Files ignored due to path filters (1)
web/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (62)
.gitignoreCLAUDE.mddocs/design/brand-and-ux.mddocs/design/page-structure.mddocs/design/ux-guidelines.mdweb/e2e/fixtures/mock-api.tsweb/e2e/visual/dashboard.spec.tsweb/e2e/visual/login.spec.tsweb/lighthouserc.jsweb/package.jsonweb/playwright.config.tsweb/src/__tests__/hooks/useAutoScroll.test.tsweb/src/__tests__/hooks/useBreakpoint.test.tsweb/src/__tests__/hooks/useCountAnimation.test.tsweb/src/__tests__/hooks/useJustUpdated.test.tsweb/src/__tests__/hooks/useRovingTabIndex.test.tsweb/src/__tests__/integration/ws-dashboard.test.tsxweb/src/__tests__/integration/ws-tasks.test.tsxweb/src/__tests__/pages/DashboardPage.test.tsxweb/src/components/layout/AppLayout.tsxweb/src/components/layout/Sidebar.tsxweb/src/components/layout/SidebarNavItem.tsxweb/src/components/layout/StatusBar.tsxweb/src/components/ui/agent-card.tsxweb/src/components/ui/command-palette.tsxweb/src/components/ui/empty-state.tsxweb/src/components/ui/inline-edit.tsxweb/src/components/ui/lazy-code-mirror-editor.stories.tsxweb/src/components/ui/lazy-code-mirror-editor.tsxweb/src/components/ui/live-region.stories.tsxweb/src/components/ui/live-region.tsxweb/src/components/ui/metric-card.tsxweb/src/components/ui/mobile-unsupported.stories.tsxweb/src/components/ui/mobile-unsupported.tsxweb/src/components/ui/section-card.tsxweb/src/components/ui/sparkline.tsxweb/src/components/ui/status-badge.tsxweb/src/components/ui/task-status-indicator.tsxweb/src/components/ui/toggle-field.tsxweb/src/hooks/useAutoScroll.tsweb/src/hooks/useBreakpoint.tsweb/src/hooks/useCountAnimation.tsweb/src/hooks/useJustUpdated.tsweb/src/hooks/useRovingTabIndex.tsweb/src/lib/utils.tsweb/src/pages/DashboardPage.tsxweb/src/pages/OrgChartPage.tsxweb/src/pages/TaskDetailPage.tsxweb/src/pages/budget/AgentSpendingTable.tsxweb/src/pages/budget/CostBreakdownChart.tsxweb/src/pages/budget/PeriodSelector.tsxweb/src/pages/budget/SpendBurnChart.tsxweb/src/pages/dashboard/ActivityFeed.tsxweb/src/pages/dashboard/BudgetBurnChart.tsxweb/src/pages/setup/WizardProgress.tsxweb/src/pages/tasks/TaskCard.tsxweb/src/pages/tasks/TaskCreateDialog.tsxweb/src/pages/tasks/TaskDependencyGraph.tsxweb/src/pages/tasks/TaskListView.tsxweb/src/stores/auth.tsweb/vite.config.tsweb/vitest.config.ts
web/lighthouserc.js
Outdated
| numberOfRuns: 3, | ||
| startServerCommand: 'npm run preview', | ||
| startServerReadyPattern: 'Local:', | ||
| url: ['http://localhost:4173/login'], |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Audit a route that actually loads the new dashboard code.
/login won't exercise the lazy CodeMirror bundle, WebSocket-driven layout, or task views changed in this PR, so those regressions can still ship with a green Lighthouse run.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/lighthouserc.js` at line 8, The configured Lighthouse test URL currently
points at '/login' which won't exercise the dashboard lazy bundles or
WebSocket-driven views; update the url array in lighthouserc.js (the url
property) to include a route that actually loads the new dashboard code (for
example the dashboard entry route used by the app such as '/dashboard' or the
main app shell) so the Lighthouse run will load CodeMirror, WebSocket layout,
and task view bundles introduced in this PR.
| it('returns desktop when all breakpoints match', () => { | ||
| mockMatchMedia(true, true, true) | ||
| const { result } = renderHook(() => useBreakpoint()) | ||
| expect(result.current.breakpoint).toBe('desktop') | ||
| }) |
There was a problem hiding this comment.
Remove duplicate test case.
This test is an exact duplicate of the test at lines 58-66 with the same name "returns desktop when all breakpoints match". Remove this duplicate.
🗑️ Proposed fix
- it('returns desktop when all breakpoints match', () => {
- mockMatchMedia(true, true, true)
- const { result } = renderHook(() => useBreakpoint())
- expect(result.current.breakpoint).toBe('desktop')
- })📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('returns desktop when all breakpoints match', () => { | |
| mockMatchMedia(true, true, true) | |
| const { result } = renderHook(() => useBreakpoint()) | |
| expect(result.current.breakpoint).toBe('desktop') | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/__tests__/hooks/useBreakpoint.test.ts` around lines 105 - 109, Remove
the duplicate test case named "returns desktop when all breakpoints match" in
useBreakpoint.test.ts: locate the redundant it(...) block that calls
mockMatchMedia(true, true, true) and renderHook(() => useBreakpoint()) and
delete that entire test (the original test at lines ~58-66 should remain),
ensuring no other tests or helper calls (mockMatchMedia, renderHook,
useBreakpoint) are altered.
| <Command | ||
| className={cn( | ||
| 'relative w-full max-w-[640px] rounded-xl border border-border-bright bg-surface shadow-lg', | ||
| 'relative w-full max-w-[640px] max-[1023px]:max-w-[calc(100vw-2rem)] rounded-xl border border-border-bright bg-surface shadow-lg', |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Avoid hardcoded calc spacing in shared UI component.
Use layout padding tokens on the wrapper instead of calc(100vw-2rem) inside the component width class.
Suggested refactor
- <div className="flex items-start justify-center pt-[15vh]">
+ <div className="flex items-start justify-center px-4 pt-[15vh] lg:px-0">
...
- 'relative w-full max-w-[640px] max-[1023px]:max-w-[calc(100vw-2rem)] rounded-xl border border-border-bright bg-surface shadow-lg',
+ 'relative w-full max-w-[640px] rounded-xl border border-border-bright bg-surface shadow-lg',As per coding guidelines web/src/components/ui/**/*.{ts,tsx}: “Use design tokens exclusively—no hardcoded colors, fonts, or spacing.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/components/ui/command-palette.tsx` at line 155, The component
currently hardcodes responsive width with
"max-[1023px]:max-w-[calc(100vw-2rem)]"; update the wrapper in CommandPalette
(the element using that class string) to remove the calc and instead use design
token-based spacing and responsive width (e.g., use max-w-full or the tokenized
max-width plus the layout padding token such as the app's horizontal padding
token) so the component relies on layout padding tokens rather than hardcoded
calc values; ensure you replace the "max-[1023px]:max-w-[calc(100vw-2rem)]"
fragment with the appropriate token-based classes from the design system and
adjust the wrapper padding token to preserve the same visual gap at small
viewports.
Wire WebSocket connection status to Sidebar and StatusBar (replacing hardcoded placeholder), add flash animation props to AgentCard and MetricCard, integrate count animation for metric value transitions, add activity feed auto-scroll, wire TaskDetailPage to WS channel, add animated status color transitions to StatusBadge, and remove the Dashboard-only WS disconnect warning (now global). New hooks: useJustUpdated (relative time tracking), useAutoScroll (scroll-to-bottom with user override), useCountAnimation (rAF-driven number transitions). Integration tests verify WS event -> store update flow for dashboard and tasks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add useRovingTabIndex hook for arrow-key navigation through card grids and lists, LiveRegion component with debounced ARIA live announcements, and FOCUS_RING constant for standardized focus indicators. Component ARIA improvements: StatusBadge and TaskStatusIndicator get role="status", PriorityBadge gets aria-label, SectionCard uses semantic <section> with aria-labelledby, EmptyState gets role="status", Sparkline supports optional ariaLabel prop for standalone use, SidebarNavItem badges get aria-live="polite". Standardize all focus rings to 2px/accent/offset-2 per design spec (fixes ring-offset-1 in budget tables, task cards, inline-edit and ring-inset in task list). Add chart accessibility with role="img" and aria-label on Recharts containers and OrgChart ReactFlow. Add aria-live="polite" to StatusBar health status. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add useBreakpoint hook with matchMedia listeners for viewport-reactive behavior at desktop (>=1280px), desktop-sm (1024-1279px), tablet (768-1023px), and mobile (<768px) breakpoints. Refactor Sidebar for responsive modes: auto-collapse to icon rail at desktop-sm, overlay drawer with backdrop at tablet (triggered by hamburger button in StatusBar), hidden at mobile. Sidebar closes on navigation and backdrop click. User's theme preference is preserved across breakpoints (computed locally, not mutated in store). Add MobileUnsupportedOverlay for <768px viewports with CLI suggestion. Make command palette near-full-width at tablet breakpoints. Wire hamburger button in StatusBar with proper aria-label/aria-expanded. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add manual vendor chunk splitting to Vite config with function-based manualChunks: vendor-react, vendor-ui, vendor-charts, vendor-flow, vendor-editor, vendor-motion, vendor-dnd, vendor-state. Heavy libraries (Recharts 377KB, CodeMirror 322KB, @xyflow 213KB) are isolated into separate chunks loaded only when needed. Add LazyCodeMirrorEditor wrapper for deferred CodeMirror bundle loading. Remove redundant @xyflow CSS import from TaskDependencyGraph (already in global.css). Add rollup-plugin-visualizer for bundle analysis (VITE_ANALYZE=true). Set up Playwright for visual regression testing with 5 browser/viewport projects (desktop/desktop-sm/tablet x chromium, plus firefox and webkit at desktop). Create deterministic mock API fixtures and screenshot tests for login and dashboard pages. Add Lighthouse CI config targeting 90+ performance score. Add new scripts: analyze, e2e, e2e:update, lighthouse. Exclude e2e/ from Vitest test discovery. Update .gitignore for stats.html, test-results/, playwright-report/, .lhci/. Closes #793 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address findings from 3 review agents (docs-consistency, frontend-reviewer): - Add FocusScope + Escape key handler to sidebar overlay (WCAG dialog requirement for focus trapping and keyboard dismiss) - Replace all inline focus ring classes with FOCUS_RING constant from utils.ts across 10 files (AgentSpendingTable, CostBreakdownChart, PeriodSelector, TaskCard, TaskListView, InlineEdit, ToggleField, WizardProgress) for single-source maintenance - Add 3 new components to CLAUDE.md inventory table (LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) - Update CLAUDE.md hooks description with 5 new hook categories - Add LazyCodeMirrorEditor Storybook stories - Update docs/design/ux-guidelines.md with sidebar overlay interaction details (backdrop, close behaviors, aria patterns, CLI hint) - Update docs/design/page-structure.md with StatusBar hamburger note Pre-reviewed by 3 agents, 9 findings addressed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…Rabbit Bugs fixed: - ActivityFeed auto-scroll stops after MAX_VISIBLE cap (used visible.length instead of activities.length) - StatusBadge animated dot missing base color class (transparent on initial paint) - useCountAnimation tween starts from previous target instead of current display (causes jumps) - useRovingTabIndex arrow handler uses stale focusedIndex after click/programmatic focus - Sidebar hidden mode not respected at desktop-sm breakpoint - useCountAnimation test leaks matchMedia stub (vi.unstubAllGlobals added to afterEach) Accessibility: - EmptyState/TaskStatusIndicator: role="status" now opt-in via announce prop (prevents excessive announcements) - Sidebar WS status: replaced inline dot with StatusBadge + sr-only live region - StatusBar WS status: added aria-live="polite" - PeriodSelector/CostBreakdownChart: replaced custom radiogroups with shared SegmentedControl (keyboard nav) - Sidebar: close overlay before opening command palette (FocusScope trap conflict) Security/robustness: - OrgChart viewport: added Number.isFinite + zoom>0 validation for localStorage data - command-palette: added regex + length validation for recent command IDs from localStorage - auth store: removed dead auth_return_path sessionStorage write (open-redirect footgun) - useCountAnimation: clamp durationMs to finite positive number - TaskDetailPage: capture and surface useWebSocket setup errors Build/tooling: - vite.config: conditional visualizer plugin import (npm run analyze now works) - vite.config: explicit preview port 4173 + strictPort - package.json: cross-env for Windows-compatible analyze script - TaskCreateDialog: migrated to FOCUS_RING constant from focus: to focus-visible: Exports/conventions: - Exported AgentCardProps, LiveRegionProps, SparklineProps (CLAUDE.md: "Export props as TypeScript interface") Tests: - cancelAnimationFrame mock: proper handle mapping (timeoutMap) - ws-dashboard: stronger assertions (action_type + task_id, exact cap count) - ws-tasks: pendingTransitions cleanup in afterEach - Renamed misleading test names (useCountAnimation, useBreakpoint) Documentation: - brand-and-ux.md: added LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor to inventory - brand-and-ux.md: added 5 new hooks, renamed section to "Design System Hooks" - CLAUDE.md: added missing devDeps (Playwright, @lhci/cli, rollup-plugin-visualizer, cross-env) - CLAUDE.md: added pre-existing hooks to hooks description Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Lighthouse: - Assert 'error' severity (was 'warn') for performance/a11y/best-practices - Added dashboard URL to test both login and authenticated views Accessibility: - StatusBadge: exported StatusBadgeProps, added announce prop (matches EmptyState/TaskStatusIndicator pattern) - SidebarNavItem: badge aria-live on persistent parent (removals now announced) - InlineEdit: FOCUS_RING on display-mode button for keyboard visibility - TaskListView: FOCUS_RING on sortable column header buttons Build/config: - vite.config: '@radix-ui/' scoped prefix for vendor chunk matching - vitest.config: extend configDefaults.exclude instead of replacing built-in excludes Code quality: - CostBreakdownChart: useMemo for dimension options (avoids new array per render) - TaskDetailPage: extract inline WsEvent import to top-level type import - useBreakpoint.test: removed duplicate test case - useCountAnimation.test: tighter mid-animation assertion (toBeLessThan not toBeLessThanOrEqual) - useAutoScroll.test: added scrollToBottom re-enables auto-scrolling test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4151935 to
387cce8
Compare
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/src/__tests__/pages/DashboardPage.test.tsx (1)
52-54: 🧹 Nitpick | 🔵 TrivialConsider removing unused mock fields.
wsConnectedandwsSetupErrorindefaultHookReturnare no longer used by the component or tests. Removing them would prevent confusion about what the component actually consumes.♻️ Proposed cleanup
const defaultHookReturn: UseDashboardDataReturn = { overview: mockOverview, forecast: null, departmentHealths: [], activities: [], budgetConfig: mockBudgetConfig, orgHealthPercent: null, loading: false, error: null, - wsConnected: true, - wsSetupError: null, }Note: This requires updating
UseDashboardDataReturntype if those fields were removed from the hook's return type.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/__tests__/pages/DashboardPage.test.tsx` around lines 52 - 54, Remove the unused mock fields wsConnected and wsSetupError from the test's defaultHookReturn object and any related mock setup in DashboardPage.test.tsx; if those properties were removed from the actual hook return type, also update the UseDashboardDataReturn type (and any mock factories) to reflect the current shape so tests and TS types remain consistent; search for references to wsConnected and wsSetupError in the test file and delete or update them accordingly to avoid stale/misleading mocks.
♻️ Duplicate comments (2)
web/src/__tests__/integration/ws-dashboard.test.tsx (1)
70-85: 🧹 Nitpick | 🔵 TrivialConsider importing
MAX_ACTIVITIESconstant for maintainability.The comment at line 82 documents the coupling, but importing the constant (if exported) would prevent silent test failures if the cap changes.
💡 Proposed improvement
+import { MAX_ACTIVITIES } from '@/stores/analytics' // if exported + // Activities should be capped at MAX_ACTIVITIES (50) defined in stores/analytics.ts const activities = useAnalyticsStore.getState().activities -expect(activities).toHaveLength(50) +expect(activities).toHaveLength(MAX_ACTIVITIES)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/__tests__/integration/ws-dashboard.test.tsx` around lines 70 - 85, The test hardcodes the expected cap (50) which can drift from the source of truth; import the exported MAX_ACTIVITIES constant from the analytics store module and use it in the assertion instead of 50. Update the test to import MAX_ACTIVITIES (from the module that defines it alongside updateFromWsEvent / useAnalyticsStore) and replace expect(activities).toHaveLength(50) with expect(activities).toHaveLength(MAX_ACTIVITIES); if MAX_ACTIVITIES is not exported, export it from the analytics store where updateFromWsEvent and activities are defined.web/src/hooks/useAutoScroll.ts (1)
26-43:⚠️ Potential issue | 🟡 MinorRebind listener to a stable element instance inside the effect.
onScrollcurrently readscontainerRef.current, while the listener is attached toelcaptured at effect start. If the ref target changes, updates can come from a stale listener and mismatched element state.♻️ Proposed fix
export function useAutoScroll( containerRef?: RefObject<HTMLElement | null>, thresholdPx = DEFAULT_THRESHOLD_PX, ): { @@ const [isAutoScrolling, setIsAutoScrolling] = useState(true) const isAutoScrollingRef = useRef(true) + const containerEl = containerRef?.current // Track scroll position to determine if user has scrolled away useEffect(() => { - const el = containerRef?.current - if (!el) return + if (!containerEl) return function onScroll() { - const container = containerRef?.current - if (!container) return const nearBottom = - container.scrollTop >= container.scrollHeight - container.clientHeight - thresholdPx + containerEl.scrollTop >= containerEl.scrollHeight - containerEl.clientHeight - thresholdPx if (nearBottom !== isAutoScrollingRef.current) { isAutoScrollingRef.current = nearBottom setIsAutoScrolling(nearBottom) } } - el.addEventListener('scroll', onScroll, { passive: true }) - return () => el.removeEventListener('scroll', onScroll) - }, [containerRef, thresholdPx]) + containerEl.addEventListener('scroll', onScroll, { passive: true }) + return () => containerEl.removeEventListener('scroll', onScroll) + }, [containerEl, thresholdPx])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/hooks/useAutoScroll.ts` around lines 26 - 43, The scroll listener reads containerRef.current but is attached to the captured el, which can drift if the ref target changes; fix by capturing the element at the top of useEffect (const el = containerRef?.current) and reference that captured el inside onScroll instead of containerRef.current, and ensure the effect depends on that captured el (add el to the dependency array) so the listener is rebound and cleaned up against the correct element; keep existing uses of thresholdPx and isAutoScrollingRef/setIsAutoScrolling in onScroll.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/e2e/fixtures/mock-api.ts`:
- Around line 71-79: The freezeTime helper currently only overrides Date.now()
so calls to new Date() still reflect the real system time; update freezeTime
(the function using page.addInitScript) to also override the global Date
constructor inside the page context: capture the original Date (e.g., const
OriginalDate = Date), compute fixedTime, set Date.now = () => fixedTime, then
replace Date with a function/constructor that delegates to OriginalDate when
called with arguments but returns a new OriginalDate(fixedTime) when called
without arguments, and ensure Date.prototype and static methods (like toString,
parse, UTC) are preserved by assigning Date.prototype = OriginalDate.prototype
and copying needed static properties so existing code like BudgetBurnChart.tsx
that calls new Date().toLocaleDateString() sees the frozen time.
In `@web/e2e/visual/login.spec.ts`:
- Around line 9-13: The test 'login page screenshot' currently captures a
screenshot immediately after page.goto and waitForFonts, which can be flaky;
after navigation (page.goto) and before
expect(page).toHaveScreenshot('login.png'), wait for the page to reach a stable
state by calling page.waitForLoadState('networkidle') (or equivalent) so all
network requests finish and animations settle; update the test function (the
test named "login page screenshot") to await
page.waitForLoadState('networkidle') (and keep waitForFonts) before taking the
screenshot to improve reliability.
In `@web/playwright.config.ts`:
- Around line 26-47: Tests are missing a mobile viewport project, so add a new
Playwright project entry in the projects array (e.g., name 'mobile-chromium')
that uses a mobile device or a mobile viewport (for example devices['Pixel 5']
or viewport { width: 375, height: 812 }) to ensure MobileUnsupportedOverlay and
useBreakpoint (<768px) behaviors are covered; update the projects array
alongside existing entries (desktop-chromium, desktop-sm-chromium,
tablet-chromium, desktop-firefox, desktop-webkit) so CI runs a mobile scenario
for visual/regression testing.
In `@web/src/__tests__/hooks/useAutoScroll.test.ts`:
- Around line 80-87: The test for useAutoScroll should wrap the call to
result.current.scrollToBottom() in act() to match other tests and avoid
warnings: update the null-ref case in the test to call act(() =>
result.current.scrollToBottom()), keeping the existing assertions
(isAutoScrolling) intact; ensure act is imported from your test utils (the same
act used elsewhere in these tests) and reference the useAutoScroll hook,
result.current.scrollToBottom, and isAutoScrolling when making the change.
In `@web/src/__tests__/hooks/useJustUpdated.test.ts`:
- Around line 4-155: Add property-based tests using fast-check for the
useJustUpdated hook to satisfy the repository standard: import fast-check in
web/src/__tests__/hooks/useJustUpdated.test.ts and add fc.property-based tests
that (1) assert TTL boundary invariants for markUpdated + isJustUpdated (given
random ttlMs and elapsed times, isJustUpdated should be true iff elapsed <
ttlMs), (2) assert re-mark resets TTL (markUpdated twice with random intervals
yields still-true if second mark + subsequent elapsed < ttlMs), and (3) assert
independent tracking for multiple IDs (generate arrays of timestamps/IDs and
ensure one ID expiring does not affect others). Use renderHook(() =>
useJustUpdated({ ttlMs })) and vi.useFakeTimers() / vi.advanceTimersByTime
within the property body, referencing markUpdated, isJustUpdated, relativeTime,
and clearAll to drive assertions; fail the property when any invariant is
violated.
In `@web/src/__tests__/hooks/useRovingTabIndex.test.ts`:
- Around line 4-14: Add a property-based test using fast-check in
web/src/__tests__/hooks/useRovingTabIndex.test.ts that generates arbitrary item
counts (e.g., 1–100) and for each run uses the existing makeContainer helper and
a container ref to renderHook(useRovingTabIndex({ containerRef: ref,
orientation: 'vertical' })); assert that result.current.focusedIndex is >= 0 and
< itemCount to ensure the hook maintains a valid focusedIndex for any container
size. Ensure you import fast-check (fc) and clean up the DOM between runs if
needed.
In `@web/src/components/layout/SidebarNavItem.tsx`:
- Around line 42-55: The aria-live on the conditionally rendered badge can cause
screen readers to miss updates; update SidebarNavItem to ensure the live region
is always present by either always rendering the badge element (the span that
currently checks badge !== undefined && badge > 0) and toggling its
visibility/styles when badge is 0 (e.g., hidden or visually hidden), or move
aria-live="polite" to a sibling element that is always rendered; keep the
aria-label logic using label and badge (e.g., `${badge} pending
${label.toLowerCase()}`) intact so announcements remain descriptive.
In `@web/src/components/ui/lazy-code-mirror-editor.tsx`:
- Around line 15-20: The LazyCodeMirrorEditor currently only uses Suspense and
will crash if the dynamic CodeMirrorEditor import fails; wrap the Suspense with
an error boundary (or ensure a higher-level one exists) so chunk-load/network
errors render a fallback UI instead of throwing. Update LazyCodeMirrorEditor to
render an ErrorBoundary (or reuse your app's existing ErrorBoundary component)
that shows an appropriate fallback (e.g., a friendly error card) around the
Suspense+CodeMirrorEditor; reference the LazyCodeMirrorEditor component and the
CodeMirrorEditor import so reviewers can locate and wrap Suspense (currently
returning <Suspense fallback={<SkeletonCard />}>).
In `@web/src/components/ui/metric-card.tsx`:
- Around line 30-32: The count animation hook is always running even when
animateValue is false; update useCountAnimation to accept an options object
(e.g., { enabled?: boolean }) and short-circuit the RAF loop when enabled is
false, then call useCountAnimation(numericValue ?? 0, { enabled: animateValue &&
numericValue !== undefined }) from MetricCard (references: useCountAnimation,
numericValue, animateValue, displayValue, MetricCard) so the hook does nothing
and does not start the RAF loop when animation is disabled.
In `@web/src/hooks/useJustUpdated.ts`:
- Around line 75-79: relativeTime currently returns a formatted string even
after the entry has expired (until GC removes it); make it mirror isJustUpdated
by returning null when the elapsed time since entriesRef.current.get(id) exceeds
the same TTL used by isJustUpdated. Update useJustUpdated's relativeTime to
compute const elapsed = Date.now() - timestamp, compare elapsed against the
shared TTL variable/parameter used by isJustUpdated, return null if elapsed >=
TTL, otherwise return formatRelative(elapsed); reference entriesRef,
relativeTime and isJustUpdated to locate where to apply this change.
- Line 37: The hook computes ttlMs from options but doesn't validate it; update
the logic in useJustUpdated (where ttlMs and DEFAULT_TTL_MS are used) to
validate that ttlMs is a finite number greater than 0 and fallback to
DEFAULT_TTL_MS if not; specifically, after const ttlMs = options?.ttlMs ??
DEFAULT_TTL_MS, replace with a validation that ensures
Number.isFinite(options?.ttlMs) && options.ttlMs > 0, otherwise set ttlMs =
DEFAULT_TTL_MS, so the hook never uses non-finite or non-positive TTL values.
In `@web/src/hooks/useRovingTabIndex.ts`:
- Around line 35-43: When updating item tabindexes in useRovingTabIndex, clamp
focusedIndex against the current items length to avoid leaving all items with
tabindex="-1" when the list shrinks: inside the useEffect that reads
containerRef and queries items via ITEM_SELECTOR, early-return if items.length
=== 0, compute a safeIndex = Math.min(focusedIndex, items.length - 1) (or 0 if
negative) and use safeIndex instead of focusedIndex when deciding which item
gets tabindex="0"; update any state setter (if present) to reflect the clamped
value so the hook stays consistent.
In `@web/src/stores/auth.ts`:
- Around line 150-156: The redirect block using currentPath and
window.location.href always sends users to '/login' and drops the original path;
update it to preserve a return path by capturing the encoded currentPath (unless
it's '/login' or '/setup'), then redirect to '/login?next=<encodedPath>' or
write it to a short-lived key in localStorage/sessionStorage before setting
window.location.href; also ensure the login completion flow (login
handler/component) reads that query param or storage key and navigates back to
the preserved path after successful auth.
---
Outside diff comments:
In `@web/src/__tests__/pages/DashboardPage.test.tsx`:
- Around line 52-54: Remove the unused mock fields wsConnected and wsSetupError
from the test's defaultHookReturn object and any related mock setup in
DashboardPage.test.tsx; if those properties were removed from the actual hook
return type, also update the UseDashboardDataReturn type (and any mock
factories) to reflect the current shape so tests and TS types remain consistent;
search for references to wsConnected and wsSetupError in the test file and
delete or update them accordingly to avoid stale/misleading mocks.
---
Duplicate comments:
In `@web/src/__tests__/integration/ws-dashboard.test.tsx`:
- Around line 70-85: The test hardcodes the expected cap (50) which can drift
from the source of truth; import the exported MAX_ACTIVITIES constant from the
analytics store module and use it in the assertion instead of 50. Update the
test to import MAX_ACTIVITIES (from the module that defines it alongside
updateFromWsEvent / useAnalyticsStore) and replace
expect(activities).toHaveLength(50) with
expect(activities).toHaveLength(MAX_ACTIVITIES); if MAX_ACTIVITIES is not
exported, export it from the analytics store where updateFromWsEvent and
activities are defined.
In `@web/src/hooks/useAutoScroll.ts`:
- Around line 26-43: The scroll listener reads containerRef.current but is
attached to the captured el, which can drift if the ref target changes; fix by
capturing the element at the top of useEffect (const el = containerRef?.current)
and reference that captured el inside onScroll instead of containerRef.current,
and ensure the effect depends on that captured el (add el to the dependency
array) so the listener is rebound and cleaned up against the correct element;
keep existing uses of thresholdPx and isAutoScrollingRef/setIsAutoScrolling in
onScroll.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 7ea4727d-0e94-4b6d-953a-cb196736ece4
⛔ Files ignored due to path filters (1)
web/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (62)
.gitignoreCLAUDE.mddocs/design/brand-and-ux.mddocs/design/page-structure.mddocs/design/ux-guidelines.mdweb/e2e/fixtures/mock-api.tsweb/e2e/visual/dashboard.spec.tsweb/e2e/visual/login.spec.tsweb/lighthouserc.jsweb/package.jsonweb/playwright.config.tsweb/src/__tests__/hooks/useAutoScroll.test.tsweb/src/__tests__/hooks/useBreakpoint.test.tsweb/src/__tests__/hooks/useCountAnimation.test.tsweb/src/__tests__/hooks/useJustUpdated.test.tsweb/src/__tests__/hooks/useRovingTabIndex.test.tsweb/src/__tests__/integration/ws-dashboard.test.tsxweb/src/__tests__/integration/ws-tasks.test.tsxweb/src/__tests__/pages/DashboardPage.test.tsxweb/src/components/layout/AppLayout.tsxweb/src/components/layout/Sidebar.tsxweb/src/components/layout/SidebarNavItem.tsxweb/src/components/layout/StatusBar.tsxweb/src/components/ui/agent-card.tsxweb/src/components/ui/command-palette.tsxweb/src/components/ui/empty-state.tsxweb/src/components/ui/inline-edit.tsxweb/src/components/ui/lazy-code-mirror-editor.stories.tsxweb/src/components/ui/lazy-code-mirror-editor.tsxweb/src/components/ui/live-region.stories.tsxweb/src/components/ui/live-region.tsxweb/src/components/ui/metric-card.tsxweb/src/components/ui/mobile-unsupported.stories.tsxweb/src/components/ui/mobile-unsupported.tsxweb/src/components/ui/section-card.tsxweb/src/components/ui/sparkline.tsxweb/src/components/ui/status-badge.tsxweb/src/components/ui/task-status-indicator.tsxweb/src/components/ui/toggle-field.tsxweb/src/hooks/useAutoScroll.tsweb/src/hooks/useBreakpoint.tsweb/src/hooks/useCountAnimation.tsweb/src/hooks/useJustUpdated.tsweb/src/hooks/useRovingTabIndex.tsweb/src/lib/utils.tsweb/src/pages/DashboardPage.tsxweb/src/pages/OrgChartPage.tsxweb/src/pages/TaskDetailPage.tsxweb/src/pages/budget/AgentSpendingTable.tsxweb/src/pages/budget/CostBreakdownChart.tsxweb/src/pages/budget/PeriodSelector.tsxweb/src/pages/budget/SpendBurnChart.tsxweb/src/pages/dashboard/ActivityFeed.tsxweb/src/pages/dashboard/BudgetBurnChart.tsxweb/src/pages/setup/WizardProgress.tsxweb/src/pages/tasks/TaskCard.tsxweb/src/pages/tasks/TaskCreateDialog.tsxweb/src/pages/tasks/TaskDependencyGraph.tsxweb/src/pages/tasks/TaskListView.tsxweb/src/stores/auth.tsweb/vite.config.tsweb/vitest.config.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Dashboard Test
- GitHub Check: Build Backend
- GitHub Check: Build Sandbox
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (7)
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
web/src/**/*.{ts,tsx}: Web dashboard: ALWAYS reuse existing components fromweb/src/components/ui/before creating new ones. Refer to the component inventory in CLAUDE.md for the 40+ shared building blocks.
Web dashboard colors: use Tailwind semantic classes (text-foreground,bg-card,text-accent,text-success,bg-danger) or CSS variables (var(--so-accent)). NEVER hardcode hex values in.tsx/.tsfiles.
Web dashboard typography: usefont-sansorfont-mono(maps to Geist tokens). NEVER setfontFamilydirectly.
Web dashboard spacing: use density-aware tokens (p-card,gap-section-gap,gap-grid-gap) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing.
Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover),border-border,border-bright).
Web dashboard: importcnfrom@/lib/utilsfor conditional class merging in components.
Do NOT recreate status dots inline — use<StatusBadge>component.
Do NOT build card-with-header layouts from scratch — use<SectionCard>component.
Do NOT create metric displays withtext-metric font-bold— use<MetricCard>component.
Do NOT render initials circles manually — use<Avatar>component.
Do NOT create complex (>8 line) JSX inside.map()— extract to a shared component.
Do NOT usergba()with hardcoded values — use design token variables.
A PostToolUse hook (scripts/check_web_design_system.py) runs automatically on every Edit/Write toweb/src/files. It catches hardcoded colors, rgba values, font-family, missing Storybook stories, duplicate patterns, and complex.map()blocks. Fix all violations before proceeding.
Web dashboard ESLint: enforce zero warnings. All violations must be fixed before merging.
Files:
web/src/pages/tasks/TaskDependencyGraph.tsxweb/src/pages/budget/SpendBurnChart.tsxweb/src/lib/utils.tsweb/src/components/ui/section-card.tsxweb/src/pages/setup/WizardProgress.tsxweb/src/pages/dashboard/BudgetBurnChart.tsxweb/src/pages/tasks/TaskCard.tsxweb/src/__tests__/pages/DashboardPage.test.tsxweb/src/stores/auth.tsweb/src/pages/DashboardPage.tsxweb/src/pages/budget/PeriodSelector.tsxweb/src/components/ui/toggle-field.tsxweb/src/components/ui/agent-card.tsxweb/src/pages/budget/AgentSpendingTable.tsxweb/src/pages/dashboard/ActivityFeed.tsxweb/src/components/ui/inline-edit.tsxweb/src/pages/TaskDetailPage.tsxweb/src/components/layout/AppLayout.tsxweb/src/components/ui/mobile-unsupported.tsxweb/src/components/layout/SidebarNavItem.tsxweb/src/components/ui/sparkline.tsxweb/src/components/ui/lazy-code-mirror-editor.tsxweb/src/pages/budget/CostBreakdownChart.tsxweb/src/components/ui/command-palette.tsxweb/src/components/ui/metric-card.tsxweb/src/components/ui/lazy-code-mirror-editor.stories.tsxweb/src/pages/tasks/TaskListView.tsxweb/src/pages/tasks/TaskCreateDialog.tsxweb/src/__tests__/hooks/useAutoScroll.test.tsweb/src/hooks/useAutoScroll.tsweb/src/components/ui/task-status-indicator.tsxweb/src/__tests__/hooks/useCountAnimation.test.tsweb/src/__tests__/hooks/useJustUpdated.test.tsweb/src/components/layout/StatusBar.tsxweb/src/hooks/useRovingTabIndex.tsweb/src/components/ui/live-region.tsxweb/src/__tests__/hooks/useRovingTabIndex.test.tsweb/src/hooks/useBreakpoint.tsweb/src/components/ui/live-region.stories.tsxweb/src/hooks/useJustUpdated.tsweb/src/hooks/useCountAnimation.tsweb/src/components/ui/status-badge.tsxweb/src/components/ui/empty-state.tsxweb/src/__tests__/hooks/useBreakpoint.test.tsweb/src/components/ui/mobile-unsupported.stories.tsxweb/src/__tests__/integration/ws-tasks.test.tsxweb/src/components/layout/Sidebar.tsxweb/src/__tests__/integration/ws-dashboard.test.tsxweb/src/pages/OrgChartPage.tsx
web/src/components/ui/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
When creating a new shared web component, place it in
web/src/components/ui/with a descriptive kebab-case filename. Create a.stories.tsxfile alongside with all states (default, hover, loading, error, empty). Export props as a TypeScript interface. Use design tokens exclusively — no hardcoded colors, fonts, or spacing.
Files:
web/src/components/ui/section-card.tsxweb/src/components/ui/toggle-field.tsxweb/src/components/ui/agent-card.tsxweb/src/components/ui/inline-edit.tsxweb/src/components/ui/mobile-unsupported.tsxweb/src/components/ui/sparkline.tsxweb/src/components/ui/lazy-code-mirror-editor.tsxweb/src/components/ui/command-palette.tsxweb/src/components/ui/metric-card.tsxweb/src/components/ui/lazy-code-mirror-editor.stories.tsxweb/src/components/ui/task-status-indicator.tsxweb/src/components/ui/live-region.tsxweb/src/components/ui/live-region.stories.tsxweb/src/components/ui/status-badge.tsxweb/src/components/ui/empty-state.tsxweb/src/components/ui/mobile-unsupported.stories.tsx
web/src/**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Web dashboard: use fast-check for property-based testing with
fc.assertandfc.property. Run tests vianpm --prefix web run test.
Files:
web/src/__tests__/pages/DashboardPage.test.tsxweb/src/__tests__/hooks/useAutoScroll.test.tsweb/src/__tests__/hooks/useCountAnimation.test.tsweb/src/__tests__/hooks/useJustUpdated.test.tsweb/src/__tests__/hooks/useRovingTabIndex.test.tsweb/src/__tests__/hooks/useBreakpoint.test.tsweb/src/__tests__/integration/ws-tasks.test.tsxweb/src/__tests__/integration/ws-dashboard.test.tsx
web/**/__tests__/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Web dashboard tests: use Vitest with coverage scoped to files changed vs origin/main via
npm --prefix web run test.
Files:
web/src/__tests__/pages/DashboardPage.test.tsxweb/src/__tests__/hooks/useAutoScroll.test.tsweb/src/__tests__/hooks/useCountAnimation.test.tsweb/src/__tests__/hooks/useJustUpdated.test.tsweb/src/__tests__/hooks/useRovingTabIndex.test.tsweb/src/__tests__/hooks/useBreakpoint.test.tsweb/src/__tests__/integration/ws-tasks.test.tsxweb/src/__tests__/integration/ws-dashboard.test.tsx
docs/design/*.md
📄 CodeRabbit inference engine (CLAUDE.md)
When approved deviations occur, update the relevant
docs/design/page to reflect the new reality.
Files:
docs/design/page-structure.mddocs/design/ux-guidelines.mddocs/design/brand-and-ux.md
docs/**/*.md
📄 CodeRabbit inference engine (CLAUDE.md)
API reference: REST API documented in
docs/rest-api.md+ auto-generateddocs/_generated/api-reference.htmlviascripts/export_openapi.py. Library reference:docs/api/auto-generated via mkdocstrings + Griffe (AST-based).
Files:
docs/design/page-structure.mddocs/design/ux-guidelines.mddocs/design/brand-and-ux.md
web/src/**/*.stories.tsx
📄 CodeRabbit inference engine (CLAUDE.md)
Storybook 10 is ESM-only. Use
defineMainfrom@storybook/react-vite/nodeanddefinePreviewfrom@storybook/react-vite. Import fromstorybook/test(not@storybook/test),storybook/actions(not@storybook/addon-actions). Useparameters.backgrounds.options(object keyed by name) andinitialGlobals.backgrounds.value. Setparameters.a11y.test: 'error' | 'todo' | 'off'globally to enforce WCAG compliance.
Files:
web/src/components/ui/lazy-code-mirror-editor.stories.tsxweb/src/components/ui/live-region.stories.tsxweb/src/components/ui/mobile-unsupported.stories.tsx
🧠 Learnings (36)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10 is ESM-only. Use `defineMain` from `storybook/react-vite/node` and `definePreview` from `storybook/react-vite`. Import from `storybook/test` (not `storybook/test`), `storybook/actions` (not `storybook/addon-actions`). Use `parameters.backgrounds.options` (object keyed by name) and `initialGlobals.backgrounds.value`. Set `parameters.a11y.test: 'error' | 'todo' | 'off'` globally to enforce WCAG compliance.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T08:28:32.845Z
Learning: Applies to web/src/__tests__/**/*.{ts,js} : Dashboard testing: Vitest unit tests organized by feature under `web/src/__tests__/`. Use fast-check for property-based testing (`fc.assert` + `fc.property`).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/**/__tests__/**/*.{ts,tsx} : Web dashboard tests: use Vitest with coverage scoped to files changed vs origin/main via `npm --prefix web run test`.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.test.{ts,tsx} : Web dashboard: use fast-check for property-based testing with `fc.assert` and `fc.property`. Run tests via `npm --prefix web run test`.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to web/** : Web dashboard: Node.js 20+, dependencies in web/package.json (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, fast-check, ESLint, vue-tsc).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to web/package.json : Web dashboard Node.js 20+; dependencies in web/package.json (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, ESLint, vue-tsc)
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/**/__tests__/**/*.{ts,tsx} : Web dashboard tests: use Vitest with coverage scoped to files changed vs origin/main via `npm --prefix web run test`.
Applied to files:
.gitignoreweb/src/__tests__/pages/DashboardPage.test.tsxweb/package.jsonweb/e2e/visual/login.spec.tsweb/e2e/visual/dashboard.spec.tsweb/src/__tests__/hooks/useAutoScroll.test.tsweb/src/__tests__/hooks/useCountAnimation.test.tsweb/lighthouserc.jsweb/src/__tests__/hooks/useJustUpdated.test.tsweb/vite.config.tsweb/vitest.config.tsweb/src/__tests__/hooks/useRovingTabIndex.test.tsCLAUDE.mdweb/playwright.config.tsweb/src/__tests__/hooks/useBreakpoint.test.tsweb/src/__tests__/integration/ws-tasks.test.tsxweb/src/__tests__/integration/ws-dashboard.test.tsx
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to web/package.json : Web dashboard Node.js 20+; dependencies in web/package.json (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, ESLint, vue-tsc)
Applied to files:
.gitignoreweb/package.jsonweb/vite.config.tsCLAUDE.md
📚 Learning: 2026-03-15T18:17:43.675Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to web/** : Web dashboard: Node.js 20+, dependencies in web/package.json (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, fast-check, ESLint, vue-tsc).
Applied to files:
.gitignoreweb/src/pages/DashboardPage.tsxweb/package.jsonweb/e2e/visual/dashboard.spec.tsweb/lighthouserc.jsweb/vite.config.tsCLAUDE.md
📚 Learning: 2026-03-20T08:28:32.845Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T08:28:32.845Z
Learning: Applies to web/src/__tests__/**/*.{ts,js} : Dashboard testing: Vitest unit tests organized by feature under `web/src/__tests__/`. Use fast-check for property-based testing (`fc.assert` + `fc.property`).
Applied to files:
.gitignoreweb/src/__tests__/pages/DashboardPage.test.tsxweb/e2e/visual/login.spec.tsweb/e2e/visual/dashboard.spec.tsweb/src/__tests__/hooks/useAutoScroll.test.tsweb/src/__tests__/hooks/useCountAnimation.test.tsweb/lighthouserc.jsweb/src/__tests__/hooks/useJustUpdated.test.tsweb/vitest.config.tsweb/src/__tests__/hooks/useRovingTabIndex.test.tsCLAUDE.mdweb/playwright.config.tsweb/src/__tests__/hooks/useBreakpoint.test.tsweb/src/__tests__/integration/ws-tasks.test.tsxweb/src/__tests__/integration/ws-dashboard.test.tsx
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10 is ESM-only. Use `defineMain` from `storybook/react-vite/node` and `definePreview` from `storybook/react-vite`. Import from `storybook/test` (not `storybook/test`), `storybook/actions` (not `storybook/addon-actions`). Use `parameters.backgrounds.options` (object keyed by name) and `initialGlobals.backgrounds.value`. Set `parameters.a11y.test: 'error' | 'todo' | 'off'` globally to enforce WCAG compliance.
Applied to files:
.gitignoreweb/src/pages/setup/WizardProgress.tsxweb/package.jsonweb/e2e/visual/dashboard.spec.tsweb/src/components/ui/lazy-code-mirror-editor.stories.tsxweb/src/__tests__/hooks/useAutoScroll.test.tsweb/src/__tests__/hooks/useCountAnimation.test.tsweb/vite.config.tsweb/vitest.config.tsweb/src/__tests__/hooks/useRovingTabIndex.test.tsCLAUDE.mdweb/src/components/ui/live-region.stories.tsxweb/playwright.config.tsweb/src/__tests__/hooks/useBreakpoint.test.tsweb/src/components/ui/mobile-unsupported.stories.tsxweb/src/__tests__/integration/ws-tasks.test.tsxdocs/design/brand-and-ux.mdweb/src/__tests__/integration/ws-dashboard.test.tsx
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : When creating a new shared web component, place it in `web/src/components/ui/` with a descriptive kebab-case filename. Create a `.stories.tsx` file alongside with all states (default, hover, loading, error, empty). Export props as a TypeScript interface. Use design tokens exclusively — no hardcoded colors, fonts, or spacing.
Applied to files:
.gitignoreweb/src/components/ui/section-card.tsxweb/src/components/ui/agent-card.tsxweb/src/pages/TaskDetailPage.tsxweb/src/components/ui/sparkline.tsxweb/e2e/visual/dashboard.spec.tsweb/src/components/ui/command-palette.tsxweb/src/components/ui/lazy-code-mirror-editor.stories.tsxweb/src/components/ui/live-region.tsxCLAUDE.mdweb/src/components/ui/live-region.stories.tsxweb/src/components/ui/mobile-unsupported.stories.tsxdocs/design/brand-and-ux.mdweb/src/components/layout/Sidebar.tsx
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.test.{ts,tsx} : Web dashboard: use fast-check for property-based testing with `fc.assert` and `fc.property`. Run tests via `npm --prefix web run test`.
Applied to files:
.gitignoreweb/src/__tests__/pages/DashboardPage.test.tsxweb/package.jsonweb/e2e/visual/login.spec.tsweb/e2e/visual/dashboard.spec.tsweb/src/__tests__/hooks/useAutoScroll.test.tsweb/src/__tests__/hooks/useCountAnimation.test.tsweb/src/__tests__/hooks/useJustUpdated.test.tsweb/vitest.config.tsweb/src/__tests__/hooks/useRovingTabIndex.test.tsweb/e2e/fixtures/mock-api.tsCLAUDE.mdweb/playwright.config.tsweb/src/__tests__/hooks/useBreakpoint.test.tsweb/src/__tests__/integration/ws-tasks.test.tsxweb/src/__tests__/integration/ws-dashboard.test.tsx
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/tsconfig.json : Web dashboard TypeScript: use `baseUrl` deprecated removal for TS 6 — use path-relative `paths` entries instead. Remove explicit `"esModuleInterop": true` (always true in TS 6). Explicitly list needed types in `"types"` array (e.g., `"types": ["vitest/globals"]`). Use `"moduleResolution": "bundler"`. `"strict"` defaults to true.
Applied to files:
.gitignoreweb/src/__tests__/pages/DashboardPage.test.tsxweb/src/pages/DashboardPage.tsxweb/package.jsonweb/e2e/visual/dashboard.spec.tsweb/vite.config.tsweb/vitest.config.tsCLAUDE.mdweb/playwright.config.ts
📚 Learning: 2026-03-15T21:20:09.993Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:20:09.993Z
Learning: Applies to web/src/components/** : Vue components organized by feature (agents/, approvals/, budget/, common/, dashboard/, layout/, messages/, org-chart/, tasks/).
Applied to files:
.gitignoreCLAUDE.md
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard: import `cn` from `@/lib/utils` for conditional class merging in components.
Applied to files:
web/src/pages/tasks/TaskDependencyGraph.tsxweb/src/lib/utils.tsweb/src/components/ui/section-card.tsxweb/src/pages/setup/WizardProgress.tsxweb/src/pages/tasks/TaskCard.tsxweb/src/__tests__/pages/DashboardPage.test.tsxweb/src/pages/DashboardPage.tsxweb/src/pages/budget/PeriodSelector.tsxweb/src/components/ui/toggle-field.tsxweb/src/pages/budget/AgentSpendingTable.tsxweb/src/components/ui/inline-edit.tsxweb/src/pages/TaskDetailPage.tsxweb/src/pages/budget/CostBreakdownChart.tsxweb/e2e/visual/dashboard.spec.tsweb/src/components/ui/metric-card.tsxweb/src/pages/tasks/TaskListView.tsxweb/src/pages/tasks/TaskCreateDialog.tsxweb/src/components/layout/StatusBar.tsxweb/vite.config.tsCLAUDE.mddocs/design/brand-and-ux.mdweb/src/components/layout/Sidebar.tsx
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside `.map()` — extract to a shared component.
Applied to files:
web/src/pages/tasks/TaskDependencyGraph.tsxweb/src/pages/tasks/TaskCard.tsxweb/src/components/ui/agent-card.tsxweb/src/pages/TaskDetailPage.tsxweb/src/components/ui/sparkline.tsxweb/src/pages/budget/CostBreakdownChart.tsxweb/src/components/ui/command-palette.tsxweb/src/pages/tasks/TaskListView.tsxweb/src/pages/tasks/TaskCreateDialog.tsxweb/src/components/ui/live-region.tsxweb/vite.config.tsCLAUDE.mddocs/design/brand-and-ux.mdweb/src/components/layout/Sidebar.tsx
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT recreate status dots inline — use `<StatusBadge>` component.
Applied to files:
web/src/pages/tasks/TaskDependencyGraph.tsxweb/src/lib/utils.tsweb/src/pages/setup/WizardProgress.tsxweb/src/pages/dashboard/BudgetBurnChart.tsxweb/src/pages/tasks/TaskCard.tsxweb/src/__tests__/pages/DashboardPage.test.tsxweb/src/pages/DashboardPage.tsxweb/src/components/ui/toggle-field.tsxweb/src/components/ui/agent-card.tsxweb/src/pages/budget/AgentSpendingTable.tsxweb/src/components/ui/inline-edit.tsxweb/src/pages/TaskDetailPage.tsxweb/src/components/layout/AppLayout.tsxweb/src/components/layout/SidebarNavItem.tsxweb/src/components/ui/sparkline.tsxweb/src/pages/budget/CostBreakdownChart.tsxweb/src/components/ui/command-palette.tsxweb/src/pages/tasks/TaskListView.tsxweb/src/pages/tasks/TaskCreateDialog.tsxweb/src/components/ui/task-status-indicator.tsxweb/src/components/layout/StatusBar.tsxCLAUDE.mdweb/src/components/ui/status-badge.tsxdocs/design/brand-and-ux.mdweb/src/components/layout/Sidebar.tsx
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create metric displays with `text-metric font-bold` — use `<MetricCard>` component.
Applied to files:
web/src/pages/budget/SpendBurnChart.tsxweb/src/components/ui/section-card.tsxweb/src/pages/dashboard/BudgetBurnChart.tsxweb/src/pages/tasks/TaskCard.tsxweb/src/__tests__/pages/DashboardPage.test.tsxweb/src/pages/DashboardPage.tsxweb/src/pages/budget/PeriodSelector.tsxweb/src/components/ui/agent-card.tsxweb/src/pages/budget/AgentSpendingTable.tsxweb/src/components/ui/sparkline.tsxweb/src/pages/budget/CostBreakdownChart.tsxweb/src/components/ui/command-palette.tsxweb/src/components/ui/metric-card.tsxweb/src/pages/tasks/TaskCreateDialog.tsxCLAUDE.mddocs/design/brand-and-ux.md
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard colors: use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`) or CSS variables (`var(--so-accent)`). NEVER hardcode hex values in `.tsx`/`.ts` files.
Applied to files:
web/src/lib/utils.tsweb/src/pages/tasks/TaskCard.tsxweb/src/pages/DashboardPage.tsxweb/src/pages/budget/AgentSpendingTable.tsxweb/src/components/ui/inline-edit.tsxweb/src/components/ui/sparkline.tsxweb/src/components/ui/command-palette.tsxweb/src/pages/tasks/TaskCreateDialog.tsxCLAUDE.mddocs/design/brand-and-ux.md
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`).
Applied to files:
web/src/lib/utils.tsweb/src/pages/tasks/TaskCard.tsxweb/src/pages/DashboardPage.tsxweb/src/components/ui/agent-card.tsxweb/src/pages/budget/AgentSpendingTable.tsxweb/src/components/ui/command-palette.tsxweb/src/pages/tasks/TaskListView.tsxweb/src/pages/tasks/TaskCreateDialog.tsxCLAUDE.mdweb/src/components/layout/Sidebar.tsx
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT use `rgba()` with hardcoded values — use design token variables.
Applied to files:
web/src/lib/utils.tsweb/src/components/ui/sparkline.tsxweb/src/components/ui/command-palette.tsxweb/src/pages/tasks/TaskCreateDialog.tsxCLAUDE.mddocs/design/brand-and-ux.md
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT build card-with-header layouts from scratch — use `<SectionCard>` component.
Applied to files:
web/src/components/ui/section-card.tsxweb/src/pages/tasks/TaskCard.tsxweb/src/components/ui/agent-card.tsxweb/src/pages/budget/AgentSpendingTable.tsxweb/src/components/layout/AppLayout.tsxweb/src/pages/budget/CostBreakdownChart.tsxweb/src/components/ui/command-palette.tsxweb/src/pages/tasks/TaskListView.tsxdocs/design/brand-and-ux.mdweb/src/components/layout/Sidebar.tsx
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT render initials circles manually — use `<Avatar>` component.
Applied to files:
web/src/pages/setup/WizardProgress.tsxweb/src/pages/tasks/TaskCard.tsxweb/src/components/ui/agent-card.tsxweb/src/pages/tasks/TaskListView.tsx
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard spacing: use density-aware tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing.
Applied to files:
web/src/pages/tasks/TaskCard.tsxweb/src/pages/budget/AgentSpendingTable.tsxweb/src/components/ui/sparkline.tsxweb/src/components/ui/command-palette.tsxweb/src/components/ui/metric-card.tsxweb/src/pages/tasks/TaskCreateDialog.tsxCLAUDE.mddocs/design/brand-and-ux.md
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard ESLint: enforce zero warnings. All violations must be fixed before merging.
Applied to files:
web/src/__tests__/pages/DashboardPage.test.tsxweb/src/pages/DashboardPage.tsxweb/lighthouserc.jsweb/src/__tests__/integration/ws-dashboard.test.tsx
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to docs/design/*.md : When approved deviations occur, update the relevant `docs/design/` page to reflect the new reality.
Applied to files:
docs/design/page-structure.mddocs/design/ux-guidelines.md
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard: ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones. Refer to the component inventory in CLAUDE.md for the 40+ shared building blocks.
Applied to files:
web/src/pages/DashboardPage.tsxweb/src/pages/TaskDetailPage.tsxweb/e2e/visual/dashboard.spec.tsweb/src/components/ui/command-palette.tsxCLAUDE.mddocs/design/brand-and-ux.mdweb/src/components/layout/Sidebar.tsx
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to web/src/**/*.{ts,tsx} : A PostToolUse hook (`scripts/check_web_design_system.py`) runs automatically on every Edit/Write to `web/src/` files. It catches hardcoded colors, rgba values, font-family, missing Storybook stories, duplicate patterns, and complex `.map()` blocks. Fix all violations before proceeding.
Applied to files:
web/src/pages/TaskDetailPage.tsxweb/vite.config.tsweb/src/hooks/useBreakpoint.tsCLAUDE.mdweb/src/__tests__/hooks/useBreakpoint.test.tsdocs/design/brand-and-ux.md
📚 Learning: 2026-03-19T11:33:01.580Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T11:33:01.580Z
Learning: Applies to web/**/*.vue : Never use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in dashboard code — use generic names: `example-provider`, `example-large-001`, etc.
Applied to files:
web/vite.config.ts
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to tests/**/*.py : Fix flaky tests completely and fundamentally; for timing-sensitive tests, mock `time.monotonic()` and `asyncio.sleep()` to make them deterministic instead of widening timing margins
Applied to files:
web/e2e/fixtures/mock-api.ts
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Pre-push hooks: mypy type-check, pytest unit tests, golangci-lint + go vet + go test (CLI), eslint-web (web dashboard). CI jobs already run these — pre-commit.ci has autoupdate disabled.
Applied to files:
CLAUDE.md
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to .pre-commit-config.yaml : Pre-commit hooks: trailing-whitespace, end-of-file-fixer, check-yaml, check-toml, check-json, check-merge-conflict, check-added-large-files, no-commit-to-branch (main), ruff check+format, gitleaks, hadolint, golangci-lint + go vet (CLI, conditional on `cli/**/*.go`), no-em-dashes, no-redundant-timeout, eslint-web (web dashboard, zero warnings, conditional on `web/src/**/*.{ts,tsx}`).
Applied to files:
CLAUDE.md
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP...
Applied to files:
CLAUDE.md
📚 Learning: 2026-03-15T18:17:43.675Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to pyproject.toml : Dependencies: all versions use == in pyproject.toml. Groups: test (pytest + plugins, hypothesis), dev (includes test + ruff, mypy, pre-commit, commitizen, pip-audit). Required: mem0ai (Mem0 memory backend — the default and currently only backend). Install: uv sync installs everything (dev group is default).
Applied to files:
CLAUDE.md
📚 Learning: 2026-03-30T06:17:45.398Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T06:17:45.398Z
Learning: Applies to pyproject.toml : Dependency groups: `test` (pytest, hypothesis), `dev` (test + ruff, mypy, pre-commit, commitizen, pip-audit). Install with `uv sync`.
Applied to files:
CLAUDE.md
📚 Learning: 2026-03-21T12:54:22.557Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T12:54:22.557Z
Learning: Go 1.26+ required; CLI dependencies in `cli/go.mod` (Cobra, charmbracelet/huh, charmbracelet/lipgloss, sigstore-go, go-containerregistry, go-tuf)
Applied to files:
CLAUDE.md
📚 Learning: 2026-03-15T18:17:43.675Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to docker/{Dockerfile*,compose.yml} : Docker: Backend uses 3-stage build (builder → setup → distroless runtime), Chainguard Python, non-root (UID 65532), CIS-hardened. Web uses nginxinc/nginx-unprivileged, Vue 3 SPA with PrimeVue + Tailwind CSS, SPA routing, API/WebSocket proxy to backend.
Applied to files:
CLAUDE.md
📚 Learning: 2026-03-19T11:19:40.044Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T11:19:40.044Z
Learning: Applies to go.mod : Maintain Go 1.26+ requirement. Dependencies: Cobra (CLI framework), charmbracelet/huh and charmbracelet/lipgloss (UI), sigstore-go (code signing), go-containerregistry (container image verification), go-tuf (TUF client for Sigstore).
Applied to files:
CLAUDE.md
📚 Learning: 2026-03-21T14:12:17.848Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T14:12:17.848Z
Learning: Applies to cli/go.mod : CLI uses Go 1.26+. Dependencies: Cobra, charmbracelet/huh, charmbracelet/lipgloss, sigstore-go, go-containerregistry, go-tuf
Applied to files:
CLAUDE.md
📚 Learning: 2026-03-15T21:32:02.880Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:32:02.880Z
Learning: Applies to cli/**/*.go : Go CLI (Go 1.26+) uses Cobra for commands, charmbracelet/huh for interactive CLI, charmbracelet/lipgloss for styled output. Cross-platform builds (linux/darwin/windows × amd64/arm64). GoReleaser for releases with cosign keyless signing of checksums.txt. SLSA L3 provenance attestations via actions/attest-build-provenance.
Applied to files:
CLAUDE.md
🪛 ast-grep (0.41.1)
web/src/__tests__/hooks/useRovingTabIndex.test.ts
[warning] 17-17: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: document.body.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
| /** | ||
| * Freeze Date.now() for deterministic timestamp rendering. | ||
| */ | ||
| export async function freezeTime(page: Page) { | ||
| await page.addInitScript(() => { | ||
| const fixedTime = new Date('2026-03-29T12:00:00Z').getTime() | ||
| Date.now = () => fixedTime | ||
| }) | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how dates are created in components that might appear in visual tests
rg -n 'new Date\(\)' web/src/ --type ts --type tsx -C 2 | head -40Repository: Aureliolo/synthorg
Length of output: 89
🏁 Script executed:
#!/bin/bash
# Search for new Date() calls with correct file type
rg -n 'new Date\(\)' web/src/ --type ts -C 2 | head -50Repository: Aureliolo/synthorg
Length of output: 3088
🏁 Script executed:
#!/bin/bash
# Also check what test files use freezeTime
rg -n 'freezeTime' web/e2e/ -C 2Repository: Aureliolo/synthorg
Length of output: 1522
🏁 Script executed:
#!/bin/bash
# Check the actual mock-api.ts file content for context
cat -n web/e2e/fixtures/mock-api.ts | head -85Repository: Aureliolo/synthorg
Length of output: 3123
freezeTime only freezes Date.now(), not new Date() without arguments.
The codebase contains multiple new Date() calls in rendered components—notably BudgetBurnChart.tsx:71 which uses new Date().toLocaleDateString()—that will still use the system clock despite the frozen Date.now(). For more complete time control in visual regression tests, also override the Date constructor:
♻️ Freeze both Date.now() and Date constructor
export async function freezeTime(page: Page) {
await page.addInitScript(() => {
const fixedTime = new Date('2026-03-29T12:00:00Z').getTime()
+ const OriginalDate = Date
Date.now = () => fixedTime
+ // `@ts-expect-error` -- overriding Date constructor
+ globalThis.Date = class extends OriginalDate {
+ constructor(...args: Parameters<typeof OriginalDate>) {
+ if (args.length === 0) {
+ super(fixedTime)
+ } else {
+ // `@ts-expect-error` -- spread args to parent
+ super(...args)
+ }
+ }
+ }
+ globalThis.Date.now = () => fixedTime
})
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/e2e/fixtures/mock-api.ts` around lines 71 - 79, The freezeTime helper
currently only overrides Date.now() so calls to new Date() still reflect the
real system time; update freezeTime (the function using page.addInitScript) to
also override the global Date constructor inside the page context: capture the
original Date (e.g., const OriginalDate = Date), compute fixedTime, set Date.now
= () => fixedTime, then replace Date with a function/constructor that delegates
to OriginalDate when called with arguments but returns a new
OriginalDate(fixedTime) when called without arguments, and ensure Date.prototype
and static methods (like toString, parse, UTC) are preserved by assigning
Date.prototype = OriginalDate.prototype and copying needed static properties so
existing code like BudgetBurnChart.tsx that calls new
Date().toLocaleDateString() sees the frozen time.
| test('login page screenshot', async ({ page }) => { | ||
| await page.goto('/login') | ||
| await waitForFonts(page) | ||
| await expect(page).toHaveScreenshot('login.png') | ||
| }) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider waiting for network idle before screenshot.
For more reliable visual regression tests, consider waiting for the page to fully load before capturing the screenshot. This can help avoid flaky tests due to pending network requests or animations.
♻️ Proposed improvement
test('login page screenshot', async ({ page }) => {
await page.goto('/login')
+ await page.waitForLoadState('networkidle')
await waitForFonts(page)
await expect(page).toHaveScreenshot('login.png')
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| test('login page screenshot', async ({ page }) => { | |
| await page.goto('/login') | |
| await waitForFonts(page) | |
| await expect(page).toHaveScreenshot('login.png') | |
| }) | |
| test('login page screenshot', async ({ page }) => { | |
| await page.goto('/login') | |
| await page.waitForLoadState('networkidle') | |
| await waitForFonts(page) | |
| await expect(page).toHaveScreenshot('login.png') | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/e2e/visual/login.spec.ts` around lines 9 - 13, The test 'login page
screenshot' currently captures a screenshot immediately after page.goto and
waitForFonts, which can be flaky; after navigation (page.goto) and before
expect(page).toHaveScreenshot('login.png'), wait for the page to reach a stable
state by calling page.waitForLoadState('networkidle') (or equivalent) so all
network requests finish and animations settle; update the test function (the
test named "login page screenshot") to await
page.waitForLoadState('networkidle') (and keep waitForFonts) before taking the
screenshot to improve reliability.
| projects: [ | ||
| { | ||
| name: 'desktop-chromium', | ||
| use: { ...devices['Desktop Chrome'], viewport: { width: 1280, height: 800 } }, | ||
| }, | ||
| { | ||
| name: 'desktop-sm-chromium', | ||
| use: { ...devices['Desktop Chrome'], viewport: { width: 1024, height: 768 } }, | ||
| }, | ||
| { | ||
| name: 'tablet-chromium', | ||
| use: { ...devices['Desktop Chrome'], viewport: { width: 768, height: 1024 } }, | ||
| }, | ||
| { | ||
| name: 'desktop-firefox', | ||
| use: { ...devices['Desktop Firefox'], viewport: { width: 1280, height: 800 } }, | ||
| }, | ||
| { | ||
| name: 'desktop-webkit', | ||
| use: { ...devices['Desktop Safari'], viewport: { width: 1280, height: 800 } }, | ||
| }, | ||
| ], |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider adding a mobile viewport project for completeness.
The PR includes MobileUnsupportedOverlay and useBreakpoint with mobile detection (<768px), but no mobile viewport project exists in the Playwright config. Adding one would ensure visual regression coverage for the mobile overlay experience.
💡 Add mobile project
{
name: 'desktop-webkit',
use: { ...devices['Desktop Safari'], viewport: { width: 1280, height: 800 } },
},
+ {
+ name: 'mobile-chromium',
+ use: { ...devices['Pixel 5'], viewport: { width: 393, height: 851 } },
+ },
],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| projects: [ | |
| { | |
| name: 'desktop-chromium', | |
| use: { ...devices['Desktop Chrome'], viewport: { width: 1280, height: 800 } }, | |
| }, | |
| { | |
| name: 'desktop-sm-chromium', | |
| use: { ...devices['Desktop Chrome'], viewport: { width: 1024, height: 768 } }, | |
| }, | |
| { | |
| name: 'tablet-chromium', | |
| use: { ...devices['Desktop Chrome'], viewport: { width: 768, height: 1024 } }, | |
| }, | |
| { | |
| name: 'desktop-firefox', | |
| use: { ...devices['Desktop Firefox'], viewport: { width: 1280, height: 800 } }, | |
| }, | |
| { | |
| name: 'desktop-webkit', | |
| use: { ...devices['Desktop Safari'], viewport: { width: 1280, height: 800 } }, | |
| }, | |
| ], | |
| projects: [ | |
| { | |
| name: 'desktop-chromium', | |
| use: { ...devices['Desktop Chrome'], viewport: { width: 1280, height: 800 } }, | |
| }, | |
| { | |
| name: 'desktop-sm-chromium', | |
| use: { ...devices['Desktop Chrome'], viewport: { width: 1024, height: 768 } }, | |
| }, | |
| { | |
| name: 'tablet-chromium', | |
| use: { ...devices['Desktop Chrome'], viewport: { width: 768, height: 1024 } }, | |
| }, | |
| { | |
| name: 'desktop-firefox', | |
| use: { ...devices['Desktop Firefox'], viewport: { width: 1280, height: 800 } }, | |
| }, | |
| { | |
| name: 'desktop-webkit', | |
| use: { ...devices['Desktop Safari'], viewport: { width: 1280, height: 800 } }, | |
| }, | |
| { | |
| name: 'mobile-chromium', | |
| use: { ...devices['Pixel 5'], viewport: { width: 393, height: 851 } }, | |
| }, | |
| ], |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/playwright.config.ts` around lines 26 - 47, Tests are missing a mobile
viewport project, so add a new Playwright project entry in the projects array
(e.g., name 'mobile-chromium') that uses a mobile device or a mobile viewport
(for example devices['Pixel 5'] or viewport { width: 375, height: 812 }) to
ensure MobileUnsupportedOverlay and useBreakpoint (<768px) behaviors are
covered; update the projects array alongside existing entries (desktop-chromium,
desktop-sm-chromium, tablet-chromium, desktop-firefox, desktop-webkit) so CI
runs a mobile scenario for visual/regression testing.
| it('handles null ref gracefully', () => { | ||
| const ref = { current: null } | ||
| const { result } = renderHook(() => useAutoScroll(ref)) | ||
|
|
||
| expect(result.current.isAutoScrolling).toBe(true) | ||
| // Should not throw | ||
| result.current.scrollToBottom() | ||
| }) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Wrap scrollToBottom() call in act() for consistency.
While the null ref case is a no-op, wrapping state-modifying hook methods in act() maintains consistency with other tests and prevents potential warnings if the hook implementation changes.
♻️ Minor consistency fix
it('handles null ref gracefully', () => {
const ref = { current: null }
const { result } = renderHook(() => useAutoScroll(ref))
expect(result.current.isAutoScrolling).toBe(true)
// Should not throw
- result.current.scrollToBottom()
+ act(() => {
+ result.current.scrollToBottom()
+ })
})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/__tests__/hooks/useAutoScroll.test.ts` around lines 80 - 87, The test
for useAutoScroll should wrap the call to result.current.scrollToBottom() in
act() to match other tests and avoid warnings: update the null-ref case in the
test to call act(() => result.current.scrollToBottom()), keeping the existing
assertions (isAutoScrolling) intact; ensure act is imported from your test utils
(the same act used elsewhere in these tests) and reference the useAutoScroll
hook, result.current.scrollToBottom, and isAutoScrolling when making the change.
| describe('useJustUpdated', () => { | ||
| beforeEach(() => { | ||
| vi.useFakeTimers() | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| vi.useRealTimers() | ||
| }) | ||
|
|
||
| it('returns false for unknown IDs', () => { | ||
| const { result } = renderHook(() => useJustUpdated()) | ||
| expect(result.current.isJustUpdated('unknown')).toBe(false) | ||
| }) | ||
|
|
||
| it('marks an ID as just updated after markUpdated', () => { | ||
| const { result } = renderHook(() => useJustUpdated()) | ||
|
|
||
| act(() => { | ||
| result.current.markUpdated('agent-1') | ||
| }) | ||
|
|
||
| expect(result.current.isJustUpdated('agent-1')).toBe(true) | ||
| }) | ||
|
|
||
| it('returns relative time string for updated IDs', () => { | ||
| const { result } = renderHook(() => useJustUpdated()) | ||
|
|
||
| act(() => { | ||
| result.current.markUpdated('task-1') | ||
| }) | ||
|
|
||
| expect(result.current.relativeTime('task-1')).toBe('just now') | ||
| }) | ||
|
|
||
| it('shows seconds after a few seconds', () => { | ||
| const { result } = renderHook(() => useJustUpdated()) | ||
|
|
||
| act(() => { | ||
| result.current.markUpdated('task-1') | ||
| }) | ||
|
|
||
| act(() => { | ||
| vi.advanceTimersByTime(5000) | ||
| }) | ||
|
|
||
| expect(result.current.relativeTime('task-1')).toBe('5s ago') | ||
| }) | ||
|
|
||
| it('clears entry after TTL expires', () => { | ||
| const ttlMs = 10_000 | ||
| const { result } = renderHook(() => useJustUpdated({ ttlMs })) | ||
|
|
||
| act(() => { | ||
| result.current.markUpdated('agent-1') | ||
| }) | ||
| expect(result.current.isJustUpdated('agent-1')).toBe(true) | ||
|
|
||
| act(() => { | ||
| vi.advanceTimersByTime(ttlMs + 100) | ||
| }) | ||
| expect(result.current.isJustUpdated('agent-1')).toBe(false) | ||
| }) | ||
|
|
||
| it('uses default TTL of 30s', () => { | ||
| const { result } = renderHook(() => useJustUpdated()) | ||
|
|
||
| act(() => { | ||
| result.current.markUpdated('id-1') | ||
| }) | ||
|
|
||
| act(() => { | ||
| vi.advanceTimersByTime(29_999) | ||
| }) | ||
| expect(result.current.isJustUpdated('id-1')).toBe(true) | ||
|
|
||
| act(() => { | ||
| vi.advanceTimersByTime(2) | ||
| }) | ||
| expect(result.current.isJustUpdated('id-1')).toBe(false) | ||
| }) | ||
|
|
||
| it('re-marking an ID resets the TTL', () => { | ||
| const ttlMs = 10_000 | ||
| const { result } = renderHook(() => useJustUpdated({ ttlMs })) | ||
|
|
||
| act(() => { | ||
| result.current.markUpdated('id-1') | ||
| }) | ||
|
|
||
| act(() => { | ||
| vi.advanceTimersByTime(8000) | ||
| }) | ||
| expect(result.current.isJustUpdated('id-1')).toBe(true) | ||
|
|
||
| // Re-mark resets the TTL | ||
| act(() => { | ||
| result.current.markUpdated('id-1') | ||
| }) | ||
|
|
||
| act(() => { | ||
| vi.advanceTimersByTime(8000) | ||
| }) | ||
| // Would have expired at 10s from first mark, but re-mark resets | ||
| expect(result.current.isJustUpdated('id-1')).toBe(true) | ||
| }) | ||
|
|
||
| it('tracks multiple IDs independently', () => { | ||
| const { result } = renderHook(() => useJustUpdated({ ttlMs: 10_000 })) | ||
|
|
||
| act(() => { | ||
| result.current.markUpdated('a') | ||
| }) | ||
|
|
||
| act(() => { | ||
| vi.advanceTimersByTime(5000) | ||
| }) | ||
|
|
||
| act(() => { | ||
| result.current.markUpdated('b') | ||
| }) | ||
|
|
||
| act(() => { | ||
| vi.advanceTimersByTime(5100) | ||
| }) | ||
|
|
||
| // 'a' has been around for 10.1s -- expired | ||
| expect(result.current.isJustUpdated('a')).toBe(false) | ||
| // 'b' has been around for 5.1s -- still valid | ||
| expect(result.current.isJustUpdated('b')).toBe(true) | ||
| }) | ||
|
|
||
| it('returns null relative time for unknown IDs', () => { | ||
| const { result } = renderHook(() => useJustUpdated()) | ||
| expect(result.current.relativeTime('unknown')).toBeNull() | ||
| }) | ||
|
|
||
| it('clearAll removes all tracked IDs', () => { | ||
| const { result } = renderHook(() => useJustUpdated()) | ||
|
|
||
| act(() => { | ||
| result.current.markUpdated('a') | ||
| result.current.markUpdated('b') | ||
| }) | ||
|
|
||
| act(() => { | ||
| result.current.clearAll() | ||
| }) | ||
|
|
||
| expect(result.current.isJustUpdated('a')).toBe(false) | ||
| expect(result.current.isJustUpdated('b')).toBe(false) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify this file contains required fast-check property assertions
rg -n --type=ts "fc\\.(assert|property)" web/src/__tests__/hooks/useJustUpdated.test.tsRepository: Aureliolo/synthorg
Length of output: 44
Add property-based tests using fast-check for hook invariants.
This test suite uses only example-based tests; coding guidelines require fast-check properties (e.g., TTL boundary invariants) per web/src/**/*.test.{ts,tsx} standards.
Suggested addition
+import fc from 'fast-check'
+
+it('property: isJustUpdated(id) is true iff elapsed < ttlMs', () => {
+ const ttlMs = 10_000
+ const { result } = renderHook(() => useJustUpdated({ ttlMs }))
+
+ fc.assert(
+ fc.property(fc.integer({ min: 0, max: 20_000 }), (elapsed) => {
+ act(() => {
+ result.current.clearAll()
+ result.current.markUpdated('prop-id')
+ vi.advanceTimersByTime(elapsed)
+ })
+ expect(result.current.isJustUpdated('prop-id')).toBe(elapsed < ttlMs)
+ }),
+ )
+})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/__tests__/hooks/useJustUpdated.test.ts` around lines 4 - 155, Add
property-based tests using fast-check for the useJustUpdated hook to satisfy the
repository standard: import fast-check in
web/src/__tests__/hooks/useJustUpdated.test.ts and add fc.property-based tests
that (1) assert TTL boundary invariants for markUpdated + isJustUpdated (given
random ttlMs and elapsed times, isJustUpdated should be true iff elapsed <
ttlMs), (2) assert re-mark resets TTL (markUpdated twice with random intervals
yields still-true if second mark + subsequent elapsed < ttlMs), and (3) assert
independent tracking for multiple IDs (generate arrays of timestamps/IDs and
ensure one ID expiring does not affect others). Use renderHook(() =>
useJustUpdated({ ttlMs })) and vi.useFakeTimers() / vi.advanceTimersByTime
within the property body, referencing markUpdated, isJustUpdated, relativeTime,
and clearAll to drive assertions; fail the property when any invariant is
violated.
| const numericValue = typeof value === 'number' ? value : undefined | ||
| const animatedValue = useCountAnimation(numericValue ?? 0) | ||
| const displayValue = animateValue && numericValue !== undefined ? animatedValue : value |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Minor inefficiency: animation runs even when animateValue=false.
The hook is always invoked (correctly, per Rules of Hooks), but the RAF loop still executes when numericValue changes even though the animated result is discarded. Consider adding an enabled option to useCountAnimation to skip the animation loop entirely.
💡 Proposed hook enhancement
In useCountAnimation.ts:
interface UseCountAnimationOptions {
durationMs?: number
+ /** Skip animation and return target immediately. Default: true. */
+ enabled?: boolean
}Then in MetricCard:
-const animatedValue = useCountAnimation(numericValue ?? 0)
+const animatedValue = useCountAnimation(numericValue ?? 0, { enabled: animateValue })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/components/ui/metric-card.tsx` around lines 30 - 32, The count
animation hook is always running even when animateValue is false; update
useCountAnimation to accept an options object (e.g., { enabled?: boolean }) and
short-circuit the RAF loop when enabled is false, then call
useCountAnimation(numericValue ?? 0, { enabled: animateValue && numericValue !==
undefined }) from MetricCard (references: useCountAnimation, numericValue,
animateValue, displayValue, MetricCard) so the hook does nothing and does not
start the RAF loop when animation is disabled.
| // Hard redirect to login -- intentionally uses window.location (not | ||
| // react-router) because this runs in a Zustand store outside the | ||
| // React tree. Saves return path so LoginPage can redirect back | ||
| // after re-authentication (sessionStorage is same-origin only). | ||
| // React tree. | ||
| const currentPath = window.location.pathname | ||
| if (currentPath !== '/login' && currentPath !== '/setup') { | ||
| sessionStorage.setItem('auth_return_path', currentPath) | ||
| window.location.href = '/login' | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if return path is handled elsewhere (e.g., in login page or router)
rg -n 'auth_return_path|returnPath|return_path' web/src --type ts --type tsx -C 2 || rg -n 'auth_return_path|returnPath|return_path' web/src -C 2Repository: Aureliolo/synthorg
Length of output: 89
🏁 Script executed:
# First, let's search for return path handling using correct syntax
rg 'auth_return_path|returnPath|return_path' web/src -C 2Repository: Aureliolo/synthorg
Length of output: 44
🏁 Script executed:
# Also, let's examine the auth.ts file to see the current clearAuth() function
cat -n web/src/stores/auth.ts | sed -n '145,160p'Repository: Aureliolo/synthorg
Length of output: 891
🏁 Script executed:
# Search for login-related files that might handle return path
fd -e ts -e tsx web/src | grep -i loginRepository: Aureliolo/synthorg
Length of output: 395
🏁 Script executed:
# Search entire repo (not just web/src) for return path handling
rg 'auth_return_path|returnPath|return_path' --type-list | head -5Repository: Aureliolo/synthorg
Length of output: 451
🏁 Script executed:
# Look for login page or auth-related pages
fd . web/src -type f -name '*login*' -o -name '*auth*'Repository: Aureliolo/synthorg
Length of output: 232
🏁 Script executed:
# Check git history for when return path was removed
git log --oneline -S 'auth_return_path' -- web/src 2>/dev/null | head -10Repository: Aureliolo/synthorg
Length of output: 44
🏁 Script executed:
# Search for any redirect logic in the codebase
rg 'window.location.href.*login|redirectTo|redirect.*login' web/src -C 2Repository: Aureliolo/synthorg
Length of output: 2905
Return path preservation is not currently implemented—verify if this is intentional.
No return-path persistence exists in the codebase (verified via search across web/src). Users are always redirected to /login after session timeout or auth failure, rather than returning to their previous page.
If this simplicity is intentional (e.g., for Playwright test reliability), no action is needed. If better UX is desired, consider storing and restoring the return path via the login page or router.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/stores/auth.ts` around lines 150 - 156, The redirect block using
currentPath and window.location.href always sends users to '/login' and drops
the original path; update it to preserve a return path by capturing the encoded
currentPath (unless it's '/login' or '/setup'), then redirect to
'/login?next=<encodedPath>' or write it to a short-lived key in
localStorage/sessionStorage before setting window.location.href; also ensure the
login completion flow (login handler/component) reads that query param or
storage key and navigates back to the preserved path after successful auth.
- useRovingTabIndex: clamp focusedIndex when items shrink below it - useJustUpdated: relativeTime returns null for expired entries (consistent with isJustUpdated) - useJustUpdated: validate ttlMs (finite positive number, fallback to default) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
🤖 I have created a release *beep* *boop* --- #MAJOR CHANGES; We got a somewhat working webui :) ## [0.5.0](v0.4.9...v0.5.0) (2026-03-30) ### Features * add analytics trends and budget forecast API endpoints ([#798](#798)) ([16b61f5](16b61f5)) * add department policies to default templates ([#852](#852)) ([7a41548](7a41548)) * add remaining activity event types (task_started, tool_used, delegation, cost_incurred) ([#832](#832)) ([4252fac](4252fac)) * agent performance, activity, and history API endpoints ([#811](#811)) ([9b75c1d](9b75c1d)) * Agent Profiles and Detail pages (biography, career, performance) ([#874](#874)) ([62d7880](62d7880)) * app shell, Storybook, and CI/CD pipeline ([#819](#819)) ([d4dde90](d4dde90)) * Approvals page with risk grouping, urgency indicators, batch actions ([#889](#889)) ([4e9673d](4e9673d)) * Budget Panel page (P&L dashboard, breakdown charts, forecast) ([#890](#890)) ([b63b0f1](b63b0f1)) * build infrastructure layer (API client, auth, WebSocket) ([#815](#815)) ([9f01d3e](9f01d3e)) * CLI global options infrastructure, UI modes, exit codes, env vars ([#891](#891)) ([fef4fc5](fef4fc5)) * CodeMirror editor and theme preferences toggle ([#905](#905), [#807](#807)) ([#909](#909)) ([41fbedc](41fbedc)) * Company page (department/agent management) ([#888](#888)) ([cfb88b0](cfb88b0)) * comprehensive hint coverage across all CLI commands ([#900](#900)) ([937974e](937974e)) * config system extensions, per-command flags for init/start/stop/status/logs ([#895](#895)) ([32f83fe](32f83fe)) * configurable currency system replacing hardcoded USD ([#854](#854)) ([b372551](b372551)) * Dashboard page (metric cards, activity feed, budget burn) ([#861](#861)) ([7d519d5](7d519d5)) * department health, provider status, and activity feed endpoints ([#818](#818)) ([6d5f196](6d5f196)) * design tokens and core UI components ([#833](#833)) ([ed887f2](ed887f2)) * extend approval, meeting, and budget API responses ([#834](#834)) ([31472bf](31472bf)) * frontend polish -- real-time UX, accessibility, responsive, performance ([#790](#790), [#792](#792), [#791](#791), [#793](#793)) ([#917](#917)) ([f04a537](f04a537)) * implement human roles and access control levels ([#856](#856)) ([d6d8a06](d6d8a06)) * implement semantic conflict detection in workspace merge ([#860](#860)) ([d97283b](d97283b)) * interaction components and animation patterns ([#853](#853)) ([82d4b01](82d4b01)) * Login page + first-run bootstrap + Company page ([#789](#789), [#888](#888)) ([#896](#896)) ([8758e8d](8758e8d)) * Meetings page with timeline viz, token bars, contribution formatting ([#788](#788)) ([#904](#904)) ([b207f46](b207f46)) * Messages page with threading, channel badges, sender indicators ([#787](#787)) ([#903](#903)) ([28293ad](28293ad)) * Org Chart force-directed view and drag-drop reassignment ([#872](#872), [#873](#873)) ([#912](#912)) ([a68a938](a68a938)) * Org Chart page (living nodes, status, CRUD, department health) ([#870](#870)) ([0acbdae](0acbdae)) * per-command flags for remaining commands, auto-behavior wiring, help/discoverability ([#897](#897)) ([3f7afa2](3f7afa2)) * Providers page with backend rework -- health, CRUD, subscription auth ([#893](#893)) ([9f8dd98](9f8dd98)) * scaffold React + Vite + TypeScript + Tailwind project ([#799](#799)) ([bd151aa](bd151aa)) * Settings page with search, dependency indicators, grouped rendering ([#784](#784)) ([#902](#902)) ([a7b9870](a7b9870)) * Setup Wizard rebuild with template comparison, cost estimator, theme customization ([#879](#879)) ([ae8b50b](ae8b50b)) * setup wizard UX -- template filters, card metadata, provider form reuse ([#910](#910)) ([7f04676](7f04676)) * setup wizard UX overhaul -- mode choice, step reorder, provider fixes ([#907](#907)) ([ee964c4](ee964c4)) * structured ModelRequirement in template agent configs ([#795](#795)) ([7433548](7433548)) * Task Board page (rich Kanban, filtering, dependency viz) ([#871](#871)) ([04a19b0](04a19b0)) ### Bug Fixes * align frontend types with backend and debounce WS refetches ([#916](#916)) ([134c11b](134c11b)) * auto-cleanup targets newly pulled images instead of old ones ([#884](#884)) ([50e6591](50e6591)) * correct wipe backup-skip flow and harden error handling ([#808](#808)) ([c05860f](c05860f)) * improve provider setup in wizard, subscription auth, dashboard bugs ([#914](#914)) ([87bf8e6](87bf8e6)) * improve update channel detection and add config get command ([#814](#814)) ([6b137f0](6b137f0)) * resolve all ESLint warnings, add zero-warnings enforcement ([#899](#899)) ([079b46a](079b46a)) * subscription auth uses api_key, base URL optional for cloud providers ([#915](#915)) ([f0098dd](f0098dd)) ### Refactoring * semantic analyzer cleanup -- shared filtering, concurrency, extraction ([#908](#908)) ([81372bf](81372bf)) ### Documentation * brand identity and UX design system from [#765](#765) exploration ([#804](#804)) ([389a9f4](389a9f4)) * page structure and information architecture for v0.5.0 dashboard ([#809](#809)) ([f8d6d4a](f8d6d4a)) * write UX design guidelines with WCAG-verified color system ([#816](#816)) ([4a4594e](4a4594e)) ### Tests * add unit tests for agent hooks and page components ([#875](#875)) ([#901](#901)) ([1d81546](1d81546)) ### CI/CD * bump actions/deploy-pages from 4.0.5 to 5.0.0 in the major group ([#831](#831)) ([01c19de](01c19de)) * bump astral-sh/setup-uv from 7.6.0 to 8.0.0 in /.github/actions/setup-python-uv in the all group ([#920](#920)) ([5f6ba54](5f6ba54)) * bump codecov/codecov-action from 5.5.3 to 6.0.0 in the major group ([#868](#868)) ([f22a181](f22a181)) * bump github/codeql-action from 4.34.1 to 4.35.0 in the all group ([#883](#883)) ([87a4890](87a4890)) * bump sigstore/cosign-installer from 4.1.0 to 4.1.1 in the minor-and-patch group ([#830](#830)) ([7a69050](7a69050)) * bump the all group with 3 updates ([#923](#923)) ([ff27c8e](ff27c8e)) * bump wrangler from 4.76.0 to 4.77.0 in /.github in the minor-and-patch group ([#822](#822)) ([07d43eb](07d43eb)) * bump wrangler from 4.77.0 to 4.78.0 in /.github in the all group ([#882](#882)) ([f84118d](f84118d)) ### Maintenance * add design system enforcement hook and component inventory ([#846](#846)) ([15abc43](15abc43)) * add dev-only auth bypass for frontend testing ([#885](#885)) ([6cdcd8a](6cdcd8a)) * add pre-push rebase check hook ([#855](#855)) ([b637a04](b637a04)) * backend hardening -- eviction/size-caps and model validation ([#911](#911)) ([81253d9](81253d9)) * bump axios from 1.13.6 to 1.14.0 in /web in the all group across 1 directory ([#922](#922)) ([b1b0232](b1b0232)) * bump brace-expansion from 5.0.4 to 5.0.5 in /web ([#862](#862)) ([ba4a565](ba4a565)) * bump eslint-plugin-react-refresh from 0.4.26 to 0.5.2 in /web ([#801](#801)) ([7574bb5](7574bb5)) * bump faker from 40.11.0 to 40.11.1 in the minor-and-patch group ([#803](#803)) ([14d322e](14d322e)) * bump https://github.com/astral-sh/ruff-pre-commit from v0.15.7 to 0.15.8 ([#864](#864)) ([f52901e](f52901e)) * bump nginxinc/nginx-unprivileged from `6582a34` to `f99cc61` in /docker/web in the all group ([#919](#919)) ([df85e4f](df85e4f)) * bump nginxinc/nginx-unprivileged from `ccbac1a` to `6582a34` in /docker/web ([#800](#800)) ([f4e9450](f4e9450)) * bump node from `44bcbf4` to `71be405` in /docker/sandbox ([#827](#827)) ([91bec67](91bec67)) * bump node from `5209bca` to `cf38e1f` in /docker/web ([#863](#863)) ([66d6043](66d6043)) * bump picomatch in /site ([#842](#842)) ([5f20bcc](5f20bcc)) * bump recharts 2->3 and @types/node 22->25 in /web ([#802](#802)) ([a908800](a908800)) * Bump requests from 2.32.5 to 2.33.0 ([#843](#843)) ([41daf69](41daf69)) * bump smol-toml from 1.6.0 to 1.6.1 in /site ([#826](#826)) ([3e5dbe4](3e5dbe4)) * bump the all group with 3 updates ([#921](#921)) ([7bace0b](7bace0b)) * bump the minor-and-patch group across 1 directory with 2 updates ([#829](#829)) ([93e611f](93e611f)) * bump the minor-and-patch group across 1 directory with 3 updates ([#841](#841)) ([7010c8e](7010c8e)) * bump the minor-and-patch group across 1 directory with 3 updates ([#869](#869)) ([548cee5](548cee5)) * bump the minor-and-patch group in /site with 2 updates ([#865](#865)) ([9558101](9558101)) * bump the minor-and-patch group with 2 updates ([#867](#867)) ([4830706](4830706)) * consolidate Dependabot groups to 1 PR per ecosystem ([06d2556](06d2556)) * consolidate Dependabot groups to 1 PR per ecosystem ([#881](#881)) ([06d2556](06d2556)) * improve worktree skill with full dep sync and status enhancements ([#906](#906)) ([772c625](772c625)) * remove Vue remnants and document framework decision ([#851](#851)) ([bf2adf6](bf2adf6)) * update web dependencies and fix brace-expansion CVE ([#880](#880)) ([a7a0ed6](a7a0ed6)) * upgrade to Storybook 10 and TypeScript 6 ([#845](#845)) ([52d95f2](52d95f2)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Summary
Final v0.5.0 polish pass implementing the 4 remaining frontend issues:
useRovingTabIndexhook for arrow-key navigation,LiveRegioncomponent with debounced ARIA announcements,FOCUS_RINGconstant used across all focus indicators (10 files standardized), component ARIA improvements (StatusBadge/TaskStatusIndicatorrole="status", SectionCard semantic<section>+aria-labelledby, PriorityBadgearia-label, Sparkline optionalariaLabel, EmptyStaterole="status"), chart accessibility (role="img"+aria-label), sidebar badgearia-live, focus ring standardization to 2px/accent/offset-2useBreakpointhook with matchMedia listeners, Sidebar refactored for responsive modes (auto-collapse to rail at desktop-sm, overlay drawer with FocusScope + Escape dismiss at tablet, hidden at mobile),MobileUnsupportedOverlayfor <768px, command palette responsive width, hamburger button in StatusBar witharia-expandedLazyCodeMirrorEditorfor deferred ~200KB+ CodeMirror bundle, redundant @xyflow CSS import removed, Playwright visual regression setup (5 browser/viewport projects), Lighthouse CI config (target 90+), bundle analysis toolingNew hooks
useJustUpdated-- track recently-updated entity IDs with relative time stringsuseAutoScroll-- auto-scroll container with user scroll-away detectionuseCountAnimation-- rAF-driven animated numeric transitionsuseRovingTabIndex-- WAI-ARIA roving tabindex for grid/list keyboard navigationuseBreakpoint-- reactive viewport breakpoint detection via matchMediaNew components
LiveRegion-- debounced ARIA live region wrapperMobileUnsupportedOverlay-- full-screen mobile viewport blockerLazyCodeMirrorEditor-- Suspense-wrapped lazy CodeMirror loaderTest plan
npm --prefix web run lint-- zero warningsnpm --prefix web run type-check-- no errorsnpm --prefix web run test-- 179 files, 2129 tests passnpm --prefix web run build-- production build with vendor chunk splitting verifiednpm --prefix web run storybook:buildPre-reviewed by 3 agents (docs-consistency, frontend-reviewer, issue-resolution-verifier), 9 findings addressed.
Closes #790
Closes #792
Closes #791
Closes #793
🤖 Generated with Claude Code