feat: Approvals page with risk grouping, urgency indicators, batch actions#889
feat: Approvals page with risk grouping, urgency indicators, batch actions#889
Conversation
WalkthroughImplements a full Approvals feature: a Zustand store with list/detail fetching, optimistic transitions, selection and batch operations, WebSocket handling, and request-sequence guards. Adds a polling-and-WS-connected 🚥 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 Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Snapshot WarningsEnsure that dependencies are being submitted on PR branches. Re-running this action after a short time may resolve the issue. See the documentation for more information and troubleshooting advice. Scanned FilesNone |
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive Approvals management system, featuring a dedicated Zustand store, a custom hook for real-time data synchronization via WebSockets and polling, and a feature-rich UI for filtering and batch-processing approval requests. Key feedback includes addressing the hardcoded 200-item limit through pagination, correcting a state synchronization anti-pattern in the ApprovalCard component, and improving store encapsulation by moving module-level variables into the Zustand state. Further suggestions focus on strengthening WebSocket payload validation, fixing incomplete useEffect dependency arrays, and adopting more declarative patterns for URL parameter updates and array copying.
| useEffect(() => { | ||
| useApprovalsStore.getState().fetchApprovals({ limit: 200 }) | ||
| }, []) | ||
|
|
||
| // Lightweight polling for approval refresh | ||
| const pollFn = useCallback(async () => { | ||
| await useApprovalsStore.getState().fetchApprovals({ limit: 200 }) | ||
| }, []) |
There was a problem hiding this comment.
Both the initial data fetch and the polling function are hardcoded to fetch a maximum of 200 approvals. If the number of approvals exceeds this limit, the UI will not display all of them, and the user will have no way to access the rest. This could lead to users missing important pending approvals.
Consider implementing pagination to handle larger datasets. The API seems to support offset and limit parameters, which should be leveraged here.
| const prevSecondsRef = useRef(approval.seconds_remaining) | ||
| if (approval.seconds_remaining !== prevSecondsRef.current) { | ||
| prevSecondsRef.current = approval.seconds_remaining | ||
| setCountdown(approval.seconds_remaining) | ||
| } |
There was a problem hiding this comment.
Calling setCountdown directly in the component body is an anti-pattern that can lead to unexpected re-renders and bugs. State synchronization with props should be done within a useEffect hook.
This ensures that the component's state is updated predictably in response to prop changes, aligning with React's lifecycle.
useEffect(() => {
setCountdown(approval.seconds_remaining);
}, [approval.seconds_remaining]);
| const pendingTransitions = new Set<string>() | ||
|
|
||
| /** Clear module-level pendingTransitions -- test-only. */ | ||
| export function _resetPendingTransitions(): void { | ||
| pendingTransitions.clear() | ||
| } |
There was a problem hiding this comment.
Using a module-level variable (pendingTransitions) for state that is also part of the Zustand store breaks encapsulation and can lead to subtle bugs, especially in testing or if the app is ever server-rendered. State should be fully contained within the store.
The _resetPendingTransitions export for tests is a strong indicator that this pattern is problematic.
To fix this, initialize pendingTransitions inside the create call and update it immutably. For example: set(state => ({ pendingTransitions: new Set(state.pendingTransitions).add(id) })).
| if (payload.approval && typeof payload.approval === 'object' && !Array.isArray(payload.approval)) { | ||
| const candidate = payload.approval as Record<string, unknown> | ||
| if ( | ||
| typeof candidate.id === 'string' && | ||
| typeof candidate.status === 'string' && | ||
| typeof candidate.title === 'string' && | ||
| typeof candidate.risk_level === 'string' && | ||
| typeof candidate.action_type === 'string' | ||
| ) { | ||
| if (pendingTransitions.has(candidate.id)) return | ||
| get().upsertApproval(candidate as unknown as ApprovalResponse) | ||
| } else { | ||
| console.error('[approvals/ws] Received malformed approval payload, skipping upsert', { | ||
| id: candidate.id, | ||
| hasTitle: typeof candidate.title === 'string', | ||
| hasStatus: typeof candidate.status === 'string', | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
The validation for incoming WebSocket payloads is minimal, only checking for the existence and string type of five properties before casting to ApprovalResponse. This is risky as a partially malformed payload could still pass validation, leading to incomplete objects in the store and potential runtime errors downstream (e.g., cannot read property '...' of undefined).
To make this more robust, consider using a validation library like Zod to parse and validate the payload against the full ApprovalResponse schema. This ensures type safety and data integrity.
| metadata: {}, | ||
| decided_by: null, | ||
| decision_reason: null, | ||
| created_at: '2026-03-27T10:00:00Z', |
There was a problem hiding this comment.
The hardcoded future date 2026-03-27T10:00:00Z might cause unexpected behavior in tests, especially those involving time-sensitive logic like expiration. Consider using a more robust approach, such as a fixed date from the past or generating it dynamically and mocking timers if needed, to make tests more predictable and less prone to future failures.
| setSearchParams((prev) => { | ||
| const next = new URLSearchParams(prev) | ||
| // Preserve selected | ||
| const sel = next.get('selected') | ||
| // Clear old filter params | ||
| next.delete('status') | ||
| next.delete('risk') | ||
| next.delete('type') | ||
| next.delete('search') | ||
| // Set new ones | ||
| if (newFilters.status) next.set('status', newFilters.status) | ||
| if (newFilters.riskLevel) next.set('risk', newFilters.riskLevel) | ||
| if (newFilters.actionType) next.set('type', newFilters.actionType) | ||
| if (newFilters.search) next.set('search', newFilters.search) | ||
| if (sel) next.set('selected', sel) | ||
| return next | ||
| }) |
There was a problem hiding this comment.
The logic for updating search parameters can be simplified. Instead of creating a mutable copy, deleting old keys, and then conditionally setting new ones, you can construct a new URLSearchParams object from the new filters and then add any parameters you want to preserve (like selected). This approach is more declarative and less error-prone.
setSearchParams((prev) => {
const newSearch = new URLSearchParams();
if (newFilters.status) newSearch.set('status', newFilters.status);
if (newFilters.riskLevel) newSearch.set('risk', newFilters.riskLevel);
if (newFilters.actionType) newSearch.set('type', newFilters.actionType);
if (newFilters.search) newSearch.set('search', newFilters.search);
const selected = prev.get('selected');
if (selected) newSearch.set('selected', selected);
return newSearch;
});
web/src/pages/ApprovalsPage.tsx
Outdated
| if (selectedId && !selectedApproval) { | ||
| fetchApproval(selectedId) | ||
| } | ||
| }, [selectedId]) // eslint-disable-line @eslint-react/exhaustive-deps |
There was a problem hiding this comment.
The dependency array for this useEffect is incomplete, which is masked by the eslint-disable-line comment. It should include selectedApproval and fetchApproval since they are used inside the effect. Omitting them can lead to stale closures and bugs where the effect doesn't re-run when it should.
While fetchApproval is likely stable, selectedApproval is not. If selectedApproval becomes null for any reason while selectedId remains, this effect will not re-trigger a fetch.
| }, [selectedId]) // eslint-disable-line @eslint-react/exhaustive-deps | |
| }, [selectedId, selectedApproval, fetchApproval]) |
web/src/utils/approvals.ts
Outdated
| approvals: readonly ApprovalResponse[], | ||
| filters: ApprovalPageFilters, | ||
| ): ApprovalResponse[] { | ||
| let result = approvals as ApprovalResponse[] |
There was a problem hiding this comment.
There was a problem hiding this comment.
Actionable comments posted: 24
🤖 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/src/__tests__/utils/approvals.test.ts`:
- Around line 17-37: The test file defines a local makeApproval factory that
duplicates the shared factory; remove the local makeApproval and import the
shared factory from web/src/__tests__/helpers/factories.ts instead. Replace uses
of the local makeApproval in this file with the imported makeApproval (which
returns an ApprovalResponse and already defaults id to 'apr-1'), ensure the
import statement is added at top and delete the local function definition to
avoid duplication.
In `@web/src/hooks/useApprovalsData.ts`:
- Around line 59-66: The hook currently hard-codes fetchApprovals({ limit: 200
}) in both the initial useEffect and pollFn, causing approvals beyond 200 to be
silently dropped; update useApprovalsData (the effect and the pollFn) to stop
using a fixed 200 limit and instead either accept a configurable limit/cursor
param or call the store API to fetch all pages and merge results: modify calls
to useApprovalsStore.getState().fetchApprovals to pass through a limit/cursor
from the hook props or state (or omit limit to mean "fetch all"), add a
fetchMore/paginate method and an isTruncated/hasMore flag to the hook return so
the UI can request additional pages, and ensure pollFn uses the same
params/merge logic so repeated polling doesn't overwrite paginated results.
In `@web/src/pages/approvals/ApprovalCard.tsx`:
- Around line 19-31: The approval card currently defines DOT_COLOR_CLASSES and
URGENCY_BADGE_CLASSES and renders an inline dot for risk state; replace that
bespoke dot implementation by using the shared StatusBadge component instead:
remove/stop using DOT_COLOR_CLASSES and the inline dot markup in ApprovalCard
(and update any usages around lines referenced 101-109) and render <StatusBadge>
with the appropriate variant/level mapped from the existing urgency/risk prop so
styling comes from the shared primitive; keep URGENCY_BADGE_CLASSES only if
still needed for non-dot badge styling, otherwise map the same urgency keys to
StatusBadge props (e.g., "danger"/"warning"/"accent"/"accent-dim") so the
component uses the central status styles.
- Around line 64-73: The effect currently starts a ticking interval whenever
countdown > 0 but doesn't stop when the card's status changes from "pending",
causing hidden timers to keep running; update the useEffect in ApprovalCard (the
effect that references countdown, setCountdown, clearInterval) to also check the
card status (e.g., status === 'pending') before creating the interval and
include status in the dependency array so the interval is cleared when status
changes; alternatively, when status transitions off pending, clear the interval
and setCountdown(null) inside the same effect to ensure no timer continues for
non-pending cards.
- Around line 128-138: The badge is currently hidden from assistive tech via
aria-hidden="true", which removes all urgency info; instead expose a non-live,
static accessible name: remove aria-hidden and add an aria-label that conveys
the urgency without reflecting the live countdown (e.g., using the existing
urgencyColor value to produce a static label like `${urgencyColor} urgency`);
update the span in ApprovalCard (the element using isPending, countdown,
URGENCY_BADGE_CLASSES, formatUrgency, and Clock) to include this aria-label so
screen readers get the urgency cue but the element remains non-live.
In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx`:
- Line 142: The drawer currently returns null when either open is false or
approval is null (the check in ApprovalDetailDrawer: "if (!open || !approval)
return null"), which prevents rendering the drawer shell and makes the initial
loading spinner unreachable; change the logic so the component only returns null
when open is false, but when open is true render the drawer shell and
conditionally render the approval-dependent header/body (and spinner) based on
whether approval is present; also update the other guarded block around the
approval content (the section referenced at lines 194-200) to show loading
UI/placeholders when approval is null while keeping the drawer mounted.
- Around line 208-210: In ApprovalDetailDrawer, several read-only section titles
currently use <label> (e.g., the "Description" element with className
"text-[11px] font-semibold uppercase tracking-wider text-text-muted"); replace
those <label> elements with an appropriate non-form semantic element such as
<h3> or <span> (keeping the existing className for styling) so they don’t convey
form semantics or require htmlFor; update the other identical occurrences in
this component (the other section headings noted in the review) to the same
pattern.
- Line 159: The drawer component (ApprovalDetailDrawer) is using hardcoded
utility classes like "shadow-lg" and "min-h-[60px]" which bypass design tokens;
replace "shadow-lg" on the panel container with the design-system shadow token
(use the shadow token via CSS var, e.g. a tailwind arbitrary value referencing
var(--so-shadow-card-hover) or the project's shadow token class) and replace any
hardcoded min-h pixel values on the textarea with density-aware spacing tokens
(use p-card / gap-section-gap / a min-height based on a CSS token such as
min-h-[var(--p-card)] or the approved token class). Apply the same replacements
to the other occurrences noted (the other container/textarea className strings
around the indicated ranges) so all shadows and spacing use the shared token
variables instead of hardcoded values.
- Around line 67-78: The Escape handler never fires because the document query
also matches the drawer itself; update the guard in the useEffect/handleKeyDown
so it only blocks closing when a different modal/confirm dialog is open.
Specifically, reference the local confirm-dialog state (e.g., confirmDialogOpen)
or inspect document.querySelector for modal elements and ensure you exclude the
drawer by checking panelRef.current (e.g., skip if foundModalElement exists AND
panelRef.current?.contains(foundModalElement) is false) before returning; keep
the handler name handleKeyDown and still call onClose() when appropriate.
In `@web/src/pages/approvals/ApprovalFilterBar.stories.tsx`:
- Around line 6-9: The story meta for ApprovalFilterBar is missing the
accessibility test gate; update the meta object (const meta: Meta<typeof
ApprovalFilterBar>) to include a parameters property that enables the a11y test
(add parameters: { a11y: { test: true } }) so Storybook 10 will run the WCAG
accessibility check for this story.
In `@web/src/pages/approvals/ApprovalFilterBar.tsx`:
- Around line 5-6: STATUSES and RISK_LEVELS are hardcoded and can drift from the
API union types ApprovalStatus and ApprovalRiskLevel; update the module to
derive these lists from the canonical source or make them type-safe by either
(a) importing a single exported constant/enum from the API surface and using
that as the source of truth, or (b) keep the arrays but add a TypeScript
constraint (e.g., use `as const` plus `satisfies ReadonlyArray<ApprovalStatus>`
/ `satisfies ReadonlyArray<ApprovalRiskLevel>`) so the compiler will error if
they diverge from ApprovalStatus/ApprovalRiskLevel; locate and modify the
STATUSES and RISK_LEVELS declarations to implement one of these approaches.
- Around line 86-88: In ApprovalFilterBar, replace the incorrect duplicated
semantic token class names: change any className occurrences of
"text-text-muted" (e.g., on the span displaying "{pendingCount} pending /
{totalCount} total") to "text-muted-foreground", and change
"text-text-secondary" to "text-secondary" (this token is provided via the `@theme`
inline that defines --color-text-secondary); search for these exact strings in
ApprovalFilterBar and update them so the components use the correct semantic
token names.
In `@web/src/pages/approvals/ApprovalsSkeleton.stories.tsx`:
- Around line 4-7: The Storybook meta for ApprovalsSkeleton (const meta:
Meta<typeof ApprovalsSkeleton>) is missing the required Storybook 10
accessibility gate; add a parameters.a11y.test property to the meta object
(e.g., parameters: { a11y: { test: true } } or the project-specific test config)
so the WCAG compliance check runs; apply the same change to the other three
story files (ApprovalTimeline.stories.tsx, ApprovalDetailDrawer.stories.tsx,
ApprovalCard.stories.tsx) by updating each file's meta object to include
parameters.a11y.test.
In `@web/src/pages/approvals/ApprovalTimeline.tsx`:
- Around line 63-105: The map body rendering each step is too large and should
be extracted into a new TimelineStep component: create a TimelineStep functional
component that accepts props (step, idx, isLast, approvalStatus) and moves the
JSX currently inside steps.map into it, keeping the same className logic that
uses DOT_CLASSES[step.state], LINE_CLASSES[step.state],
OUTCOME_CLASSES[getApprovalStatusColor(approvalStatus)], the timestamp
formatting via formatDate(step.timestamp), and the role="listitem" on the root
element; then replace the inline map body in ApprovalTimeline.tsx with
steps.map((step, idx) => <TimelineStep key={step.label} step={step} idx={idx}
isLast={idx === steps.length - 1} approvalStatus={approval.status} />) so all
styling and accessibility logic remains identical but isolated for testing and
maintenance.
In `@web/src/pages/approvals/BatchActionBar.stories.tsx`:
- Around line 5-14: The Storybook meta for BatchActionBar is missing the
accessibility enforcement parameter; update the exported "meta" object
(Meta<typeof BatchActionBar>) to include a parameters.a11y.test entry set to
'error' so Storybook will fail on WCAG violations (add parameters: { a11y: {
test: 'error' } } alongside args/decorators in the meta).
In `@web/src/pages/approvals/BatchActionBar.tsx`:
- Line 35: The action bar in the BatchActionBar component is using the reserved
utility bg-base; change the container's className to use bg-surface instead
while keeping all other classes (flex items-center gap-3 rounded-lg border
border-border px-4 py-2.5 shadow-lg) intact so the visual elevation and spacing
remain consistent with other small elevated surfaces; update the div rendered in
BatchActionBar accordingly.
- Line 69: Replace the non-semantic Tailwind class "text-text-muted" with the
project's semantic token "text-muted-foreground" wherever it is used in the
approvals UI (start with the className on the BatchActionBar component in
BatchActionBar.tsx). Update the className prop value in the BatchActionBar JSX
and then search the approvals folder for any other occurrences of
"text-text-muted" and replace them with "text-muted-foreground" to keep styles
consistent.
In `@web/src/pages/ApprovalsPage.tsx`:
- Around line 111-120: handleApproveOne currently calls optimisticApprove(id)
which updates the item's status but does not remove the id from selectedIds,
leaving stale selections; fix this in the approvals store by ensuring the
optimistic status transition also removes the id from selectedIds (or expose a
removeSelectedId/deselectId method and call it from handleApproveOne). Update
the function optimisticApprove (in approvals.ts) to perform both the approval
swap and selectedIds.delete(id) (or call the new deselect helper) so the UI
batch bar and drawer/card paths stay consistent after a single-item approve; if
you add a store method, call it from handleApproveOne in addition to
optimisticApprove so optimistic rollback still reverses both actions.
- Around line 343-349: The textarea uses a hardcoded Tailwind class
min-h-[60px]; replace this with the repo's spacing tokens or standard Tailwind
spacing to follow the design system (e.g., use a density-aware token like
p-card/gap-section-gap or a standard min-h class such as min-h-16/min-h-14) in
the JSX where the controlled textarea (value={batchComment}, onChange={e =>
setBatchComment(e.target.value)}) is rendered, and apply the same change to the
other approval textarea in this component so both use tokenized/standard spacing
instead of min-h-[60px].
- Around line 256-304: The map callback rendering each risk-level group is too
large and mixes selection logic with JSX; extract it into a new small component
(e.g., ApprovalRiskGroupSection) that accepts props: riskLevel, items,
selectedIds, selectAllInGroup, deselectAllInGroup, handleSelectApproval,
handleApproveOne, handleRejectOne, toggleSelection and uses
getRiskLevelIcon/getRiskLevelLabel to build the same SectionCard UI and
selection checkbox behavior; replace the inline function passed to
[...grouped.entries()].map(...) with a call that returns
<ApprovalRiskGroupSection .../> so selection math (pendingIds, allSelected) and
card mapping (ApprovalCard) live inside the new component for easier testing and
readability.
- Around line 95-108: The current useEffect only fetches when selectedId exists
and selectedApproval is falsy, which misses cases where selectedId changes to a
different id while selectedApproval remains populated; update the effect
watching selectedId and selectedApproval so it calls fetchApproval(selectedId)
whenever selectedId is truthy and either selectedApproval is missing or
selectedApproval.id !== selectedId, and include selectedApproval in the
dependency array (keep fetchApproval and selectedId as dependencies) so the
effect re-runs on URL changes and refetches the correct approval details.
In `@web/src/stores/approvals.ts`:
- Around line 67-74: fetchApprovals currently overwrites the entire approvals
array with the server response which can clobber in-flight optimistic changes;
update fetchApprovals so after receiving result.data it merges server items into
the existing approvals state and preserves any optimistic transitions recorded
in pendingTransitions (e.g., for each approval id, if pendingTransitions has a
pending status/use-case, keep that status or keep the local approval object
instead of replacing it with server.status), then set total and loading as
before; locate the fetchApprovals method and the pendingTransitions state in
this file to implement the merge logic so polling cannot revert optimistic
approve/reject changes.
- Around line 77-84: In fetchApproval, clear stale UI state at start and update
it consistently on both success and failure: when entering the function set
selectedApproval to null/undefined and error to null (along with loadingDetail:
true) so old approval A isn't shown while loading B; on success set
selectedApproval to the returned approval and clear error; on catch set
loadingDetail false, set error to getErrorMessage(err) and also clear
selectedApproval; use the existing symbols approvalsApi.getApproval,
getErrorMessage, selectedApproval, loadingDetail, and error to locate and
implement these updates.
In `@web/src/utils/approvals.ts`:
- Around line 143-171: The function filterApprovals currently uses a
readonly-to-mutable cast on the approvals parameter; remove the type assertion
and instead create a mutable copy (e.g., let result: ApprovalResponse[] =
approvals.slice()) or initialize result with the result of the first filter call
so you never cast away readonly; update the variable declaration in
filterApprovals and keep the subsequent result = result.filter(...) calls
unchanged.
🪄 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: a9b3eb06-b2f4-4d18-be22-1018b56484da
📒 Files selected for processing (25)
docs/design/brand-and-ux.mddocs/design/page-structure.mdweb/src/__tests__/helpers/factories.tsweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/__tests__/stores/approvals.test.tsweb/src/__tests__/utils/approvals.test.tsweb/src/hooks/useApprovalsData.tsweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/pages/approvals/ApprovalsSkeleton.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/approvals/BatchActionBar.tsxweb/src/stores/approvals.tsweb/src/utils/approvals.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). (6)
- GitHub Check: Dashboard Test
- GitHub Check: Build Sandbox
- GitHub Check: Build Backend
- GitHub Check: Build Web
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (5)
web/src/**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
web/src/**/*.{tsx,ts}: Always reuse existing components from web/src/components/ui/ before creating new ones
Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files
Use font-sans or font-mono (maps to Geist tokens); never set fontFamily directly
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; never hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values
Do not recreate status dots inline -- use
Do not build card-with-header layouts from scratch -- use
Do not create metric displays with 'text-metric font-bold' -- use
Do not render initials circles manually -- use
Do not create complex (>8 line) JSX inside .map() -- extract to a shared component
Do not use rgba() with hardcoded values -- use design token variables
CSS side-effect imports need type declarations; Vite's '/// ' covers this in TS 6
Files:
web/src/__tests__/helpers/factories.tsweb/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/pages/approvals/ApprovalsSkeleton.tsxweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalFilterBar.tsxweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/__tests__/stores/approvals.test.tsweb/src/hooks/useApprovalsData.tsweb/src/utils/approvals.tsweb/src/stores/approvals.ts
web/src/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
PostToolUse hook (scripts/check_web_design_system.py) runs automatically on every Edit/Write to web/src/ files; fix all violations before proceeding
Files:
web/src/__tests__/helpers/factories.tsweb/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/pages/approvals/ApprovalsSkeleton.tsxweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalFilterBar.tsxweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/__tests__/stores/approvals.test.tsweb/src/hooks/useApprovalsData.tsweb/src/utils/approvals.tsweb/src/stores/approvals.ts
web/src/**/__tests__/**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
Property-based testing in React uses fast-check (fc.assert + fc.property); integrated with Vitest
Files:
web/src/__tests__/helpers/factories.tsweb/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/__tests__/stores/approvals.test.ts
web/src/**/*.stories.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
web/src/**/*.stories.{tsx,ts}: Use 'storybook/test' (not '@storybook/test'), 'storybook/actions' (not '@storybook/addon-actions') in Storybook 10
Use 'parameters.backgrounds.options' (object keyed by name) + 'initialGlobals.backgrounds.value' in Storybook 10 (replaces old default + values array)
Use 'parameters.a11y.test: "error" | "todo" | "off"' in Storybook 10 to enforce WCAG compliance
Files:
web/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/approvals/ApprovalCard.stories.tsx
docs/design/*.md
📄 CodeRabbit inference engine (CLAUDE.md)
Update the relevant docs/design/ page to reflect approved deviations from the spec
Files:
docs/design/page-structure.mddocs/design/brand-and-ux.md
🧠 Learnings (19)
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.stories.{tsx,ts} : Use 'storybook/test' (not 'storybook/test'), 'storybook/actions' (not 'storybook/addon-actions') in Storybook 10
Applied to files:
web/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/__tests__/stores/approvals.test.ts
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/__tests__/**/*.{tsx,ts} : Property-based testing in React uses fast-check (fc.assert + fc.property); integrated with Vitest
Applied to files:
web/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/__tests__/stores/approvals.test.ts
📚 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:
web/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/__tests__/stores/approvals.test.ts
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively
Applied to files:
web/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/pages/approvals/ApprovalsSkeleton.tsxweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalFilterBar.tsxweb/src/pages/approvals/ApprovalCard.stories.tsxdocs/design/brand-and-ux.mdweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/utils/approvals.ts
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not build card-with-header layouts from scratch -- use <SectionCard>
Applied to files:
web/src/pages/approvals/ApprovalsSkeleton.tsxweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Always reuse existing components from web/src/components/ui/ before creating new ones
Applied to files:
web/src/pages/approvals/ApprovalsSkeleton.tsxdocs/design/brand-and-ux.mdweb/src/pages/approvals/ApprovalCard.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.stories.{tsx,ts} : Use 'parameters.a11y.test: "error" | "todo" | "off"' in Storybook 10 to enforce WCAG compliance
Applied to files:
web/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/approvals/ApprovalCard.stories.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.stories.{tsx,ts} : Use 'parameters.backgrounds.options' (object keyed by name) + 'initialGlobals.backgrounds.value' in Storybook 10 (replaces old default + values array)
Applied to files:
web/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/approvals/ApprovalCard.stories.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/.storybook/**/*.{ts,tsx} : Use 'defineMain' from 'storybook/react-vite/node' and 'definePreview' from 'storybook/react-vite' in Storybook 10; include explicit 'framework' field
Applied to files:
web/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/approvals/ApprovalCard.stories.tsx
📚 Learning: 2026-03-18T08:23:08.912Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T08:23:08.912Z
Learning: When approved deviations occur, update the relevant `docs/design/` page to reflect the new reality.
Applied to files:
docs/design/page-structure.mddocs/design/brand-and-ux.md
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to docs/design/*.md : Update the relevant docs/design/ page to reflect approved deviations from the spec
Applied to files:
docs/design/page-structure.mddocs/design/brand-and-ux.md
📚 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:
docs/design/page-structure.mddocs/design/brand-and-ux.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 docs/design/*.md : Design spec pages: 7 pages in `docs/design/` — index, agents, organization, communication, engine, memory, operations
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not create metric displays with 'text-metric font-bold' -- use <MetricCard>
Applied to files:
web/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/pages/approvals/ApprovalCard.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : CSS side-effect imports need type declarations; Vite's '/// <reference types="vite/client" />' covers this in TS 6
Applied to files:
web/src/__tests__/utils/approvals.test.ts
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not recreate status dots inline -- use <StatusBadge>
Applied to files:
web/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalCard.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files
Applied to files:
docs/design/brand-and-ux.mdweb/src/pages/approvals/ApprovalCard.tsxweb/src/utils/approvals.ts
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not create complex (>8 line) JSX inside .map() -- extract to a shared component
Applied to files:
web/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values
Applied to files:
web/src/pages/approvals/ApprovalCard.tsx
🔇 Additional comments (10)
web/src/__tests__/helpers/factories.ts (1)
37-57: LGTM!The
makeApprovalfactory follows the same pattern asmakeTask, providing sensible defaults that can be overridden per-test. The default values cover allApprovalResponsefields appropriately.web/src/pages/approvals/BatchActionBar.tsx (1)
1-78: Well-structured component with proper animation setup.Good use of the existing
Buttoncomponent, semantic color tokens (text-success,text-danger,border-border), and the sharedspringDefaulttransition config. The exit animation will work correctly since the parent usesAnimatePresence(confirmed inApprovalsPage.tsx:321-331).web/src/__tests__/pages/approvals/BatchActionBar.test.tsx (1)
1-46: LGTM!Good test coverage for
BatchActionBar: verifies selected count display, all three button callbacks, and the loading/disabled state. The use ofuserEventfor async click interactions andvi.fn()for mocks follows Vitest best practices.web/src/utils/approvals.ts (2)
1-106: Well-organized utility module.Clean separation of concerns with mapping functions for colors, labels, and icons. The use of
Recordtypes ensures exhaustive coverage of all enum values. The urgency formatting handles edge cases properly (null, negative, boundary values).
161-168: No issue. Thedescriptionfield inApprovalItem(whichApprovalResponseextends) is typed asstring, not nullable. The code safely calls.toLowerCase()without defensive checks needed.> Likely an incorrect or invalid review comment.docs/design/brand-and-ux.md (1)
242-267: LGTM!Documentation accurately reflects the new approval utility functions and types added in
web/src/utils/approvals.ts. The table format is consistent with existing entries, and the type values match the API definitions.web/src/__tests__/utils/approvals.test.ts (2)
165-197: Good test coverage for groupByRiskLevel.Tests verify insertion order preservation, empty input handling, and omission of empty risk levels. The Map key ordering test is important for ensuring consistent UI rendering.
201-253: Comprehensive filter tests with good edge case coverage.Tests cover individual filters, combined AND logic, case-insensitive search, and empty description handling. Consider adding a property-based test for filter idempotency if you want to strengthen coverage further.
web/src/pages/approvals/ApprovalFilterBar.tsx (1)
16-117: Well-structured filter bar with good UX patterns.The component properly handles filter state, displays active filter pills with remove actions, and provides a "Clear all" option. The use of
aria-labelon form controls is good for accessibility.web/src/__tests__/stores/approvals.test.ts (1)
247-253: > Likely an incorrect or invalid review comment.
| useEffect(() => { | ||
| useApprovalsStore.getState().fetchApprovals({ limit: 200 }) | ||
| }, []) | ||
|
|
||
| // Lightweight polling for approval refresh | ||
| const pollFn = useCallback(async () => { | ||
| await useApprovalsStore.getState().fetchApprovals({ limit: 200 }) | ||
| }, []) |
There was a problem hiding this comment.
The hook silently drops approvals after the first 200.
Both the initial load and the poll hard-code limit: 200, but this hook doesn't expose pagination or a truncated-state UX. Once the queue grows past 200, the remainder never render or participate in batch actions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/hooks/useApprovalsData.ts` around lines 59 - 66, The hook currently
hard-codes fetchApprovals({ limit: 200 }) in both the initial useEffect and
pollFn, causing approvals beyond 200 to be silently dropped; update
useApprovalsData (the effect and the pollFn) to stop using a fixed 200 limit and
instead either accept a configurable limit/cursor param or call the store API to
fetch all pages and merge results: modify calls to
useApprovalsStore.getState().fetchApprovals to pass through a limit/cursor from
the hook props or state (or omit limit to mean "fetch all"), add a
fetchMore/paginate method and an isTruncated/hasMore flag to the hook return so
the UI can request additional pages, and ensure pollFn uses the same
params/merge logic so repeated polling doesn't overwrite paginated results.
| const DOT_COLOR_CLASSES: Record<string, string> = { | ||
| danger: 'bg-danger', | ||
| warning: 'bg-warning', | ||
| accent: 'bg-accent', | ||
| 'accent-dim': 'bg-accent-dim', | ||
| } | ||
|
|
||
| const URGENCY_BADGE_CLASSES: Record<string, string> = { | ||
| danger: 'border-danger/30 bg-danger/10 text-danger', | ||
| warning: 'border-warning/30 bg-warning/10 text-warning', | ||
| accent: 'border-accent/30 bg-accent/10 text-accent', | ||
| 'text-secondary': 'border-border bg-surface text-text-secondary', | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use the shared <StatusBadge> for the risk indicator.
DOT_COLOR_CLASSES plus the inline dot forks the design-system status pattern and leaves the risk state to a bespoke color-only implementation. Please render this through the shared badge primitive instead of maintaining a second indicator here.
As per coding guidelines, Do not recreate status dots inline -- use <StatusBadge>.
Also applies to: 101-109
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/pages/approvals/ApprovalCard.tsx` around lines 19 - 31, The approval
card currently defines DOT_COLOR_CLASSES and URGENCY_BADGE_CLASSES and renders
an inline dot for risk state; replace that bespoke dot implementation by using
the shared StatusBadge component instead: remove/stop using DOT_COLOR_CLASSES
and the inline dot markup in ApprovalCard (and update any usages around lines
referenced 101-109) and render <StatusBadge> with the appropriate variant/level
mapped from the existing urgency/risk prop so styling comes from the shared
primitive; keep URGENCY_BADGE_CLASSES only if still needed for non-dot badge
styling, otherwise map the same urgency keys to StatusBadge props (e.g.,
"danger"/"warning"/"accent"/"accent-dim") so the component uses the central
status styles.
There was a problem hiding this comment.
Actionable comments posted: 15
♻️ Duplicate comments (2)
web/src/pages/ApprovalsPage.tsx (1)
266-314: 🛠️ Refactor suggestion | 🟠 MajorExtract the risk-group renderer out of the
.map().This callback is still carrying selection math, header actions, and the full card list inline. Please move it into a small
ApprovalRiskGroupSectioncomponent/helper.As per coding guidelines, "Do not create complex (>8 line) JSX inside .map() -- extract to a shared component".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/ApprovalsPage.tsx` around lines 266 - 314, The JSX inside the map over [...grouped.entries()] is too large and should be extracted into a new component called ApprovalRiskGroupSection; create a new component that accepts props (riskLevel, items, selectedIds, selectAllInGroup, deselectAllInGroup, handleSelectApproval, handleApproveOne, handleRejectOne, toggleSelection) and moves the current logic that uses getRiskLevelIcon, getRiskLevelLabel, pendingIds/allSelected calculation, the SectionCard with its header action checkbox, StaggerGroup mapping to ApprovalCard, and ErrorBoundary into that component, then replace the inline map body with a call like <ApprovalRiskGroupSection .../> passing the matching values so selection math and rendering are encapsulated and the .map() callback stays small.web/src/hooks/useApprovalsData.ts (1)
63-68:⚠️ Potential issue | 🟠 MajorThis hook still silently drops approvals after the first 200.
Both fetch paths hard-code
limit: 200, but the hook/page expose no pagination or truncated-state UX. Once the queue grows past 200, the remainder never render or participate in batch actions.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/hooks/useApprovalsData.ts` around lines 63 - 68, The hook is hard-capping results by calling useApprovalsStore.getState().fetchApprovals({ limit: 200 }) in both initial effect and pollFn, causing approvals beyond 200 to be dropped; update useApprovalsData.ts to stop using a fixed limit — either remove the limit argument so fetchApprovals returns all items, accept a configurable page size parameter from the hook/page, or implement iterative fetching (call fetchApprovals repeatedly with offset/page until fewer than the requested page size are returned) and wire that into both the initial load and pollFn; make references to useApprovalsStore.getState().fetchApprovals and pollFn when making the change so both call sites are updated consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/design/page-structure.md`:
- Line 351: The Approvals batch selection UI lacks keyboard support; update the
relevant components (e.g., ApprovalCard, ApprovalsList, ApprovalCheckbox,
BatchApproveButton, BatchRejectButton) to add keyboard accessibility: give
interactive elements explicit focusability (tabindex or native buttons), add key
handlers to toggle selection on Space/Enter (handle keydown in ApprovalCheckbox
or ApprovalCard), ensure Tab moves focus between approval cards (manage
tabindex/focus order) and expose ARIA states (aria-checked/aria-selected) for
screen readers, and implement keyboard shortcuts for batch actions (e.g.,
onKeyDown at ApprovalsList or a focused container to trigger
BatchApproveButton/BatchRejectButton on a defined shortcut like Ctrl/Cmd+Enter).
Also add automated unit/axe tests to cover keyboard toggle, tab navigation, and
shortcut activation.
In `@web/src/__tests__/hooks/useApprovalsData.test.ts`:
- Around line 85-91: The test for useApprovalsData should stop using a redundant
dynamic import of useWebSocket; since useWebSocket is mocked at module scope,
access the hoisted mock directly via vi.mocked(useWebSocket).mock.calls—update
the test that calls renderHook(() => useApprovalsData()) to either import {
useWebSocket } at the top of the test file or remove the dynamic import and
reference useWebSocket directly when capturing callArgs (look for the test
function that currently does const { useWebSocket } = await
import('@/hooks/useWebSocket')).
In `@web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx`:
- Around line 69-72: The test beforeEach directly mutates the toast array via
useToastStore.getState().toasts = [], which can bypass the store's reactivity;
instead reset the store using its public API (e.g., call the store's clear/reset
method or use the store's setter like useToastStore.setState({ toasts: [] }))
inside the beforeEach so the toast state is replaced via the store and not by
direct assignment to useToastStore.getState().toasts.
In `@web/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsx`:
- Around line 14-17: The test uses filterRow! without a TypeScript-understood
null guarantee; add an explicit assertion such as
expect(filterRow).not.toBeNull() immediately after obtaining filterRow (or cast
filterRow to Element with as Element) so the following line can safely call
querySelectorAll; update the code around the
container.querySelector('.flex.items-center.gap-3') assignment to include this
assertion before using filterRow in filterSkeletons.
In `@web/src/__tests__/pages/approvals/ApprovalTimeline.test.tsx`:
- Around line 36-40: Replace the silent per-item assertion loop in
ApprovalTimeline.test.tsx (where items = screen.getAllByRole('listitem') and
each item is checked for '.animate-pulse') with a clearer assertion that reports
which index failed—either use the jest-extended matcher toSatisfyAll (e.g.,
expect(items).toSatisfyAll(item => !item.querySelector('.animate-pulse'))) if
available, or implement explicit indexed checks (e.g., iterate items and call
fail(...) with the index when item.querySelector('.animate-pulse') is truthy) so
failures include the item index for easier diagnostics.
In `@web/src/__tests__/pages/ApprovalsPage.test.tsx`:
- Around line 35-38: The mock uses an unconventional computed property name in
vi.mock which is unnecessary; update the mock of useApprovalsData so it directly
returns the hook by name instead of using [hookName], i.e., change the vi.mock
callback to return an object with the literal property useApprovalsData that
calls getApprovalsData(), keeping vi.mock and getApprovalsData references
intact.
In `@web/src/hooks/useApprovalsData.ts`:
- Around line 61-76: The hook double-fetches because the initial useEffect calls
useApprovalsStore.getState().fetchApprovals(...) and polling.start() immediately
invokes pollFn which calls the same fetch; remove the standalone initial fetch
to avoid the duplicate. Specifically, delete the first useEffect that calls
fetchApprovals({ limit: 200 }) and rely on the existing pollFn (which calls
useApprovalsStore.getState().fetchApprovals) together with polling.start/stop
from usePolling to perform the initial and subsequent loads.
In `@web/src/pages/approvals/ApprovalCard.tsx`:
- Around line 19-33: Remove the duplicated constant definitions
DOT_COLOR_CLASSES and URGENCY_BADGE_CLASSES from ApprovalCard.tsx and import the
existing exported constants from '@/utils/approvals' instead; update the top of
the file to import { DOT_COLOR_CLASSES, URGENCY_BADGE_CLASSES } from
'@/utils/approvals' and delete the local Record declarations so the component
uses the single source of truth.
In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx`:
- Around line 156-179: Replace the custom backdrop/motion.aside implementation
in ApprovalDetailDrawer with the shared Drawer primitive: remove the motion.div
(overlayBackdrop) and motion.aside (PANEL_VARIANTS, Escape/focus handling,
panelRef) and wrap the panel content with the Drawer component exported from
web/src/components/ui/drawer.tsx, passing the same open state, onClose handler,
aria-label (use approval ? `Approval detail: ${approval.title}` : 'Approval
detail'), and any needed className overrides; move the existing panel children
into Drawer’s children and delete PANEL_VARIANTS, overlayBackdrop, and panelRef
usages so the built-in animations, focus trap, and Escape handling from Drawer
are used instead.
- Around line 283-292: The Metadata rendering is unsafe because
approval.metadata values may be nested objects; update the mapping in
ApprovalDetailDrawer (where Object.entries(approval.metadata) is iterated) to
render values defensively by handling null and object types: for each [key,
value] detect if value is null/undefined, a primitive, or an object/array and
render a safe string (e.g., String(value) for primitives and
JSON.stringify(value) for objects) so that nested objects don't throw when
inserted into the React tree; ensure the change targets the rendering of the
<dd> value for each metadata entry.
In `@web/src/pages/ApprovalsPage.tsx`:
- Around line 145-149: The warning toast branches in ApprovalsPage.tsx currently
only show counts; update the handlers that call batchApprove and batchReject to
include the returned failedReasons in the toast message when result.failed > 0 —
locate the branches that call useToastStore.getState().add (the success/warning
branches) and append or include result.failedReasons (e.g., join into a readable
string or show first N reasons) into the title or description of the warning
toast so users see why items failed; do the same for the analogous branch
handling batchReject results.
- Around line 95-102: handleSelectApproval currently sets the URL param and then
eagerly calls fetchApproval, which duplicates requests because the effect
watching selectedId also fetches; remove the fetchApproval call from
handleSelectApproval and any other approval-select handler that only sets the
'selected' search param so that fetching happens only from the selectedId effect
(keep setSearchParams((prev)=>{...}) intact and rely on the existing useEffect
that reads selectedId to call fetchApproval).
In `@web/src/stores/approvals.ts`:
- Around line 89-96: fetchApproval currently races: any resolution (success or
error) from approvalsApi.getApproval(id) will overwrite shared state even if a
newer fetch was started. Fix by tracking the latest requested id or a request
counter inside the store (e.g., latestDetailRequestId or lastRequestedId) when
fetchApproval starts, capture that token in the async closure, and only commit
set({ selectedApproval, loadingDetail, detailError }) if the token still
matches; on mismatch ignore the stale response. Reference: fetchApproval,
selectedApproval, loadingDetail, detailError, approvalsApi.getApproval,
getErrorMessage.
- Around line 182-194: upsertApproval currently doesn't remove ids from
selectedIds when an approval leaves pending, causing the batch selector to
include stale ids; modify upsertApproval to remove approval.id from the
selectedIds set/array when pendingTransitions no longer contains approval.id
(mirror the behavior in approveOne/rejectOne/handleWsEvent), and also clear
selectedApproval if it was the removed id; locate the logic in upsertApproval
(references: upsertApproval, pendingTransitions, selectedIds, selectedApproval)
and prune the non-pending id before calling set so the UI batch bar won't count
hidden approved/rejected items.
In `@web/src/utils/approvals.ts`:
- Around line 179-186: The search filter can throw if a.description is
null/undefined; update the block that runs when filters.search is truthy (the
result = result.filter(...) usage) to defensively handle a.description before
calling toLowerCase — e.g., use a safe fallback or optional chaining so you call
toLowerCase on (a.description ?? '') or (a.description || '') and keep the
existing title check (a.title.toLowerCase()) intact; ensure you also normalize
title/description consistently when computing query from filters.search.
---
Duplicate comments:
In `@web/src/hooks/useApprovalsData.ts`:
- Around line 63-68: The hook is hard-capping results by calling
useApprovalsStore.getState().fetchApprovals({ limit: 200 }) in both initial
effect and pollFn, causing approvals beyond 200 to be dropped; update
useApprovalsData.ts to stop using a fixed limit — either remove the limit
argument so fetchApprovals returns all items, accept a configurable page size
parameter from the hook/page, or implement iterative fetching (call
fetchApprovals repeatedly with offset/page until fewer than the requested page
size are returned) and wire that into both the initial load and pollFn; make
references to useApprovalsStore.getState().fetchApprovals and pollFn when making
the change so both call sites are updated consistently.
In `@web/src/pages/ApprovalsPage.tsx`:
- Around line 266-314: The JSX inside the map over [...grouped.entries()] is too
large and should be extracted into a new component called
ApprovalRiskGroupSection; create a new component that accepts props (riskLevel,
items, selectedIds, selectAllInGroup, deselectAllInGroup, handleSelectApproval,
handleApproveOne, handleRejectOne, toggleSelection) and moves the current logic
that uses getRiskLevelIcon, getRiskLevelLabel, pendingIds/allSelected
calculation, the SectionCard with its header action checkbox, StaggerGroup
mapping to ApprovalCard, and ErrorBoundary into that component, then replace the
inline map body with a call like <ApprovalRiskGroupSection .../> passing the
matching values so selection math and rendering are encapsulated and the .map()
callback stays small.
🪄 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: 1037a974-b825-4ce6-a7e1-f273d7f9c6d8
📒 Files selected for processing (25)
docs/design/brand-and-ux.mddocs/design/page-structure.mdweb/src/__tests__/helpers/factories.tsweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsxweb/src/__tests__/stores/approvals.test.tsweb/src/__tests__/utils/approvals.test.tsweb/src/hooks/useApprovalsData.tsweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/approvals/BatchActionBar.tsxweb/src/stores/approvals.tsweb/src/utils/approvals.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). (6)
- GitHub Check: Dashboard Test
- GitHub Check: Build Backend
- GitHub Check: Build Web
- GitHub Check: Build Sandbox
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (5)
web/src/**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
web/src/**/*.{tsx,ts}: Always reuse existing components from web/src/components/ui/ before creating new ones
Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files
Use font-sans or font-mono (maps to Geist tokens); never set fontFamily directly
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; never hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values
Do not recreate status dots inline -- use
Do not build card-with-header layouts from scratch -- use
Do not create metric displays with 'text-metric font-bold' -- use
Do not render initials circles manually -- use
Do not create complex (>8 line) JSX inside .map() -- extract to a shared component
Do not use rgba() with hardcoded values -- use design token variables
CSS side-effect imports need type declarations; Vite's '/// ' covers this in TS 6
Files:
web/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/helpers/factories.tsweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/utils/approvals.tsweb/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/pages/approvals/ApprovalCard.tsxweb/src/__tests__/stores/approvals.test.tsweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/hooks/useApprovalsData.tsweb/src/stores/approvals.tsweb/src/pages/approvals/ApprovalFilterBar.tsx
web/src/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
PostToolUse hook (scripts/check_web_design_system.py) runs automatically on every Edit/Write to web/src/ files; fix all violations before proceeding
Files:
web/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/helpers/factories.tsweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/utils/approvals.tsweb/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/pages/approvals/ApprovalCard.tsxweb/src/__tests__/stores/approvals.test.tsweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/hooks/useApprovalsData.tsweb/src/stores/approvals.tsweb/src/pages/approvals/ApprovalFilterBar.tsx
web/src/**/__tests__/**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
Property-based testing in React uses fast-check (fc.assert + fc.property); integrated with Vitest
Files:
web/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/helpers/factories.tsweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/__tests__/stores/approvals.test.ts
web/src/**/*.stories.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
web/src/**/*.stories.{tsx,ts}: Use 'storybook/test' (not '@storybook/test'), 'storybook/actions' (not '@storybook/addon-actions') in Storybook 10
Use 'parameters.backgrounds.options' (object keyed by name) + 'initialGlobals.backgrounds.value' in Storybook 10 (replaces old default + values array)
Use 'parameters.a11y.test: "error" | "todo" | "off"' in Storybook 10 to enforce WCAG compliance
Files:
web/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsx
docs/design/*.md
📄 CodeRabbit inference engine (CLAUDE.md)
Update the relevant docs/design/ page to reflect approved deviations from the spec
Files:
docs/design/page-structure.mddocs/design/brand-and-ux.md
🧠 Learnings (24)
📚 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:
web/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/__tests__/stores/approvals.test.ts
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/__tests__/**/*.{tsx,ts} : Property-based testing in React uses fast-check (fc.assert + fc.property); integrated with Vitest
Applied to files:
web/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/__tests__/stores/approvals.test.ts
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.stories.{tsx,ts} : Use 'storybook/test' (not 'storybook/test'), 'storybook/actions' (not 'storybook/addon-actions') in Storybook 10
Applied to files:
web/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/__tests__/stores/approvals.test.ts
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively
Applied to files:
web/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/utils/approvals.tsweb/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxdocs/design/brand-and-ux.mdweb/src/pages/approvals/ApprovalFilterBar.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.stories.{tsx,ts} : Use 'parameters.a11y.test: "error" | "todo" | "off"' in Storybook 10 to enforce WCAG compliance
Applied to files:
web/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.stories.{tsx,ts} : Use 'parameters.backgrounds.options' (object keyed by name) + 'initialGlobals.backgrounds.value' in Storybook 10 (replaces old default + values array)
Applied to files:
web/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/.storybook/**/*.{ts,tsx} : Use 'defineMain' from 'storybook/react-vite/node' and 'definePreview' from 'storybook/react-vite' in Storybook 10; include explicit 'framework' field
Applied to files:
web/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files
Applied to files:
web/src/utils/approvals.tsweb/src/pages/approvals/BatchActionBar.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxdocs/design/brand-and-ux.mdweb/src/pages/approvals/ApprovalFilterBar.tsx
📚 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:
web/src/utils/approvals.tsdocs/design/page-structure.mddocs/design/brand-and-ux.md
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not use rgba() with hardcoded values -- use design token variables
Applied to files:
web/src/utils/approvals.tsweb/src/pages/approvals/BatchActionBar.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/pages/approvals/ApprovalFilterBar.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values
Applied to files:
web/src/pages/approvals/BatchActionBar.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/pages/approvals/ApprovalFilterBar.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : 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/approvals/BatchActionBar.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/pages/approvals/ApprovalFilterBar.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not create metric displays with 'text-metric font-bold' -- use <MetricCard>
Applied to files:
web/src/pages/approvals/BatchActionBar.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/pages/approvals/ApprovalFilterBar.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use font-sans or font-mono (maps to Geist tokens); never set fontFamily directly
Applied to files:
web/src/pages/approvals/BatchActionBar.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not recreate status dots inline -- use <StatusBadge>
Applied to files:
web/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/pages/approvals/ApprovalFilterBar.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : CSS side-effect imports need type declarations; Vite's '/// <reference types="vite/client" />' covers this in TS 6
Applied to files:
web/src/__tests__/utils/approvals.test.ts
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not build card-with-header layouts from scratch -- use <SectionCard>
Applied to files:
web/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not create complex (>8 line) JSX inside .map() -- extract to a shared component
Applied to files:
web/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Always reuse existing components from web/src/components/ui/ before creating new ones
Applied to files:
web/src/pages/approvals/ApprovalCard.tsxdocs/design/brand-and-ux.md
📚 Learning: 2026-03-18T08:23:08.912Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T08:23:08.912Z
Learning: When approved deviations occur, update the relevant `docs/design/` page to reflect the new reality.
Applied to files:
docs/design/page-structure.mddocs/design/brand-and-ux.md
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to docs/design/*.md : Update the relevant docs/design/ page to reflect approved deviations from the spec
Applied to files:
docs/design/page-structure.mddocs/design/brand-and-ux.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 docs/design/*.md : Design spec pages: 7 pages in `docs/design/` — index, agents, organization, communication, engine, memory, operations
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to docs/design/**/*.md : Design specification pages in `docs/design/` must be consulted before implementing features (7 pages: index, agents, organization, communication, engine, memory, operations)
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-03-21T11:08:01.542Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T11:08:01.542Z
Learning: At every phase of planning and implementation, be critical — actively look for ways to improve the design in the spirit of what we're building (robustness, correctness, simplicity, future-proofing where it's free). Surface improvements as suggestions, not silent changes — user decides.
Applied to files:
docs/design/page-structure.md
🪛 GitHub Check: Dashboard Lint
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
[failure] 12-12:
Do not define component 'AnimatePresence' inside a function. Components should be defined at the module level. Move it to the top level
🔇 Additional comments (18)
docs/design/page-structure.md (2)
56-56: LGTM! Comprehensive documentation of the new Approvals page features.The updated description accurately reflects the implemented functionality: risk-level grouping with collapsible sections, urgency countdown indicators, batch approve/reject actions, detail drawer with timeline and metadata, and URL-synced filter bar. The emphasis on this being the "highest-urgency page" appropriately conveys its importance in the workflow.
219-222: LGTM! URL routing patterns follow established conventions.The new approval routes correctly use query parameters for filtering (
risk,type,search) and theselected=:idpattern for the overlay detail drawer, consistent with the Task Board's overlay pattern on line 214.web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx (2)
7-39: Static analysis false positive — mock component factories are evaluated once at load time.The linter flags "Do not define component inside a function" for the
AnimatePresenceandmotion.*mock components. However, these are insidevi.mock()callback which is hoisted and evaluated once at module load, not inside a render function that would cause remounting issues. This is a valid mock pattern.If you want to silence the linter while keeping the code clear:
♻️ Optional: Extract mock components to module level
+// Mock components extracted to module level to satisfy linter +const MockAnimatePresence = ({ children }: { children: React.ReactNode }) => <>{children}</> +const MockMotionDiv = (props: React.ComponentProps<'div'> & Record<string, unknown>) => ( + <div className={props.className} onClick={props.onClick}> + {props.children} + </div> +) +// ... similar for MockMotionAside + vi.mock('framer-motion', async () => { const actual = await vi.importActual<typeof import('framer-motion')>('framer-motion') return { ...actual, - AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>, - motion: { - div: (props: React.ComponentProps<'div'> & Record<string, unknown>) => ( - ... - ), - ... - }, + AnimatePresence: MockAnimatePresence, + motion: { + div: MockMotionDiv, + aside: MockMotionAside, + }, } })
74-182: LGTM — comprehensive drawer test coverage.The test suite thoroughly covers:
- Core rendering with approval data
- Loading and error states
- Close interactions (button and Escape key)
- Confirmation dialog flows including rejection reason validation
- Focus trapping and ARIA attributes
- Conditional rendering based on
openprop and approval statusThe Escape key suppression test (lines 119-124) correctly validates the drawer behavior from the relevant code snippet.
web/src/pages/approvals/ApprovalsSkeleton.stories.tsx (1)
1-15: LGTM!The story file correctly includes
parameters.a11y.test: 'error'for WCAG compliance enforcement per Storybook 10 conventions. The structure is clean and appropriate for a simple skeleton component.web/src/__tests__/helpers/factories.ts (1)
37-57: LGTM!The
makeApprovalfactory correctly provides all required fields forApprovalResponse(including the extended fieldsseconds_remainingandurgency_level), follows the same pattern as the existingmakeTaskfactory, and uses sensible defaults. The dynamiccreated_attimestamp is a good choice for realistic test data.web/src/pages/approvals/ApprovalTimeline.stories.tsx (1)
1-71: LGTM!Comprehensive Storybook coverage with all approval lifecycle states (Pending, Approved, Rejected, Expired). The
basefixture is well-structured, and each story appropriately overrides the relevant fields. Theparameters.a11y.test: 'error'is correctly configured for WCAG compliance enforcement.web/src/__tests__/pages/ApprovalsPage.test.tsx (1)
53-123: LGTM — comprehensive page-level test coverage.The test suite effectively covers:
- Loading states (skeleton shown only when loading with no data)
- Error and WebSocket disconnection banners
- Empty state handling
- Risk-level grouping and metric cards
- Proper skeleton hiding when data exists during refresh
The mutable
hookReturnpattern allows clean per-test state customization.web/src/pages/approvals/ApprovalFilterBar.stories.tsx (1)
1-52: LGTM!The stories module correctly includes
parameters.a11y.test: 'error'for WCAG compliance enforcement. Good coverage of empty, active filters, and interactive states.web/src/pages/approvals/BatchActionBar.tsx (1)
1-79: LGTM!The component correctly uses
bg-surfacefor the floating bar background andtext-muted-foregroundfor the clear button. Design tokens are used consistently throughout (semantic colors, border tokens). Animation implementation with framer-motion is clean.web/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsx (1)
1-104: LGTM!Comprehensive test coverage for the filter bar: rendering, interaction callbacks, filter pills display/removal, and edge cases. The test assertions correctly use
riskLevel(camelCase) matching theApprovalPageFiltersinterface.web/src/utils/approvals.ts (1)
1-159: LGTM on the rest of the utilities module.
- Risk/status/urgency mappings are well-organized with proper type safety.
groupByRiskLevelcorrectly maintains order and excludes empty buckets.filterApprovalsnow uses spread[...approvals]instead of casting away readonly (past review addressed).- Shared CSS class mappings use semantic tokens consistently.
web/src/pages/approvals/BatchActionBar.stories.tsx (1)
1-36: LGTM!Correctly uses
'storybook/test'forfn()imports (Storybook 10 convention) and includesparameters.a11y.test: 'error'for WCAG compliance. Good coverage of different selection counts and loading state.web/src/pages/approvals/ApprovalFilterBar.tsx (1)
1-133: LGTM!All past review items addressed:
- Type-safe arrays with
as const satisfies(lines 5-6)- Correct semantic tokens:
text-muted-foreground(line 86),text-secondary(line 121)Clean component structure with the
FilterPillsubcomponent properly extracted.web/src/pages/approvals/ApprovalDetailDrawer.stories.tsx (1)
1-78: LGTM!Correctly uses
'storybook/test'for imports and includesparameters.a11y.test: 'error'. Good coverage of drawer states including the edge case with empty metadata. The mock handlers correctly returnPromise<void>matching the component's prop types.web/src/pages/approvals/ApprovalCard.tsx (3)
61-64: Direct state update during render is unconventional but works here.This pattern (checking
prevSecondsRefand callingsetCountdownduring render) is a known technique to sync state with changing props without an extra render cycle. It follows the React docs pattern for "adjusting state when a prop changes." Just noting this is intentional—no change needed.
66-80: Timer correctly stops for non-pending cards.The
shouldTickguard (line 69) now includesisPending, ensuring the interval doesn't run for approved/rejected cards. This addresses the past review concern about hidden timers.
134-150: Accessibility for urgency badge is correctly implemented.The countdown badge now has
aria-label={...}(line 141) exposing the expiry time to assistive tech, with the visual text markedaria-hidden="true"(line 144). This addresses the past review concern about screen reader access.
| | 4 | Status arrives, doesn't flash | WS events update Zustand stores; animation profile is "status-driven" (only changed elements animate) | | ||
| | 5 | Progressive disclosure | Dashboard summary to page detail. Agent list to agent detail page. Org chart node to agent detail | | ||
| | 6 | Keyboard-first | Cmd+K command palette. URL-addressable everything for bookmark/share. Arrow key nav in Task Board and Approvals | | ||
| | 6 | Keyboard-first | Cmd+K command palette. URL-addressable everything for bookmark/share. Arrow key nav in Task Board | |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Verify keyboard accessibility implementation in Approvals components
# Check for keyboard event handlers and ARIA attributes
echo "=== Checking ApprovalCard for keyboard handlers ==="
rg -n -A 3 -B 3 'onKeyDown|onKeyPress|role=|aria-' --type=tsx --type=ts web/src/pages/ApprovalsPage/ web/src/components/approvals/
echo -e "\n=== Checking for batch selection keyboard shortcuts ==="
rg -n 'useHotkeys|useKeyboard|KeyboardEvent|key ===' --type=tsx --type=ts web/src/pages/ApprovalsPage/ web/src/components/approvals/
echo -e "\n=== Checking accessibility attributes on interactive elements ==="
rg -n 'tabIndex|aria-selected|aria-checked' --type=tsx --type=ts web/src/components/approvals/Repository: Aureliolo/synthorg
Length of output: 338
🏁 Script executed:
#!/bin/bash
# Find Approvals-related components
echo "=== Finding Approvals-related files ==="
find . -type f -name "*pproval*" -o -name "*approval*" | grep -E '\.(ts|tsx|js|jsx)$' | head -20
echo -e "\n=== Checking directory structure ==="
ls -la web/src/pages/ 2>/dev/null || echo "web/src/pages/ not found"
ls -la web/src/components/ 2>/dev/null | head -20 || echo "web/src/components/ not found"Repository: Aureliolo/synthorg
Length of output: 3230
🏁 Script executed:
#!/bin/bash
# Search for keyboard handlers with correct rg syntax (without file type flags)
echo "=== Searching for keyboard event handlers ==="
rg -n 'onKeyDown|onKeyPress|onKeyUp' --max-count=20
echo -e "\n=== Searching for batch selection logic ==="
rg -n 'batch|select|toggle.*select' --max-count=30 | grep -i 'approval\|approver' || echo "No batch selection found in approval context"
echo -e "\n=== Searching for ARIA and accessibility attributes ==="
rg -n 'aria-selected|aria-checked|tabIndex' --max-count=20Repository: Aureliolo/synthorg
Length of output: 16653
🏁 Script executed:
#!/bin/bash
# Check the actual documentation context to understand what was removed
echo "=== Context around line 351 in docs/design/page-structure.md ==="
sed -n '345,360p' docs/design/page-structure.md
echo -e "\n=== Search for any mentions of Approvals keyboard navigation in docs ==="
rg -n 'Approvals.*arrow|arrow.*Approvals|batch.*key|keyboard.*batch' docs/Repository: Aureliolo/synthorg
Length of output: 1592
Add keyboard accessibility for Approvals batch selection workflow.
The removal of "Approvals" from the arrow key navigation statement is correct—arrow key navigation is only implemented in Task Board components, as confirmed by the codebase. However, this reveals a critical accessibility gap: the batch selection UI (checkboxes and approve/reject buttons) has no keyboard support at all.
Currently, users cannot:
- Toggle checkbox selection via Space/Enter
- Navigate between approval cards with Tab
- Trigger batch approve/reject via keyboard shortcuts
The batch selection workflow is entirely mouse-dependent, violating WCAG 2.1 accessibility standards. Implement keyboard support: Space/Enter for checkbox toggle, Tab navigation between items, and keyboard shortcuts for batch operations.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/design/page-structure.md` at line 351, The Approvals batch selection UI
lacks keyboard support; update the relevant components (e.g., ApprovalCard,
ApprovalsList, ApprovalCheckbox, BatchApproveButton, BatchRejectButton) to add
keyboard accessibility: give interactive elements explicit focusability
(tabindex or native buttons), add key handlers to toggle selection on
Space/Enter (handle keydown in ApprovalCheckbox or ApprovalCard), ensure Tab
moves focus between approval cards (manage tabindex/focus order) and expose ARIA
states (aria-checked/aria-selected) for screen readers, and implement keyboard
shortcuts for batch actions (e.g., onKeyDown at ApprovalsList or a focused
container to trigger BatchApproveButton/BatchRejectButton on a defined shortcut
like Ctrl/Cmd+Enter). Also add automated unit/axe tests to cover keyboard
toggle, tab navigation, and shortcut activation.
| it('sets up WebSocket with approvals channel', async () => { | ||
| const { useWebSocket } = await import('@/hooks/useWebSocket') | ||
| renderHook(() => useApprovalsData()) | ||
| const callArgs = vi.mocked(useWebSocket).mock.calls[0]![0] | ||
| const channels = callArgs.bindings.map((b) => b.channel) | ||
| expect(channels).toEqual(['approvals']) | ||
| }) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Dynamic import in test is redundant since the mock is already hoisted.
The useWebSocket mock is set up at module level via vi.mock, so you can access the mock directly without a dynamic import.
♻️ Simpler approach
- it('sets up WebSocket with approvals channel', async () => {
- const { useWebSocket } = await import('@/hooks/useWebSocket')
+ it('sets up WebSocket with approvals channel', () => {
+ // Import the mock at the top of the file instead
renderHook(() => useApprovalsData())
+ // Access via vi.mocked on the imported module
+ const { useWebSocket } = require('@/hooks/useWebSocket')
const callArgs = vi.mocked(useWebSocket).mock.calls[0]![0]
const channels = callArgs.bindings.map((b) => b.channel)
expect(channels).toEqual(['approvals'])
})Or import at file level:
import { useWebSocket } from '@/hooks/useWebSocket'
// ... in test:
const callArgs = vi.mocked(useWebSocket).mock.calls[0]![0]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/__tests__/hooks/useApprovalsData.test.ts` around lines 85 - 91, The
test for useApprovalsData should stop using a redundant dynamic import of
useWebSocket; since useWebSocket is mocked at module scope, access the hoisted
mock directly via vi.mocked(useWebSocket).mock.calls—update the test that calls
renderHook(() => useApprovalsData()) to either import { useWebSocket } at the
top of the test file or remove the dynamic import and reference useWebSocket
directly when capturing callArgs (look for the test function that currently does
const { useWebSocket } = await import('@/hooks/useWebSocket')).
| if (filters.search) { | ||
| const query = filters.search.toLowerCase() | ||
| result = result.filter( | ||
| (a) => | ||
| a.title.toLowerCase().includes(query) || | ||
| a.description.toLowerCase().includes(query), | ||
| ) | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider defensive null check for description in search filter.
If description is ever null or undefined from the API, a.description.toLowerCase() will throw. This is a minor edge case if the API contract guarantees a non-null description.
♻️ Defensive fix
if (filters.search) {
const query = filters.search.toLowerCase()
result = result.filter(
(a) =>
a.title.toLowerCase().includes(query) ||
- a.description.toLowerCase().includes(query),
+ a.description?.toLowerCase().includes(query),
)
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/utils/approvals.ts` around lines 179 - 186, The search filter can
throw if a.description is null/undefined; update the block that runs when
filters.search is truthy (the result = result.filter(...) usage) to defensively
handle a.description before calling toLowerCase — e.g., use a safe fallback or
optional chaining so you call toLowerCase on (a.description ?? '') or
(a.description || '') and keep the existing title check (a.title.toLowerCase())
intact; ensure you also normalize title/description consistently when computing
query from filters.search.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx (1)
75-78:⚠️ Potential issue | 🟡 MinorReset toast state via Zustand API instead of direct state mutation.
Line 77 directly assigns
useToastStore.getState().toasts = [], which can bypass store update semantics and cause flaky isolation between tests.Suggested fix
beforeEach(() => { vi.clearAllMocks() - useToastStore.getState().toasts = [] + useToastStore.setState({ toasts: [] }) })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx` around lines 75 - 78, The test resets the toast list by directly mutating state (useToastStore.getState().toasts = []), which bypasses Zustand semantics; update the beforeEach to call the store's setter instead (e.g., useToastStore.setState({ toasts: [] })) so the state is updated via the Zustand API; change the line referencing useToastStore.getState().toasts to useToastStore.setState({ toasts: [] }) in the beforeEach block surrounding the existing vi.clearAllMocks() call.
🤖 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/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx`:
- Around line 161-171: The test in ApprovalDetailDrawer.test currently only
checks for focusable elements and aria-modal but doesn't assert actual tab-trap
behavior; update the 'focus is trapped within the drawer' test (the renderDrawer
helper and the dialog/querySelectorAll usage) to simulate keyboard Tab and
Shift+Tab navigation (using userEvent.tab or equivalent) and after each Tab
press assert document.activeElement remains one of the dialog's
focusableElements so focus never escapes; additionally cycle through more than
the number of focusable elements to confirm wrapping in both forward and
backward directions.
- Around line 184-187: The test "hides approve/reject buttons for non-pending
approvals" only asserts the approve button is absent; update the test to also
assert the reject button is absent so partial regressions are caught: when
calling renderDrawer({ status: 'approved' }) add a second expectation using
screen.queryByRole('button', { name: /reject/i }) and assert
.not.toBeInTheDocument(), mirroring the approve assertion and keeping the same
status input and test name.
---
Duplicate comments:
In `@web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx`:
- Around line 75-78: The test resets the toast list by directly mutating state
(useToastStore.getState().toasts = []), which bypasses Zustand semantics; update
the beforeEach to call the store's setter instead (e.g.,
useToastStore.setState({ toasts: [] })) so the state is updated via the Zustand
API; change the line referencing useToastStore.getState().toasts to
useToastStore.setState({ toasts: [] }) in the beforeEach block surrounding the
existing vi.clearAllMocks() call.
🪄 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: 727b81e2-45f9-4429-b95b-3203c1b96f13
📒 Files selected for processing (1)
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📜 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)
- GitHub Check: Dashboard Test
- GitHub Check: Build Sandbox
- GitHub Check: Build Web
- GitHub Check: Build Backend
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (3)
web/src/**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
web/src/**/*.{tsx,ts}: Always reuse existing components from web/src/components/ui/ before creating new ones
Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files
Use font-sans or font-mono (maps to Geist tokens); never set fontFamily directly
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; never hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values
Do not recreate status dots inline -- use
Do not build card-with-header layouts from scratch -- use
Do not create metric displays with 'text-metric font-bold' -- use
Do not render initials circles manually -- use
Do not create complex (>8 line) JSX inside .map() -- extract to a shared component
Do not use rgba() with hardcoded values -- use design token variables
CSS side-effect imports need type declarations; Vite's '/// ' covers this in TS 6
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
web/src/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
PostToolUse hook (scripts/check_web_design_system.py) runs automatically on every Edit/Write to web/src/ files; fix all violations before proceeding
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
web/src/**/__tests__/**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
Property-based testing in React uses fast-check (fc.assert + fc.property); integrated with Vitest
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
🧠 Learnings (5)
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📚 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:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.stories.{tsx,ts} : Use 'storybook/test' (not 'storybook/test'), 'storybook/actions' (not 'storybook/addon-actions') in Storybook 10
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/__tests__/**/*.{tsx,ts} : Property-based testing in React uses fast-check (fc.assert + fc.property); integrated with Vitest
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.stories.{tsx,ts} : Use 'parameters.a11y.test: "error" | "todo" | "off"' in Storybook 10 to enforce WCAG compliance
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx (1)
161-171: 🧹 Nitpick | 🔵 TrivialTest validates accessibility attributes but not actual focus trap behavior.
This test confirms focusable elements exist and
aria-modal="true"is set, but it doesn't verify that keyboard navigation (Tab/Shift+Tab) actually cycles within the dialog without escaping. Consider adding tab-cycling assertions usinguserEvent.tab()to confirm focus wraps correctly at boundaries.♻️ Suggested enhancement for actual focus trap verification
- it('focus is trapped within the drawer', () => { + it('focus is trapped within the drawer', async () => { + const user = userEvent.setup() renderDrawer() const dialog = screen.getByRole('dialog') - // The first focusable element should receive focus when the drawer opens const focusableElements = dialog.querySelectorAll<HTMLElement>( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', ) expect(focusableElements.length).toBeGreaterThan(0) - // The dialog should have aria-modal="true" to indicate focus trapping expect(dialog).toHaveAttribute('aria-modal', 'true') + + // Verify focus cycles within dialog + focusableElements[0].focus() + for (let i = 0; i < focusableElements.length + 1; i++) { + await user.tab() + expect(dialog.contains(document.activeElement)).toBe(true) + } + // Verify reverse cycling + for (let i = 0; i < focusableElements.length + 1; i++) { + await user.tab({ shift: true }) + expect(dialog.contains(document.activeElement)).toBe(true) + } })
🤖 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/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx`:
- Around line 99-102: The test is brittle because it queries a Tailwind CSS
class ('.animate-spin'); update the component that renders the Loader2 spinner
(e.g., the spinner inside ApprovalDetailDrawer / Loader component) to add a
stable attribute like data-testid="approval-spinner" or an appropriate ARIA
role, then change the test in ApprovalDetailDrawer.test.tsx to find the spinner
via screen.getByTestId('approval-spinner') or getByRole instead of
dialog.querySelector('.animate-spin'); this removes coupling to styling and
makes the test resilient to CSS changes.
🪄 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: c7dfb0c2-f94b-470c-a90c-ccece0851fbf
📒 Files selected for processing (1)
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📜 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)
- GitHub Check: Dashboard Test
- GitHub Check: Build Web
- GitHub Check: Build Sandbox
- GitHub Check: Build Backend
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (3)
web/src/**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
web/src/**/*.{tsx,ts}: Always reuse existing components from web/src/components/ui/ before creating new ones
Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files
Use font-sans or font-mono (maps to Geist tokens); never set fontFamily directly
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; never hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values
Do not recreate status dots inline -- use
Do not build card-with-header layouts from scratch -- use
Do not create metric displays with 'text-metric font-bold' -- use
Do not render initials circles manually -- use
Do not create complex (>8 line) JSX inside .map() -- extract to a shared component
Do not use rgba() with hardcoded values -- use design token variables
CSS side-effect imports need type declarations; Vite's '/// ' covers this in TS 6
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
web/src/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
PostToolUse hook (scripts/check_web_design_system.py) runs automatically on every Edit/Write to web/src/ files; fix all violations before proceeding
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
web/src/**/__tests__/**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
Property-based testing in React uses fast-check (fc.assert + fc.property); integrated with Vitest
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
🔇 Additional comments (4)
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx (4)
75-78: LGTM! Toast store reset now uses proper state management.The
useToastStore.setState({ toasts: [] })correctly resets the store through Zustand's public API rather than direct array mutation, ensuring reliable state isolation between tests.
184-188: LGTM! Test now covers both action buttons.Both approve and reject button assertions are present, ensuring complete regression coverage for the non-pending approval state.
47-51: LGTM! Mock handlers are well-typed.The handlers use proper Vitest mock typing with
vi.fn<...>()and sensible default return values for async operations.
148-159: LGTM! Toast validation correctly uses store API.The test properly verifies the toast error flow by:
- Confirming
onRejectis not called when reason is empty- Checking the actual toast store state for the expected error message
This aligns with the component implementation that calls
useToastStore.getState().add({ variant: 'error', title: 'Please provide a rejection reason' }).
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
♻️ Duplicate comments (3)
web/src/pages/approvals/ApprovalDetailDrawer.tsx (2)
289-294:⚠️ Potential issue | 🟠 MajorRender metadata values defensively.
approval.metadatavalues could be nested objects or arrays. Rendering{value}directly will throw a React error if the value is not a primitive, crashing the drawer.🛡️ Safer metadata rendering
+function formatMetadataValue(value: unknown): string { + if (value == null) return '—' + return typeof value === 'object' ? JSON.stringify(value) : String(value) +} + ... - <dd className="text-secondary">{value}</dd> + <dd className="text-secondary">{formatMetadataValue(value)}</dd>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx` around lines 289 - 294, In ApprovalDetailDrawer where approval.metadata is iterated (Object.entries(approval.metadata).map(...)), render metadata values defensively instead of injecting {value} directly: detect primitives and render them as-is, otherwise serialize complex values (arrays/objects) with JSON.stringify (with a safe fallback like "[object]" or String(value) if stringify fails) or render a compact preview; ensure this change is applied inside the map callback that returns the <div key={key} ...> so nested objects/arrays won't crash the drawer.
156-179: 🧹 Nitpick | 🔵 TrivialConsider using the shared
Drawerprimitive.This implementation manually handles backdrop animation, focus trapping, and Escape key behavior. If
web/src/components/ui/drawer.tsxprovides these features, composing it would reduce code duplication and ensure consistent modal behavior across the dashboard.As per coding guidelines: "Always reuse existing components from web/src/components/ui/ before creating new ones".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx` around lines 156 - 179, This file implements a custom backdrop, panel, focus trap and Escape handling via motion.div, motion.aside and panelRef in ApprovalDetailDrawer.tsx; replace this manual implementation by composing the shared Drawer primitive from web/src/components/ui/drawer.tsx: use Drawer (or Drawer.Root/Drawer.Portal/Drawer.Content as the primitive exposes) instead of the motion backdrop and motion.aside, forward panelRef (or let the Drawer handle focus) and pass onClose as the Drawer close handler, preserve aria-modal/role/aria-label with approval ? `Approval detail: ${approval.title}` : 'Approval detail', and ensure any animation props (PANEL_VARIANTS/overlayBackdrop) are applied to the Drawer slots or removed if the primitive provides them so behavior and accessibility (Escape/backdrop click/focus trap) come from the shared component.web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx (1)
158-168: 🧹 Nitpick | 🔵 TrivialFocus trap test does not validate actual trap behavior.
The test only verifies that focusable elements exist and
aria-modal="true"is set. A broken Tab loop would still pass. Consider adding keyboard assertions to verify focus cycling.🧪 More robust focus trap test
- it('focus is trapped within the drawer', () => { + it('focus is trapped within the drawer', async () => { + const user = userEvent.setup() renderDrawer() const dialog = screen.getByRole('dialog') - // The first focusable element should receive focus when the drawer opens const focusableElements = dialog.querySelectorAll<HTMLElement>( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', ) expect(focusableElements.length).toBeGreaterThan(0) - // The dialog should have aria-modal="true" to indicate focus trapping expect(dialog).toHaveAttribute('aria-modal', 'true') + + // Verify first element is focused on open + expect(document.activeElement).toBe(focusableElements[0]) + + // Tab through all elements and verify focus wraps + for (let i = 0; i < focusableElements.length; i++) { + await user.tab() + } + // After cycling through all, should wrap to first + expect(document.activeElement).toBe(focusableElements[0]) })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx` around lines 158 - 168, The test for focus trapping in ApprovalDetailDrawer is insufficient; update the test that uses renderDrawer and dialog (screen.getByRole('dialog')) to simulate keyboard Tab and Shift+Tab events (e.g., via userEvent.tab or fireEvent.keyDown) and assert that focus moves through the focusableElements list and wraps from last back to first and vice versa; specifically, identify the first and last elements from the focusableElements NodeList and add assertions that pressing Tab from the last returns focus to the first and pressing Shift+Tab from the first moves focus to the last to verify actual trap behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx`:
- Around line 158-168: The test for focus trapping in ApprovalDetailDrawer is
insufficient; update the test that uses renderDrawer and dialog
(screen.getByRole('dialog')) to simulate keyboard Tab and Shift+Tab events
(e.g., via userEvent.tab or fireEvent.keyDown) and assert that focus moves
through the focusableElements list and wraps from last back to first and vice
versa; specifically, identify the first and last elements from the
focusableElements NodeList and add assertions that pressing Tab from the last
returns focus to the first and pressing Shift+Tab from the first moves focus to
the last to verify actual trap behavior.
In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx`:
- Around line 289-294: In ApprovalDetailDrawer where approval.metadata is
iterated (Object.entries(approval.metadata).map(...)), render metadata values
defensively instead of injecting {value} directly: detect primitives and render
them as-is, otherwise serialize complex values (arrays/objects) with
JSON.stringify (with a safe fallback like "[object]" or String(value) if
stringify fails) or render a compact preview; ensure this change is applied
inside the map callback that returns the <div key={key} ...> so nested
objects/arrays won't crash the drawer.
- Around line 156-179: This file implements a custom backdrop, panel, focus trap
and Escape handling via motion.div, motion.aside and panelRef in
ApprovalDetailDrawer.tsx; replace this manual implementation by composing the
shared Drawer primitive from web/src/components/ui/drawer.tsx: use Drawer (or
Drawer.Root/Drawer.Portal/Drawer.Content as the primitive exposes) instead of
the motion backdrop and motion.aside, forward panelRef (or let the Drawer handle
focus) and pass onClose as the Drawer close handler, preserve
aria-modal/role/aria-label with approval ? `Approval detail: ${approval.title}`
: 'Approval detail', and ensure any animation props
(PANEL_VARIANTS/overlayBackdrop) are applied to the Drawer slots or removed if
the primitive provides them so behavior and accessibility (Escape/backdrop
click/focus trap) come from the shared component.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: dec49b3a-6d17-4323-9ac7-c7ea6ca1dfa8
📒 Files selected for processing (2)
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📜 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)
- GitHub Check: Dashboard Test
- GitHub Check: Build Web
- GitHub Check: Build Backend
- GitHub Check: Build Sandbox
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (3)
web/src/**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
web/src/**/*.{tsx,ts}: Always reuse existing components from web/src/components/ui/ before creating new ones
Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files
Use font-sans or font-mono (maps to Geist tokens); never set fontFamily directly
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; never hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values
Do not recreate status dots inline -- use
Do not build card-with-header layouts from scratch -- use
Do not create metric displays with 'text-metric font-bold' -- use
Do not render initials circles manually -- use
Do not create complex (>8 line) JSX inside .map() -- extract to a shared component
Do not use rgba() with hardcoded values -- use design token variables
CSS side-effect imports need type declarations; Vite's '/// ' covers this in TS 6
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
web/src/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
PostToolUse hook (scripts/check_web_design_system.py) runs automatically on every Edit/Write to web/src/ files; fix all violations before proceeding
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
web/src/**/__tests__/**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
Property-based testing in React uses fast-check (fc.assert + fc.property); integrated with Vitest
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
🧠 Learnings (9)
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.stories.{tsx,ts} : Use 'storybook/test' (not 'storybook/test'), 'storybook/actions' (not 'storybook/addon-actions') in Storybook 10
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : 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/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not use rgba() with hardcoded values -- use design token variables
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not create metric displays with 'text-metric font-bold' -- use <MetricCard>
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Always reuse existing components from web/src/components/ui/ before creating new ones
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not create complex (>8 line) JSX inside .map() -- extract to a shared component
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
🔇 Additional comments (12)
web/src/pages/approvals/ApprovalDetailDrawer.tsx (7)
1-19: Imports and component structure look good.The imports are well-organized, using path aliases consistently. The component properly separates concerns with animation variants, style mappings, and the main component logic.
70-81: Escape key handling is correct.The fix properly checks
approveOpen || rejectOpenstate to prevent closing when confirmation dialogs are open, addressing the previous review feedback.
83-118: Focus trap implementation is functional.The focus trap correctly:
- Stores the opener element for focus restoration
- Focuses the first focusable element on mount
- Wraps Tab/Shift+Tab at boundaries
- Restores focus on cleanup
120-150: Approve/reject handlers are well-implemented.Both handlers properly:
- Guard against null approval
- Manage submitting state
- Trim and validate input (reject requires reason)
- Show error toasts on failure with descriptive messages
- Reset state on success
180-193: Loading and error states render correctly.The loading state now displays within the drawer shell (not returning null), and the error state provides clear feedback with a close action. This addresses the previous review about unreachable loading state.
300-322: Footer actions correctly guard on pending status.The approve/reject buttons only render when
isPendingis true, properly hiding actions for already-decided approvals.
371-381: MetaField helper is clean and reusable.The helper component is concise (<8 lines), uses semantic classes, and properly marks the icon as
aria-hidden.web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx (5)
7-45: Mock components are appropriate for test isolation.The framer-motion mocks correctly bypass animation timing while preserving essential props (className, role, aria attributes, ref). This is a good pattern for component testing.
75-78: Toast state reset uses proper store API.Using
useToastStore.setState({ toasts: [] })correctly resets the store state between tests, addressing the previous feedback about direct array mutation.
90-100: Loading state test uses robust selector.Querying by
role="status"with accessible name"Loading approval"is more resilient than CSS class selectors, making the test less brittle to styling changes.
145-156: Reject validation test is thorough.The test correctly verifies that:
onRejectis not called when reason is empty- A toast with the expected title is shown
This properly covers the validation behavior in
handleReject.
181-185: Non-pending approval test now checks both buttons.Both approve and reject button absence assertions are present, ensuring complete coverage for the hidden actions case.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
web/src/pages/approvals/ApprovalDetailDrawer.tsx (1)
289-296: 🧹 Nitpick | 🔵 TrivialNested object values render as "[object Object]".
The current handling uses
String(value)for non-strings, which produces unhelpful output for nested objects. Consider usingJSON.stringifyfor object values to provide readable output.🔧 Suggested improvement
<dd className="text-secondary"> - {typeof value === 'string' ? value : String(value)} + {typeof value === 'object' && value !== null + ? JSON.stringify(value) + : String(value ?? '—')} </dd>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx` around lines 289 - 296, The metadata renderer in ApprovalDetailDrawer maps approval.metadata entries and currently uses typeof value === 'string' ? value : String(value), which renders nested objects as "[object Object]"; update the rendering logic inside the Object.entries(approval.metadata).map callback to detect objects (and null/undefined) and use JSON.stringify(value, null, 2) (or JSON.stringify(value) if compact output is preferred) for object/array values while preserving plain strings as-is, and ensure the key for the div remains key={key} so the mapped items (in the ApprovalDetailDrawer component) continue to render correctly.
🤖 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/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx`:
- Around line 145-156: Add tests covering successful approve/reject flows and
Escape-key behavior: extend ApprovalDetailDrawer.test.tsx to include three new
specs that use renderDrawer() and userEvent to (1) open the approve dialog, type
into the "Approval comment" textarea, confirm, and assert
defaultHandlers.onApprove was called with 'test-1' and { comment: 'Looks good'
}, (2) open the reject dialog, type into the "Rejection reason" textarea,
confirm, and assert defaultHandlers.onReject was called with 'test-1' and {
reason: 'Missing documentation' }, and (3) open a confirm dialog (e.g., via
clicking the "approve" button), press Escape with user.keyboard('{Escape}'), and
assert defaultHandlers.onClose was not called and the confirm dialog text (e.g.,
'Approve Action') remains in the document; use the same helpers (renderDrawer,
defaultHandlers, useToastStore) and role/label queries as in existing tests.
---
Duplicate comments:
In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx`:
- Around line 289-296: The metadata renderer in ApprovalDetailDrawer maps
approval.metadata entries and currently uses typeof value === 'string' ? value :
String(value), which renders nested objects as "[object Object]"; update the
rendering logic inside the Object.entries(approval.metadata).map callback to
detect objects (and null/undefined) and use JSON.stringify(value, null, 2) (or
JSON.stringify(value) if compact output is preferred) for object/array values
while preserving plain strings as-is, and ensure the key for the div remains
key={key} so the mapped items (in the ApprovalDetailDrawer component) continue
to render correctly.
🪄 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: d39ab5cf-41cc-4645-a959-b2ca5a6f7b92
📒 Files selected for processing (2)
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
web/src/**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
web/src/**/*.{tsx,ts}: Always reuse existing components from web/src/components/ui/ before creating new ones
Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files
Use font-sans or font-mono (maps to Geist tokens); never set fontFamily directly
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; never hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values
Do not recreate status dots inline -- use
Do not build card-with-header layouts from scratch -- use
Do not create metric displays with 'text-metric font-bold' -- use
Do not render initials circles manually -- use
Do not create complex (>8 line) JSX inside .map() -- extract to a shared component
Do not use rgba() with hardcoded values -- use design token variables
CSS side-effect imports need type declarations; Vite's '/// ' covers this in TS 6
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
web/src/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
PostToolUse hook (scripts/check_web_design_system.py) runs automatically on every Edit/Write to web/src/ files; fix all violations before proceeding
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
web/src/**/__tests__/**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
Property-based testing in React uses fast-check (fc.assert + fc.property); integrated with Vitest
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
🧠 Learnings (11)
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.stories.{tsx,ts} : Use 'storybook/test' (not 'storybook/test'), 'storybook/actions' (not 'storybook/addon-actions') in Storybook 10
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📚 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:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/__tests__/**/*.{tsx,ts} : Property-based testing in React uses fast-check (fc.assert + fc.property); integrated with Vitest
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : 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/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not use rgba() with hardcoded values -- use design token variables
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not create metric displays with 'text-metric font-bold' -- use <MetricCard>
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Always reuse existing components from web/src/components/ui/ before creating new ones
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-27T17:07:16.441Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T17:07:16.441Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not create complex (>8 line) JSX inside .map() -- extract to a shared component
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
🔇 Additional comments (10)
web/src/pages/approvals/ApprovalDetailDrawer.tsx (6)
1-48: LGTM!Imports are well-organized. The animation variants and risk color mappings correctly use semantic design tokens (
bg-danger,bg-warning,bg-accent, etc.) for consistent theming.
70-81: LGTM!The Escape key handler correctly gates on
approveOpen || rejectOpento prevent closing the drawer when a confirmation dialog is open. Dependencies are properly included.
83-118: LGTM!Focus trap implementation is well-structured: saves opener reference, focuses first element on mount, handles Tab cycling for both forward and backward navigation, and properly restores focus on cleanup.
120-151: LGTM!Approve/reject handlers correctly validate input (rejection requires non-empty reason), show error toasts on failure, and reset state appropriately. The async flow with submitting state prevents double-submissions.
156-184: LGTM!Loading and error states render correctly within the drawer shell. The
showLoadingStatelogic ensures the spinner displays while data is loading, and the error state provides a clear message with a close action.
329-368: LGTM!ConfirmDialog usage for approve/reject flows is correct. The dialogs properly reset their input state when closed via
onOpenChange, and theloadingprop prevents interaction during submission.web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx (4)
7-45: LGTM!Clean mock setup for framer-motion. The mock components preserve essential props (className, role, aria attributes, ref) needed for testing while eliminating animation timing issues.
75-78: LGTM!Toast store reset now uses
setStateAPI which properly triggers reactivity, addressing the previous direct mutation issue.
158-180: LGTM!Focus trap test now properly validates Tab cycling behavior by asserting forward Tab wraps last→first and Shift+Tab wraps first→last. This provides real coverage of the trap mechanics.
193-197: LGTM!Test now asserts both approve and reject buttons are hidden for non-pending approvals, providing complete regression coverage for the conditional footer.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx`:
- Around line 53-73: The tests currently never exercise the conditional branches
because renderDrawer (which uses makeApproval and renders ApprovalDetailDrawer)
relies on defaults with expires_at, decided_by, decision_reason, task_id,
metadata null/empty; add new unit tests that call renderDrawer with overrides to
set status (e.g., 'approved'), expires_at and seconds_remaining, decided_by and
decided_at, decision_reason, task_id, and metadata (including a nested object)
so the component renders the expiry countdown, Decided By/At fields, decision
reason, linked task, and metadata key/value pairs (assert visible plain strings
and JSON.stringify output for nested objects).
- Around line 158-174: Add tests covering the error paths for
ApprovalDetailDrawer by mocking the handlers to reject and asserting toast
errors: in the test file add two cases that use
defaultHandlers.onApprove.mockRejectedValueOnce(...) and
defaultHandlers.onReject.mockRejectedValueOnce(...), renderDrawer(), trigger the
approve/reject flows (click the open button, fill the comment/reason where
needed, then click the confirm button) and then assert
useToastStore.getState().toasts contains a toast with title 'Failed to approve'
and 'Failed to reject' respectively; ensure you reference the same symbols used
in the file (defaultHandlers, renderDrawer, onApprove, onReject, useToastStore)
so the tests exercise the component's catch blocks and error-toast behavior.
🪄 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: c02edec9-bf90-4959-a4be-09fc8fdf9506
📒 Files selected for processing (2)
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Preferinterfacefor defining object shapes in TypeScript; use Pydantic v2 models (BaseModel,model_validator,computed_field,ConfigDict) for Python
Use camelCase for variable and function names
Always use async/await for promises and preferasyncio.TaskGroupfor fan-out/fan-in parallel operations in Python
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
web/src/**/*.tsx
📄 CodeRabbit inference engine (CLAUDE.md)
ALWAYS reuse existing components from
web/src/components/ui/before creating new ones - check the component inventory in CLAUDE.md before building
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
web/src/**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use fast-check (
fc.assert+fc.property) for property-based testing in React tests
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
In
tsconfig.json: remove deprecatedbaseUrl; removeesModuleInterop(always true in TS 6); addtypes: []and explicitly list needed types; usemoduleResolution: 'bundler'or'nodenext'
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
**/*
📄 CodeRabbit inference engine (CLAUDE.md)
Never use
cdin Bash commands - the working directory is already set to the project root; use absolute paths or run commands directly
Files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
web/src/pages/**/*.tsx
📄 CodeRabbit inference engine (CLAUDE.md)
Place page-scoped sub-components in
pages/<page-name>/subdirectories (e.g.,pages/tasks/) instead of inline in the page file
Files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
🧠 Learnings (8)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:28:25.843Z
Learning: Applies to web/src/components/ui/**/*.stories.tsx : Every UI component story MUST demonstrate all component states: default, hover, loading, error, empty, and any variant props
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/).
📚 Learning: 2026-03-27T22:28:25.842Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:28:25.842Z
Learning: Applies to web/src/components/ui/**/*.tsx : Create a `.stories.tsx` file alongside every new shared component with all states (default, hover, loading, error, empty)
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📚 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:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📚 Learning: 2026-03-27T22:28:25.843Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:28:25.843Z
Learning: Applies to web/src/**/*.test.{ts,tsx} : Use fast-check (`fc.assert` + `fc.property`) for property-based testing in React tests
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📚 Learning: 2026-03-27T22:28:25.843Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:28:25.843Z
Learning: Applies to web/src/**/*.stories.tsx : Use `definePreview` from `storybook/react-vite` and `defineMain` from `storybook/react-vite/node` with explicit `framework` field; set `parameters.a11y.test: 'error' | 'todo' | 'off'` to enforce WCAG compliance
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📚 Learning: 2026-03-27T22:28:25.843Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:28:25.843Z
Learning: Applies to web/src/components/ui/**/*.stories.tsx : Every UI component story MUST demonstrate all component states: default, hover, loading, error, empty, and any variant props
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📚 Learning: 2026-03-27T22:28:25.842Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:28:25.842Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design tokens exclusively (Tailwind semantic classes like `text-foreground`, `bg-card`, `text-accent` and CSS variables like `var(--so-accent)`) - NEVER hardcode hex values, font-family, or pixel spacing values
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-27T22:28:25.842Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:28:25.842Z
Learning: Applies to web/src/**/*.tsx : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones - check the component inventory in CLAUDE.md before building
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
🔇 Additional comments (2)
web/src/pages/approvals/ApprovalDetailDrawer.tsx (1)
1-387: LGTM!The component is well-implemented with comprehensive accessibility support (focus trap, Escape handling, ARIA attributes), defensive metadata rendering, proper loading/error states, and design token usage. Previous review feedback has been addressed effectively.
The outstanding suggestion to use the shared
Drawerprimitive fromweb/src/components/ui/drawer.tsxwould reduce the custom a11y surface and ensure consistency, but that was already noted in a previous review.web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx (1)
80-228: LGTM on core test coverage!Good coverage of the essential flows:
- Rendering states (data, loading, error)
- User interactions (close, Escape, approve/reject dialogs)
- Validation (reject requires reason)
- Accessibility (focus trap with Tab cycling, aria-modal)
- Conditional rendering (non-pending status hides action buttons)
Previous review feedback has been incorporated well, including the improved focus trap validation and toast state reset.
2e972b3 to
15bd14f
Compare
There was a problem hiding this comment.
Actionable comments posted: 13
♻️ Duplicate comments (10)
web/src/stores/approvals.ts (2)
182-195:⚠️ Potential issue | 🟠 Major
upsertApprovaldoes not prune non-pending items fromselectedIds.The previous review flagged this: when
handleWsEventupserts an approval that transitioned away frompending(e.g., decided by another user), the id remains inselectedIds. This causes the batch action bar to count and potentially submit stale ids.🧹 Suggested fix
upsertApproval: (approval) => { pendingTransitions.delete(approval.id) set((s) => { const idx = s.approvals.findIndex((a) => a.id === approval.id) const newApprovals = idx === -1 ? [approval, ...s.approvals] : [...s.approvals] if (idx !== -1) newApprovals[idx] = approval const selectedApproval = s.selectedApproval?.id === approval.id ? approval : s.selectedApproval + const selectedIds = new Set(s.selectedIds) + if (approval.status !== 'pending') selectedIds.delete(approval.id) return { approvals: newApprovals, selectedApproval, + selectedIds, ...(idx === -1 ? { total: s.total + 1 } : {}), } }) },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/stores/approvals.ts` around lines 182 - 195, When upserting an approval in upsertApproval, remove its id from selectedIds if the approval is no longer pending so stale ids don't remain selected; inside the set((s) => { ... }) compute a newSelectedIds = approval.status !== 'pending' ? s.selectedIds.filter(id => id !== approval.id) : s.selectedIds and return selectedIds: newSelectedIds along with approvals, selectedApproval, and total adjustments (keep existing logic that updates approvals and total). This ensures pendingTransitions, selectedIds, and selectedApproval handling in upsertApproval correctly prunes non-pending ids.
89-97:⚠️ Potential issue | 🔴 CriticalRace condition in
fetchApprovalremains unaddressed.The previous review flagged this: rapid detail panel navigation (A→B) can result in stale response A overwriting B's data if A's network call resolves later. This bug causes the wrong approval to display in the detail drawer.
🔁 Suggested fix using a request counter
+let activeDetailRequest = 0 + ... fetchApproval: async (id) => { + const requestId = ++activeDetailRequest set({ loadingDetail: true, detailError: null, selectedApproval: null }) try { const approval = await approvalsApi.getApproval(id) + if (requestId !== activeDetailRequest) return set({ selectedApproval: approval, loadingDetail: false, detailError: null }) } catch (err) { + if (requestId !== activeDetailRequest) return set({ loadingDetail: false, detailError: getErrorMessage(err) }) } },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/stores/approvals.ts` around lines 89 - 97, The fetchApproval function can have stale responses overwrite newer ones; add a monotonically incremented request counter in this approvals store (e.g., detailRequestId in state) that you increment before calling approvalsApi.getApproval(id), capture its current value in a local variable, then only call set({ selectedApproval: approval, loadingDetail: false, detailError: null }) if the captured request id still matches the state's detailRequestId; likewise when catching an error only set detailError/loadingDetail if the ids match. Update fetchApproval to increment detailRequestId at start, use approvalsApi.getApproval(id) as before, and guard all state writes with the request-id match to avoid the A→B race.web/src/__tests__/hooks/useApprovalsData.test.ts (1)
85-87: 🧹 Nitpick | 🔵 TrivialRemove redundant dynamic import in mocked module test.
Since
@/hooks/useWebSocketis already mocked at module scope, the dynamic import adds noise without extra coverage.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/__tests__/hooks/useApprovalsData.test.ts` around lines 85 - 87, Remove the redundant dynamic import of useWebSocket inside the test: delete the line "const { useWebSocket } = await import('@/hooks/useWebSocket')" and rely on the module-scoped mock already provided for '@/hooks/useWebSocket'; keep the call to renderHook(() => useApprovalsData()) (and any assertions) so the test uses the mocked useWebSocket via the existing jest.mock setup.web/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsx (1)
14-17:⚠️ Potential issue | 🟡 MinorAvoid
filterRow!to keep test failures explicit.Use an explicit null guard (
expect(filterRow).not.toBeNull()) and optional chaining for the selector call, so failures point to the missing DOM node directly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsx` around lines 14 - 17, Replace the non-null assertion on filterRow with an explicit null-guard assertion: call expect(filterRow).not.toBeNull() after you assign filterRow (from container.querySelector('.flex.items-center.gap-3')), then use optional chaining when selecting children (e.g., filterRow?.querySelectorAll('.h-8')) so the test failure clearly points to the missing DOM node; update the length assertion to use the optional-chained result (or assert on the defined NodeList) to avoid using the `!` operator in ApprovalsSkeleton.test.tsx.web/src/hooks/useApprovalsData.ts (1)
63-69:⚠️ Potential issue | 🟠 MajorStop hard-coding
limit: 200in the primary fetch path.Line 63 and Line 68 both enforce a fixed cap, which can silently drop approvals beyond the first 200 and make grouping/batch actions incomplete for larger queues.
Proposed direction
- useApprovalsStore.getState().fetchApprovals({ limit: 200 }) + useApprovalsStore.getState().fetchApprovals()- await useApprovalsStore.getState().fetchApprovals({ limit: 200 }) + await useApprovalsStore.getState().fetchApprovals()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/hooks/useApprovalsData.ts` around lines 63 - 69, The code is hard-coding fetchApprovals({ limit: 200 }) in two places which can drop items; replace the literal 200 by a configurable source and use it in both the initial fetch and poll function — for example read a page size or limit from a settings value (e.g., an approvalsPageSize constant, an env/config value, or a value exposed by useApprovalsStore like getState().settings.approvalsLimit) or allow fetchApprovals() to default to no limit; update both useApprovalsStore.getState().fetchApprovals(...) calls (including the pollFn) to use that variable or omit the limit so the store's default is used.web/src/__tests__/pages/approvals/ApprovalTimeline.test.tsx (1)
37-39: 🧹 Nitpick | 🔵 TrivialPrefer indexed assertions for clearer failure diagnostics.
The
forEachexpectation pattern hides which timeline item failed; explicit indexed checks are easier to debug.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/__tests__/pages/approvals/ApprovalTimeline.test.tsx` around lines 37 - 39, Replace the unindexed forEach assertion with explicit indexed assertions so test failures show which item failed: in ApprovalTimeline.test.tsx, iterate using a standard for loop (for (let i = 0; i < items.length; i++)) and call expect(items[i].querySelector('.animate-pulse')).not.toBeInTheDocument() for each index (or use items[i] in an indexed .map/assert pattern) instead of items.forEach(...), so the failing index is clear in test output.web/src/pages/ApprovalsPage.tsx (3)
145-149:⚠️ Potential issue | 🟠 MajorInclude
failedReasonsin partial batch-failure toasts.
batchApprove()andbatchReject()already return recovery text, but these warning branches drop it. When the batch cap or server-side validation fails, users still get no clue how to fix the problem.Also applies to: 168-172
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/ApprovalsPage.tsx` around lines 145 - 149, The warning toasts in ApprovalsPage (the branches that call useToastStore.getState().add when result.failed > 0) omit the server-returned recovery text; include result.failedReasons (or join it into a readable string) in the toast title or description so partial batch failures show actionable reasons to the user. Update both places that show the warning toast (the batchApprove/batchReject response handlers) to append or set the failedReasons details in the message (e.g., “Approved X of Y. Z failed: <failedReasons>”) and ensure you handle empty/undefined failedReasons gracefully.
95-102:⚠️ Potential issue | 🟠 MajorSelecting a card still triggers two detail fetches.
handleSelectApproval()fetches immediately, and theselectedIdeffect below usually fetches again before the first request resolves. That doubles drawer-open traffic and increases stale-response races; keep a single fetch path.Also applies to: 113-117
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/ApprovalsPage.tsx` around lines 95 - 102, handleSelectApproval currently calls fetchApproval immediately and also updates the URL param, while the effect watching selectedId also calls fetchApproval, causing duplicate requests; fix by removing the immediate fetch from handleSelectApproval (leave setSearchParams only) so the single fetch path goes through the effect that reacts to selectedId, ensuring only one fetch per selection; update any tests or comments referencing handleSelectApproval’s behavior and keep references to setSearchParams, handleSelectApproval, selectedId effect, and fetchApproval so reviewers can locate the change.
266-314: 🛠️ Refactor suggestion | 🟠 MajorExtract the risk-group section renderer into
./approvals/ApprovalRiskGroupSection.tsx.This
.map()callback now owns selection math, bulk-select wiring, and card rendering. Pull it into a small page-scoped component so the page stops carrying the whole section inline and the group behavior becomes easier to test.As per coding guidelines,
Functions should be under 50 lines; files should be under 800 lines.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/ApprovalsPage.tsx` around lines 266 - 314, Extract the inline .map() renderer into a new component ApprovalRiskGroupSection (file ./approvals/ApprovalRiskGroupSection.tsx) that accepts props: riskLevel, items (approval[]), selectedIds (Set), selectAllInGroup, deselectAllInGroup, toggleSelection, handleSelectApproval, handleApproveOne, handleRejectOne; inside the component use getRiskLevelIcon, getRiskLevelLabel, SectionCard, ErrorBoundary, StaggerGroup, StaggerItem and ApprovalCard exactly as in the original JSX and compute pendingInGroup, pendingIds, and allSelected locally before rendering; replace the original .map() callback with a call to <ApprovalRiskGroupSection ... /> passing the referenced symbols (selectedIds, selectAllInGroup, deselectAllInGroup, toggleSelection, handleSelectApproval, handleApproveOne, handleRejectOne) so page logic is unchanged but the section is moved into the new component for easier testing.web/src/pages/approvals/ApprovalCard.tsx (1)
19-80: 🛠️ Refactor suggestion | 🟠 MajorPull the shared style maps and countdown behavior out of
ApprovalCard.
DOT_COLOR_CLASSES/URGENCY_BADGE_CLASSESalready belong to@/utils/approvals, and the ticking countdown logic makes this component carry a lot of non-render behavior inline. Import the shared maps and move the timer into a small helper/hook so the card stops accumulating infrastructure code.As per coding guidelines,
Functions should be under 50 lines; files should be under 800 lines.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/approvals/ApprovalCard.tsx` around lines 19 - 80, ApprovalCard currently inlines DOT_COLOR_CLASSES, URGENCY_BADGE_CLASSES and the ticking countdown logic; pull the style maps into imports from "@/utils/approvals" (replace the local DOT_COLOR_CLASSES and URGENCY_BADGE_CLASSES with those imports) and extract the countdown logic into a small hook (e.g. useApprovalCountdown or useCountdown) that accepts approval.seconds_remaining and approval.status and returns the current countdown and any cleanup; then update ApprovalCard to call the new hook (removing prevSecondsRef, setCountdown, shouldTick and the interval useEffect) so the component only renders and uses useFlash, while keeping getRiskLevelColor/getUrgencyColor and onSelect/onApprove/onReject handlers unchanged.
🤖 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/src/__tests__/hooks/useApprovalsData.test.ts`:
- Around line 51-55: The test in useApprovalsData.test.ts tightly asserts a
specific pagination payload ({ limit: 200 }) on mount; update the assertion to
avoid coupling to the current bug by verifying that mockFetchApprovals is
invoked on mount without asserting the exact limit. Replace
expect(mockFetchApprovals).toHaveBeenCalledWith({ limit: 200 }) with a call
existence check such as expect(mockFetchApprovals).toHaveBeenCalled() or a more
flexible matcher like
expect(mockFetchApprovals).toHaveBeenCalledWith(expect.any(Object)) so the test
ensures useApprovalsData triggers a fetch without depending on the hard-coded
limit.
In `@web/src/__tests__/pages/approvals/ApprovalTimeline.test.tsx`:
- Around line 71-73: The test ApprovalTimeline.test.tsx uses a locale-dependent
string check (expect(items[0]!.textContent).toContain('Mar')) which makes it
fragile; update the assertions to use a locale-agnostic check by either
formatting the expected date with the same formatter used by the
ApprovalTimeline component (or the utility it uses) or by asserting on a stable
representation (e.g., ISO date substring or numeric timestamp) instead of 'Mar';
locate the items array access in the test and replace the toContain('Mar')
assertions with a comparison that uses the component's date formatter function
or a deterministic date string so the test is no longer locale-sensitive.
In `@web/src/__tests__/stores/approvals.test.ts`:
- Around line 355-386: Add a unit test that verifies batchApprove enforces
MAX_BATCH_SIZE (50) by calling useApprovalsStore.getState().batchApprove with 51
ids and asserting the returned result reports 0 succeeded, 51 failed and
includes the failure message about exceeding the max batch size; reference
MAX_BATCH_SIZE and the batchApprove method on useApprovalsStore to locate where
to add the test and assert the failedReasons contains the "Batch size exceeds
maximum of 50" message.
- Around line 43-77: Add a unit test for fetchApprovals that verifies the store
preserves locally modified items in pendingTransitions: set an initial approval
in useApprovalsStore state, call the optimistic update method
(optimisticApprove) to add it to pendingTransitions and change its status, mock
api.listApprovals to return stale server data for that item, then call
useApprovalsStore.getState().fetchApprovals() and assert the approval's status
remains the optimistic value; reference useApprovalsStore.fetchApprovals,
optimisticApprove, pendingTransitions and the mocked api.listApprovals in the
test.
In `@web/src/__tests__/utils/approvals.test.ts`:
- Around line 47-55: The test for getRiskLevelIcon currently asserts internal
React implementation detail ($$typeof); update it to assert the public contract
instead by either (a) comparing the returned value to the known expected icon
reference for each ApprovalRiskLevel (e.g., expectedIconMap[level] ===
getRiskLevelIcon(level)), or (b) rendering the returned component with React
Testing Library (render(React.createElement(getRiskLevelIcon(level))) and
asserting it renders without error or finds an expected role/text). Replace the
$$typeof checks with one of these public-facing assertions referencing
getRiskLevelIcon and ApprovalRiskLevel.
In `@web/src/hooks/useApprovalsData.ts`:
- Around line 40-117: The useApprovalsData function is over the 50-line limit
and mixes store selectors, polling, and WebSocket bindings; split it into
focused helpers: extract the repeated store selectors into a new hook (e.g.,
useApprovalsSelectors) that returns approvals, selectedApproval, total, loading,
loadingDetail, error, detailError, fetchApproval, approveOne, rejectOne,
optimisticApprove, optimisticReject, selectedIds, toggleSelection,
selectAllInGroup, deselectAllInGroup, clearSelection, batchApprove, batchReject
(pulling from useApprovalsStore). Move polling logic into a new hook (e.g.,
useApprovalsPolling) that encapsulates pollFn and usePolling and calls
useApprovalsStore.getState().fetchApprovals({ limit: 200 }). Move WebSocket
setup into a new hook (e.g., useApprovalsWebSocket) that creates bindings using
APPROVAL_CHANNELS and handleWsEvent and returns { connected, setupError } from
useWebSocket. Finally, simplify useApprovalsData to compose those three hooks
and return the combined values.
In `@web/src/pages/approvals/ApprovalDetailDrawer.stories.tsx`:
- Around line 42-78: Add an Error story to ApprovalDetailDrawer.stories.tsx to
exercise the component's error state: define a new exported story named Error
that supplies args matching the component props used in tests (set approval to
null and error to a descriptive string like "Failed to load approval details"),
so the ApprovalDetailDrawer story set (exports such as Pending, Approved,
Rejected, Loading, NoMetadata) includes the error case and mirrors the error
prop exercised in the tests.
In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx`:
- Around line 59-65: The confirm dialogs (approveOpen and rejectOpen) remain
open across store/WebSocket updates and can submit stale actions when
approval.status changes; update ApprovalDetailDrawer to reset approveOpen and
rejectOpen whenever approval?.status !== 'pending' (i.e., derive/reset them from
isPending or use useEffect watching approval?.status) and add a fresh status
check at the start of both approve/reject handlers (the functions that perform
the submit, e.g., the approve/reject submit handlers referenced in this file) to
bail out if approval.status is no longer 'pending' before proceeding. Ensure you
reference approveOpen, rejectOpen, isPending and the submit handler names in
your changes so dialogs close on status change and cannot submit stale actions.
- Around line 83-118: The focus-trap useEffect in ApprovalDetailDrawer only runs
on changes to open and therefore never engages when the drawer first mounts
showing only the spinner (no focusable descendants); split the logic into two
effects: 1) a tiny opener-restore effect that runs on open (and cleans up by
restoring openerRef.current) and 2) the focus-in-panel/focus-trap effect that
depends on open plus loading/detailError/approval so it re-evaluates when real
content arrives; in the focus-trap effect (which references panelRef and handles
nodes/focus), if no focusable descendants exist, make the panel itself focusable
(set tabindex="-1" on panelRef.current) and call panelRef.current.focus(),
otherwise focus the first focusable node; keep the existing handleTab logic and
cleanup (remove keydown handler and restore any tabindex change if you add one).
Ensure you update references to openerRef, panelRef, handleTab, and the
useEffect dependency arrays accordingly.
- Around line 186-194: The error view currently renders while the old approval
detail may still render because `detailError` and `approval` are independent;
modify the render condition for the approval body in ApprovalDetailDrawer (the
block that currently checks `!showLoadingState && approval`) to also require no
error (e.g., `!detailError && !showLoadingState && approval`) so that when
`detailError` is true the centered error state is shown alone and the stale
approval content is not rendered underneath.
In `@web/src/pages/approvals/ApprovalFilterBar.tsx`:
- Line 119: The FilterPill component currently uses an inline object type for
its props; define a named interface (e.g., interface FilterPillProps { label:
string; onRemove: () => void }) and use it in the function signature (function
FilterPill(props: FilterPillProps) or function FilterPill({ label, onRemove }:
FilterPillProps)) to comply with the project's TypeScript object-shape rule and
ensure consistent typing for FilterPill.
- Around line 16-117: ApprovalFilterBar is too large; extract the filter row and
the active-pills area into two small subcomponents (e.g., ApprovalFilterRow and
ApprovalActivePills) to keep each function under 50 lines. Move the JSX for the
select/input/counts into ApprovalFilterRow and pass filters, actionTypes, and
onChange handlers (reuse updateFilter and clearFilters from ApprovalFilterBar or
lift their logic into small helpers like updateFilterInProps and
clearAllFilters) as props; move the active filter pills JSX into
ApprovalActivePills and pass filters and onRemove handlers (calling updateFilter
with undefined). Update ApprovalFilterBar to only compute hasActiveFilters,
define the minimal updateFilter/clearFilters helpers, and render
ApprovalFilterRow and ApprovalActivePills with the appropriate props.
- Around line 34-41: The ApprovalFilterBar component is using hardcoded utility
classes (e.g., h-8, w-48, gap-2, px-2, text-[10px]) which must be replaced with
semantic design tokens; update the JSX in ApprovalFilterBar (the select, inputs,
buttons and surrounding divs referenced by filters and updateFilter) to use the
project's semantic Tailwind classes (e.g., gap-section-gap, p-card,
text-foreground / text-xs-token, w-control / h-control or the equivalent token
names used across the codebase) or CSS variables (var(--so-...)) instead of the
numeric utilities, and apply the same replacements at the other noted blocks
(around lines referenced in the review: the other select/input/button groups) so
spacing, padding, and font-size use tokens consistently.
---
Duplicate comments:
In `@web/src/__tests__/hooks/useApprovalsData.test.ts`:
- Around line 85-87: Remove the redundant dynamic import of useWebSocket inside
the test: delete the line "const { useWebSocket } = await
import('@/hooks/useWebSocket')" and rely on the module-scoped mock already
provided for '@/hooks/useWebSocket'; keep the call to renderHook(() =>
useApprovalsData()) (and any assertions) so the test uses the mocked
useWebSocket via the existing jest.mock setup.
In `@web/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsx`:
- Around line 14-17: Replace the non-null assertion on filterRow with an
explicit null-guard assertion: call expect(filterRow).not.toBeNull() after you
assign filterRow (from container.querySelector('.flex.items-center.gap-3')),
then use optional chaining when selecting children (e.g.,
filterRow?.querySelectorAll('.h-8')) so the test failure clearly points to the
missing DOM node; update the length assertion to use the optional-chained result
(or assert on the defined NodeList) to avoid using the `!` operator in
ApprovalsSkeleton.test.tsx.
In `@web/src/__tests__/pages/approvals/ApprovalTimeline.test.tsx`:
- Around line 37-39: Replace the unindexed forEach assertion with explicit
indexed assertions so test failures show which item failed: in
ApprovalTimeline.test.tsx, iterate using a standard for loop (for (let i = 0; i
< items.length; i++)) and call
expect(items[i].querySelector('.animate-pulse')).not.toBeInTheDocument() for
each index (or use items[i] in an indexed .map/assert pattern) instead of
items.forEach(...), so the failing index is clear in test output.
In `@web/src/hooks/useApprovalsData.ts`:
- Around line 63-69: The code is hard-coding fetchApprovals({ limit: 200 }) in
two places which can drop items; replace the literal 200 by a configurable
source and use it in both the initial fetch and poll function — for example read
a page size or limit from a settings value (e.g., an approvalsPageSize constant,
an env/config value, or a value exposed by useApprovalsStore like
getState().settings.approvalsLimit) or allow fetchApprovals() to default to no
limit; update both useApprovalsStore.getState().fetchApprovals(...) calls
(including the pollFn) to use that variable or omit the limit so the store's
default is used.
In `@web/src/pages/approvals/ApprovalCard.tsx`:
- Around line 19-80: ApprovalCard currently inlines DOT_COLOR_CLASSES,
URGENCY_BADGE_CLASSES and the ticking countdown logic; pull the style maps into
imports from "@/utils/approvals" (replace the local DOT_COLOR_CLASSES and
URGENCY_BADGE_CLASSES with those imports) and extract the countdown logic into a
small hook (e.g. useApprovalCountdown or useCountdown) that accepts
approval.seconds_remaining and approval.status and returns the current countdown
and any cleanup; then update ApprovalCard to call the new hook (removing
prevSecondsRef, setCountdown, shouldTick and the interval useEffect) so the
component only renders and uses useFlash, while keeping
getRiskLevelColor/getUrgencyColor and onSelect/onApprove/onReject handlers
unchanged.
In `@web/src/pages/ApprovalsPage.tsx`:
- Around line 145-149: The warning toasts in ApprovalsPage (the branches that
call useToastStore.getState().add when result.failed > 0) omit the
server-returned recovery text; include result.failedReasons (or join it into a
readable string) in the toast title or description so partial batch failures
show actionable reasons to the user. Update both places that show the warning
toast (the batchApprove/batchReject response handlers) to append or set the
failedReasons details in the message (e.g., “Approved X of Y. Z failed:
<failedReasons>”) and ensure you handle empty/undefined failedReasons
gracefully.
- Around line 95-102: handleSelectApproval currently calls fetchApproval
immediately and also updates the URL param, while the effect watching selectedId
also calls fetchApproval, causing duplicate requests; fix by removing the
immediate fetch from handleSelectApproval (leave setSearchParams only) so the
single fetch path goes through the effect that reacts to selectedId, ensuring
only one fetch per selection; update any tests or comments referencing
handleSelectApproval’s behavior and keep references to setSearchParams,
handleSelectApproval, selectedId effect, and fetchApproval so reviewers can
locate the change.
- Around line 266-314: Extract the inline .map() renderer into a new component
ApprovalRiskGroupSection (file ./approvals/ApprovalRiskGroupSection.tsx) that
accepts props: riskLevel, items (approval[]), selectedIds (Set),
selectAllInGroup, deselectAllInGroup, toggleSelection, handleSelectApproval,
handleApproveOne, handleRejectOne; inside the component use getRiskLevelIcon,
getRiskLevelLabel, SectionCard, ErrorBoundary, StaggerGroup, StaggerItem and
ApprovalCard exactly as in the original JSX and compute pendingInGroup,
pendingIds, and allSelected locally before rendering; replace the original
.map() callback with a call to <ApprovalRiskGroupSection ... /> passing the
referenced symbols (selectedIds, selectAllInGroup, deselectAllInGroup,
toggleSelection, handleSelectApproval, handleApproveOne, handleRejectOne) so
page logic is unchanged but the section is moved into the new component for
easier testing.
In `@web/src/stores/approvals.ts`:
- Around line 182-195: When upserting an approval in upsertApproval, remove its
id from selectedIds if the approval is no longer pending so stale ids don't
remain selected; inside the set((s) => { ... }) compute a newSelectedIds =
approval.status !== 'pending' ? s.selectedIds.filter(id => id !== approval.id) :
s.selectedIds and return selectedIds: newSelectedIds along with approvals,
selectedApproval, and total adjustments (keep existing logic that updates
approvals and total). This ensures pendingTransitions, selectedIds, and
selectedApproval handling in upsertApproval correctly prunes non-pending ids.
- Around line 89-97: The fetchApproval function can have stale responses
overwrite newer ones; add a monotonically incremented request counter in this
approvals store (e.g., detailRequestId in state) that you increment before
calling approvalsApi.getApproval(id), capture its current value in a local
variable, then only call set({ selectedApproval: approval, loadingDetail: false,
detailError: null }) if the captured request id still matches the state's
detailRequestId; likewise when catching an error only set
detailError/loadingDetail if the ids match. Update fetchApproval to increment
detailRequestId at start, use approvalsApi.getApproval(id) as before, and guard
all state writes with the request-id match to avoid the A→B race.
🪄 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: 4d88d077-994b-4385-a5f3-3d052f8e8a7b
📒 Files selected for processing (29)
docs/design/brand-and-ux.mddocs/design/page-structure.mdweb/src/__tests__/helpers/factories.tsweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsxweb/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/__tests__/stores/approvals.test.tsweb/src/__tests__/utils/approvals.test.tsweb/src/hooks/useApprovalsData.tsweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/pages/approvals/ApprovalsSkeleton.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/approvals/BatchActionBar.tsxweb/src/stores/approvals.tsweb/src/utils/approvals.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). (6)
- GitHub Check: Dashboard Test
- GitHub Check: Build Sandbox
- GitHub Check: Build Web
- GitHub Check: Build Backend
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Preferinterfacefor defining object shapes in TypeScript, use Pydantic v2 models for Python
Always use async/await for promises; preferasyncio.TaskGroupfor parallel operations in Python
Use design tokens exclusively - no hardcoded hex colors, rgba values, font-family declarations, or pixel spacing values in.tsx/.tsfiles
Files:
web/src/pages/approvals/ApprovalsSkeleton.tsxweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/__tests__/helpers/factories.tsweb/src/pages/approvals/BatchActionBar.tsxweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.tsxweb/src/stores/approvals.tsweb/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/__tests__/stores/approvals.test.tsweb/src/hooks/useApprovalsData.tsweb/src/utils/approvals.ts
**/*.{ts,tsx,js}
📄 CodeRabbit inference engine (CLAUDE.md)
Use camelCase for variable names, function names, and identifiers
Files:
web/src/pages/approvals/ApprovalsSkeleton.tsxweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/__tests__/helpers/factories.tsweb/src/pages/approvals/BatchActionBar.tsxweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.tsxweb/src/stores/approvals.tsweb/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/__tests__/stores/approvals.test.tsweb/src/hooks/useApprovalsData.tsweb/src/utils/approvals.ts
**/*.{py,ts,tsx,go}
📄 CodeRabbit inference engine (CLAUDE.md)
Functions should be under 50 lines; files should be under 800 lines
Files:
web/src/pages/approvals/ApprovalsSkeleton.tsxweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/__tests__/helpers/factories.tsweb/src/pages/approvals/BatchActionBar.tsxweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.tsxweb/src/stores/approvals.tsweb/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/__tests__/stores/approvals.test.tsweb/src/hooks/useApprovalsData.tsweb/src/utils/approvals.ts
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Never hardcode colors, fonts, or spacing - always use Tailwind semantic classes (text-foreground, bg-card, p-card, gap-section-gap) or CSS variables (var(--so-accent))
Files:
web/src/pages/approvals/ApprovalsSkeleton.tsxweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/__tests__/helpers/factories.tsweb/src/pages/approvals/BatchActionBar.tsxweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.tsxweb/src/stores/approvals.tsweb/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/__tests__/stores/approvals.test.tsweb/src/hooks/useApprovalsData.tsweb/src/utils/approvals.ts
docs/design/**/*.md
📄 CodeRabbit inference engine (CLAUDE.md)
Update relevant
docs/design/pages whenever an approved deviation from the spec occurs during implementation
Files:
docs/design/page-structure.mddocs/design/brand-and-ux.md
**/*.md
📄 CodeRabbit inference engine (CLAUDE.md)
Do not use real vendor names in documentation, use generic names instead - vendor names only appear in design pages,
.claude/files, third-party imports, and provider presets
Files:
docs/design/page-structure.mddocs/design/brand-and-ux.md
🧠 Learnings (13)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:32:02.880Z
Learning: Applies to .github/workflows/*.yml : Dependabot: daily updates for uv + github-actions + npm + pre-commit + docker + gomod, grouped minor/patch, no auto-merge. Use `/review-dep-pr` to review Dependabot PRs before merging.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:49:53.264Z
Learning: Fix everything valid — never skip when review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes). No deferring, no 'out of scope' skipping.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:44:47.320Z
Learning: Applies to web/src/components/ui/** : Create a `.stories.tsx` file alongside every new UI component with all states (default, hover, loading, error, empty)
📚 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:
web/src/__tests__/hooks/useApprovalsData.test.tsweb/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/__tests__/utils/approvals.test.tsweb/src/__tests__/stores/approvals.test.ts
📚 Learning: 2026-03-18T08:23:08.912Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T08:23:08.912Z
Learning: When approved deviations occur, update the relevant `docs/design/` page to reflect the new reality.
Applied to files:
docs/design/page-structure.mddocs/design/brand-and-ux.md
📚 Learning: 2026-03-27T22:44:47.320Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:44:47.320Z
Learning: Applies to docs/design/**/*.md : Update relevant `docs/design/` pages whenever an approved deviation from the spec occurs during implementation
Applied to files:
docs/design/page-structure.mddocs/design/brand-and-ux.md
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to docs/design/**/*.md : Design specification pages in `docs/design/` must be consulted before implementing features (7 pages: index, agents, organization, communication, engine, memory, operations)
Applied to files:
docs/design/page-structure.md
📚 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:
docs/design/page-structure.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 docs/design/*.md : Design spec pages: 7 pages in `docs/design/` — index, agents, organization, communication, engine, memory, operations
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-03-21T11:08:01.542Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T11:08:01.542Z
Learning: At every phase of planning and implementation, be critical — actively look for ways to improve the design in the spirit of what we're building (robustness, correctness, simplicity, future-proofing where it's free). Surface improvements as suggestions, not silent changes — user decides.
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-03-27T22:44:47.320Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:44:47.320Z
Learning: Applies to web/src/components/ui/** : Create a `.stories.tsx` file alongside every new UI component with all states (default, hover, loading, error, empty)
Applied to files:
web/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/__tests__/stores/approvals.test.ts
📚 Learning: 2026-03-27T22:44:47.320Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:44:47.320Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, SectionCard, AgentCard, Avatar, Button, Toast, etc.) before creating new ones
Applied to files:
web/src/pages/approvals/BatchActionBar.tsxweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/pages/approvals/ApprovalFilterBar.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxdocs/design/brand-and-ux.md
📚 Learning: 2026-03-27T22:44:47.320Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:44:47.320Z
Learning: Applies to web/src/**/*.{ts,tsx} : Never hardcode colors, fonts, or spacing - always use Tailwind semantic classes (text-foreground, bg-card, p-card, gap-section-gap) or CSS variables (var(--so-accent))
Applied to files:
web/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalFilterBar.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxdocs/design/brand-and-ux.md
📚 Learning: 2026-03-27T22:44:47.320Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:44:47.320Z
Learning: Applies to **/*.{ts,tsx} : Use design tokens exclusively - no hardcoded hex colors, rgba values, font-family declarations, or pixel spacing values in `.tsx`/`.ts` files
Applied to files:
web/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalFilterBar.tsxweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxdocs/design/brand-and-ux.md
📚 Learning: 2026-03-27T22:44:47.320Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:44:47.320Z
Learning: Applies to web/src/pages/**/*.{tsx} : Lazy-load page components using React Router; place page-scoped sub-components in `pages/<page-name>/` subdirectories
Applied to files:
web/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/ApprovalsPage.tsx
🔇 Additional comments (26)
web/src/pages/approvals/ApprovalsSkeleton.tsx (1)
1-28: LGTM!Clean skeleton implementation with proper accessibility attributes (
role="status",aria-label), semantic Tailwind classes, and correct usage of the shared skeleton components. The structure matches test expectations (4 metrics, 3 cards).web/src/pages/approvals/ApprovalsSkeleton.stories.tsx (1)
1-15: LGTM!Story file is appropriately configured with accessibility testing (
parameters.a11y.test: 'error'). For a loading skeleton component, only the default state is applicable—hover/error/empty states don't apply.web/src/__tests__/helpers/factories.ts (1)
37-57: LGTM!The
makeApprovalfactory correctly provides all required fields from theApprovalResponseinterface with sensible defaults. The dynamiccreated_atand spread-based overrides pattern matches the existingmakeTaskfactory style.web/src/pages/approvals/BatchActionBar.tsx (1)
1-79: LGTM!Well-structured batch action bar with proper design tokens (
bg-surface,text-success,text-danger,text-muted-foreground), accessible decorative dividers (aria-hidden), and smooth framer-motion animations. The loading state correctly disables all interactive elements.web/src/pages/approvals/BatchActionBar.stories.tsx (1)
1-36: LGTM!Good story coverage with default, single item, many items, and loading states. The decorator providing a fixed-height wrapper ensures the fixed-position bar renders correctly in Storybook. Accessibility testing is properly enabled.
web/src/__tests__/pages/approvals/BatchActionBar.test.tsx (1)
1-46: LGTM!Solid test coverage using role-based queries and
userEventfor user interactions. The tests verify the core behaviors: display, all three button handlers, and the loading disabled state.web/src/stores/approvals.ts (2)
60-70: Store initialization and structure are well-designed.Good patterns: module-level
pendingTransitionswith test-only reset function, comprehensive state interface, and proper separation of concerns (data, loading, CRUD, real-time, optimistic, selection, batch).
229-264: Batch operations are well-implemented.Proper use of
Promise.allSettledfor concurrent API calls, per-item rollback on failure, andMAX_BATCH_SIZEenforcement. Selection is correctly cleared after completion.web/src/__tests__/stores/approvals.test.ts (2)
1-40: Test setup is well-structured.Good use of
resetStorehelper with_resetPendingTransitions()andsetState(), proper mock isolation withvi.clearAllMocks()/vi.restoreAllMocks(), and theimportApi()pattern for accessing mocked functions.
247-319: Solid WebSocket event handling tests.Good coverage of edge cases: valid payload, missing payload, missing required fields, array-typed payload, and pending transition skip. This thoroughly validates the defensive checks in
handleWsEvent.web/src/hooks/useApprovalsData.ts (1)
61-76: Potential mount-time double fetch should be re-verified.Line 62-64 triggers an initial fetch, and Line 74 starts polling. If
start()immediately invokespollFn, the hook issues duplicateGET /approvalson mount.Use this read-only check to confirm
usePolling.start()behavior:#!/bin/bash set -euo pipefail echo "== usePolling implementation ==" fd usePolling.ts web/src/hooks | sed -n '1,20p' sed -n '1,220p' web/src/hooks/usePolling.ts echo echo "== call sites in useApprovalsData ==" sed -n '55,95p' web/src/hooks/useApprovalsData.ts echo echo "== quick pattern scan for immediate fn invocation in start ==" rg -n "start\\s*[:=].*\\(|fn\\(\\)|setInterval|setTimeout" web/src/hooks/usePolling.ts -C 3Expected: If
start()callsfn()before scheduling interval, remove one of the two mount fetch triggers.web/src/utils/approvals.ts (1)
110-132: Solid utility decomposition and typing coverage.Risk grouping and client-side filtering are cleanly separated and type-safe; this is easy to reuse across page/components/tests.
Also applies to: 161-189
docs/design/page-structure.md (1)
56-57: Docs updates are aligned with the implemented Approvals UX and routing behavior.The added risk/type/search/selected params and revised keyboard-nav wording keep the design spec synchronized with feature reality.
Also applies to: 219-223, 351-351
web/src/pages/approvals/ApprovalTimeline.stories.tsx (1)
24-71: Story variants are well-structured and cover the key approval states.Typed
Meta/StoryObjusage plus explicit Pending/Approved/Rejected/Expired scenarios gives good visual/test surface.web/src/__tests__/pages/ApprovalsPage.test.tsx (2)
35-38: Computed property name pattern is unconventional but functional.This mock pattern using
[hookName]was previously flagged. The simpler direct property approach is preferred for readability.
53-123: Good test coverage for page-level conditional rendering.Tests cover the essential states: loading skeleton, error banner, WebSocket disconnection, empty state, risk-level grouping, and the important edge case of not showing skeleton when data already exists during a refresh.
web/src/pages/approvals/ApprovalTimeline.tsx (3)
63-105: Consider extracting the step rendering to aTimelineStepcomponent.The
.map()body spans ~40 lines with styling logic, conditional rendering, and accessibility attributes. Extracting to a dedicated component would improve testability and maintainability.
52-56: OUTCOME_CLASSES mapping is complete for rendered cases.The map covers
success,danger, andtext-secondarywhich correspond toapproved,rejected, andexpiredstatuses. Thependingstatus returns'accent'fromgetApprovalStatusColor, but sinceoutcomeLabelis only set whendecided=true(line 35), pending approvals never render the outcome badge, making the fallback safe.
58-62: Accessible timeline structure with proper ARIA attributes.Good use of
role="list"witharia-label,role="listitem"on children, andaria-hidden="true"on decorative elements (dots and lines).web/src/__tests__/pages/approvals/ApprovalCard.test.tsx (1)
36-106: Comprehensive ApprovalCard test coverage.Tests cover all essential paths: rendering, conditional UI for pending vs non-pending states, all four interaction handlers, and the selected checkbox state. The urgency countdown and "No expiry" edge cases are properly verified.
web/src/pages/approvals/ApprovalFilterBar.stories.tsx (1)
6-52: Well-structured stories with accessibility testing enabled.The
a11y.test = 'error'parameter is correctly set. TheInteractivewrapper component pattern nicely demonstrates the controlled filter state behavior. Stories cover default, active filters, and empty states appropriately for a filter bar component.web/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsx (1)
18-103: Thorough filter bar test coverage.Tests verify all filter controls, callback behavior with partial filter updates, active filter pill rendering/removal, and the clear-all functionality. The
expect.objectContainingpattern appropriately validates partial filter objects.web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx (3)
34-45: Clean framer-motion mock implementation.The mock components properly preserve essential props (
className,role,aria-modal,aria-label,ref) while bypassing animation timing, enabling reliable test assertions.
231-253: Robust focus trap verification with tab cycling.The test properly validates bidirectional tab wrapping: forward tab from last element returns to first, and shift+tab from first returns to last. This confirms the focus trap implementation works correctly.
80-270: Comprehensive drawer test coverage addressing all critical paths.Tests cover: rendering states (data, loading, error), close interactions (button, Escape), confirm dialogs (approve/reject), validation (reject requires reason), error handling (toast on failure), successful submissions with payloads, conditional field rendering, focus trapping, and button visibility for non-pending approvals.
web/src/pages/approvals/ApprovalCard.stories.tsx (1)
45-87: Excellent story coverage for ApprovalCard states.Stories comprehensively cover all risk levels (critical/high/medium/low), selection state, decision outcomes (approved/rejected/expired), and expiry variations (with countdown, no expiry). The decorator ensures consistent card sizing across stories.
| it('calls fetchApprovals on mount', async () => { | ||
| renderHook(() => useApprovalsData()) | ||
| await waitFor(() => { | ||
| expect(mockFetchApprovals).toHaveBeenCalledWith({ limit: 200 }) | ||
| }) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
This assertion hard-couples tests to the current limit: 200 bug.
Line 54 will need rework once fetch pagination/cap handling is corrected. Prefer asserting that mount triggers a fetch call, not a hard-coded limit payload.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/__tests__/hooks/useApprovalsData.test.ts` around lines 51 - 55, The
test in useApprovalsData.test.ts tightly asserts a specific pagination payload
({ limit: 200 }) on mount; update the assertion to avoid coupling to the current
bug by verifying that mockFetchApprovals is invoked on mount without asserting
the exact limit. Replace expect(mockFetchApprovals).toHaveBeenCalledWith({
limit: 200 }) with a call existence check such as
expect(mockFetchApprovals).toHaveBeenCalled() or a more flexible matcher like
expect(mockFetchApprovals).toHaveBeenCalledWith(expect.any(Object)) so the test
ensures useApprovalsData triggers a fetch without depending on the hard-coded
limit.
| export function useApprovalsData(): UseApprovalsDataReturn { | ||
| const approvals = useApprovalsStore((s) => s.approvals) | ||
| const selectedApproval = useApprovalsStore((s) => s.selectedApproval) | ||
| const total = useApprovalsStore((s) => s.total) | ||
| const loading = useApprovalsStore((s) => s.loading) | ||
| const loadingDetail = useApprovalsStore((s) => s.loadingDetail) | ||
| const error = useApprovalsStore((s) => s.error) | ||
| const detailError = useApprovalsStore((s) => s.detailError) | ||
| const fetchApproval = useApprovalsStore((s) => s.fetchApproval) | ||
| const approveOne = useApprovalsStore((s) => s.approveOne) | ||
| const rejectOne = useApprovalsStore((s) => s.rejectOne) | ||
| const optimisticApprove = useApprovalsStore((s) => s.optimisticApprove) | ||
| const optimisticReject = useApprovalsStore((s) => s.optimisticReject) | ||
| const selectedIds = useApprovalsStore((s) => s.selectedIds) | ||
| const toggleSelection = useApprovalsStore((s) => s.toggleSelection) | ||
| const selectAllInGroup = useApprovalsStore((s) => s.selectAllInGroup) | ||
| const deselectAllInGroup = useApprovalsStore((s) => s.deselectAllInGroup) | ||
| const clearSelection = useApprovalsStore((s) => s.clearSelection) | ||
| const batchApprove = useApprovalsStore((s) => s.batchApprove) | ||
| const batchReject = useApprovalsStore((s) => s.batchReject) | ||
|
|
||
| // Initial data fetch | ||
| useEffect(() => { | ||
| useApprovalsStore.getState().fetchApprovals({ limit: 200 }) | ||
| }, []) | ||
|
|
||
| // Lightweight polling for approval refresh | ||
| const pollFn = useCallback(async () => { | ||
| await useApprovalsStore.getState().fetchApprovals({ limit: 200 }) | ||
| }, []) | ||
|
|
||
| const polling = usePolling(pollFn, APPROVAL_POLL_INTERVAL) | ||
|
|
||
| useEffect(() => { | ||
| polling.start() | ||
| return () => polling.stop() | ||
| }, [polling]) | ||
|
|
||
| // WebSocket bindings for real-time updates | ||
| const bindings: ChannelBinding[] = useMemo( | ||
| () => | ||
| APPROVAL_CHANNELS.map((channel) => ({ | ||
| channel, | ||
| handler: (event) => { | ||
| useApprovalsStore.getState().handleWsEvent(event) | ||
| }, | ||
| })), | ||
| [], | ||
| ) | ||
|
|
||
| const { connected: wsConnected, setupError: wsSetupError } = useWebSocket({ | ||
| bindings, | ||
| }) | ||
|
|
||
| return { | ||
| approvals, | ||
| selectedApproval, | ||
| total, | ||
| loading, | ||
| loadingDetail, | ||
| error, | ||
| detailError, | ||
| wsConnected, | ||
| wsSetupError, | ||
| fetchApproval, | ||
| approveOne, | ||
| rejectOne, | ||
| optimisticApprove, | ||
| optimisticReject, | ||
| selectedIds, | ||
| toggleSelection, | ||
| selectAllInGroup, | ||
| deselectAllInGroup, | ||
| clearSelection, | ||
| batchApprove, | ||
| batchReject, | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Split useApprovalsData into smaller units.
This function exceeds the 50-line function limit and currently mixes store selectors, polling, and WebSocket bindings in one block.
As per coding guidelines: **/*.{py,ts,tsx,go}: Functions should be under 50 lines; files should be under 800 lines.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/hooks/useApprovalsData.ts` around lines 40 - 117, The
useApprovalsData function is over the 50-line limit and mixes store selectors,
polling, and WebSocket bindings; split it into focused helpers: extract the
repeated store selectors into a new hook (e.g., useApprovalsSelectors) that
returns approvals, selectedApproval, total, loading, loadingDetail, error,
detailError, fetchApproval, approveOne, rejectOne, optimisticApprove,
optimisticReject, selectedIds, toggleSelection, selectAllInGroup,
deselectAllInGroup, clearSelection, batchApprove, batchReject (pulling from
useApprovalsStore). Move polling logic into a new hook (e.g.,
useApprovalsPolling) that encapsulates pollFn and usePolling and calls
useApprovalsStore.getState().fetchApprovals({ limit: 200 }). Move WebSocket
setup into a new hook (e.g., useApprovalsWebSocket) that creates bindings using
APPROVAL_CHANNELS and handleWsEvent and returns { connected, setupError } from
useWebSocket. Finally, simplify useApprovalsData to compose those three hooks
and return the combined values.
| export function ApprovalFilterBar({ | ||
| filters, | ||
| onFiltersChange, | ||
| pendingCount, | ||
| totalCount, | ||
| actionTypes, | ||
| }: ApprovalFilterBarProps) { | ||
| const hasActiveFilters = !!(filters.status || filters.riskLevel || filters.actionType || filters.search) | ||
|
|
||
| function updateFilter<K extends keyof ApprovalPageFilters>(key: K, value: ApprovalPageFilters[K]) { | ||
| onFiltersChange({ ...filters, [key]: value || undefined }) | ||
| } | ||
|
|
||
| function clearFilters() { | ||
| onFiltersChange({}) | ||
| } | ||
|
|
||
| return ( | ||
| <div className="space-y-2"> | ||
| <div className="flex flex-wrap items-center gap-2"> | ||
| {/* Status filter */} | ||
| <select | ||
| value={filters.status ?? ''} | ||
| onChange={(e) => updateFilter('status', (e.target.value || undefined) as ApprovalStatus | undefined)} | ||
| className="h-8 rounded-md border border-border bg-surface px-2 text-xs text-foreground focus:outline-none focus:ring-2 focus:ring-accent" | ||
| aria-label="Filter by status" | ||
| > | ||
| <option value="">All statuses</option> | ||
| {STATUSES.map((s) => ( | ||
| <option key={s} value={s}>{getApprovalStatusLabel(s)}</option> | ||
| ))} | ||
| </select> | ||
|
|
||
| {/* Risk level filter */} | ||
| <select | ||
| value={filters.riskLevel ?? ''} | ||
| onChange={(e) => updateFilter('riskLevel', (e.target.value || undefined) as ApprovalRiskLevel | undefined)} | ||
| className="h-8 rounded-md border border-border bg-surface px-2 text-xs text-foreground focus:outline-none focus:ring-2 focus:ring-accent" | ||
| aria-label="Filter by risk level" | ||
| > | ||
| <option value="">All risk levels</option> | ||
| {RISK_LEVELS.map((r) => ( | ||
| <option key={r} value={r}>{getRiskLevelLabel(r)}</option> | ||
| ))} | ||
| </select> | ||
|
|
||
| {/* Action type filter */} | ||
| <select | ||
| value={filters.actionType ?? ''} | ||
| onChange={(e) => updateFilter('actionType', e.target.value || undefined)} | ||
| className="h-8 rounded-md border border-border bg-surface px-2 text-xs text-foreground focus:outline-none focus:ring-2 focus:ring-accent" | ||
| aria-label="Filter by action type" | ||
| > | ||
| <option value="">All action types</option> | ||
| {actionTypes.map((t) => ( | ||
| <option key={t} value={t}>{t}</option> | ||
| ))} | ||
| </select> | ||
|
|
||
| {/* Search */} | ||
| <input | ||
| type="text" | ||
| value={filters.search ?? ''} | ||
| onChange={(e) => updateFilter('search', e.target.value || undefined)} | ||
| placeholder="Search approvals..." | ||
| className="h-8 w-48 rounded-md border border-border bg-surface px-2 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent" | ||
| aria-label="Search approvals" | ||
| /> | ||
|
|
||
| {/* Counts */} | ||
| <span className="text-xs text-muted-foreground"> | ||
| {pendingCount} pending / {totalCount} total | ||
| </span> | ||
| </div> | ||
|
|
||
| {/* Active filter pills */} | ||
| {hasActiveFilters && ( | ||
| <div className="flex flex-wrap items-center gap-1.5"> | ||
| {filters.status && ( | ||
| <FilterPill label={`Status: ${getApprovalStatusLabel(filters.status)}`} onRemove={() => updateFilter('status', undefined)} /> | ||
| )} | ||
| {filters.riskLevel && ( | ||
| <FilterPill label={`Risk: ${getRiskLevelLabel(filters.riskLevel)}`} onRemove={() => updateFilter('riskLevel', undefined)} /> | ||
| )} | ||
| {filters.actionType && ( | ||
| <FilterPill label={`Type: ${filters.actionType}`} onRemove={() => updateFilter('actionType', undefined)} /> | ||
| )} | ||
| {filters.search && ( | ||
| <FilterPill label={`Search: "${filters.search}"`} onRemove={() => updateFilter('search', undefined)} /> | ||
| )} | ||
| <button | ||
| type="button" | ||
| onClick={clearFilters} | ||
| className="text-xs text-muted-foreground hover:text-foreground transition-colors" | ||
| > | ||
| Clear all | ||
| </button> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Break ApprovalFilterBar into smaller functions/components.
ApprovalFilterBar exceeds the 50-line function limit; extract sections (filters row, active pills) into focused subcomponents/helpers.
As per coding guidelines: **/*.{py,ts,tsx,go}: Functions should be under 50 lines; files should be under 800 lines.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/pages/approvals/ApprovalFilterBar.tsx` around lines 16 - 117,
ApprovalFilterBar is too large; extract the filter row and the active-pills area
into two small subcomponents (e.g., ApprovalFilterRow and ApprovalActivePills)
to keep each function under 50 lines. Move the JSX for the select/input/counts
into ApprovalFilterRow and pass filters, actionTypes, and onChange handlers
(reuse updateFilter and clearFilters from ApprovalFilterBar or lift their logic
into small helpers like updateFilterInProps and clearAllFilters) as props; move
the active filter pills JSX into ApprovalActivePills and pass filters and
onRemove handlers (calling updateFilter with undefined). Update
ApprovalFilterBar to only compute hasActiveFilters, define the minimal
updateFilter/clearFilters helpers, and render ApprovalFilterRow and
ApprovalActivePills with the appropriate props.
| <div className="space-y-2"> | ||
| <div className="flex flex-wrap items-center gap-2"> | ||
| {/* Status filter */} | ||
| <select | ||
| value={filters.status ?? ''} | ||
| onChange={(e) => updateFilter('status', (e.target.value || undefined) as ApprovalStatus | undefined)} | ||
| className="h-8 rounded-md border border-border bg-surface px-2 text-xs text-foreground focus:outline-none focus:ring-2 focus:ring-accent" | ||
| aria-label="Filter by status" |
There was a problem hiding this comment.
Replace hardcoded spacing/font-size classes with semantic design tokens.
This file uses fixed utility values (h-8, w-48, gap-2, px-2, text-[10px], etc.), which violates the token-only styling rule for web/src.
As per coding guidelines: web/src/**/*.{ts,tsx}: Never hardcode colors, fonts, or spacing - always use Tailwind semantic classes (text-foreground, bg-card, p-card, gap-section-gap) or CSS variables (var(--so-accent)).
Also applies to: 53-54, 66-67, 81-82, 93-94, 121-127
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/pages/approvals/ApprovalFilterBar.tsx` around lines 34 - 41, The
ApprovalFilterBar component is using hardcoded utility classes (e.g., h-8, w-48,
gap-2, px-2, text-[10px]) which must be replaced with semantic design tokens;
update the JSX in ApprovalFilterBar (the select, inputs, buttons and surrounding
divs referenced by filters and updateFilter) to use the project's semantic
Tailwind classes (e.g., gap-section-gap, p-card, text-foreground /
text-xs-token, w-control / h-control or the equivalent token names used across
the codebase) or CSS variables (var(--so-...)) instead of the numeric utilities,
and apply the same replacements at the other noted blocks (around lines
referenced in the review: the other select/input/button groups) so spacing,
padding, and font-size use tokens consistently.
…bbit - WS payload validation: check all required fields before cast - Escape key bug: use local state instead of querySelector that matched drawer - Polling merge: preserve optimistic transitions during fetchApprovals - Stale drawer: refetch when selectedId changes to different ID - SelectedIds cleanup: remove ID on optimistic approve/reject - Error handling: capture and surface error details in all catch blocks - Batch operations: return failedReasons, add 50-item size cap - Separate detailError: drawer errors don't clobber page-level error - Drawer loading: show spinner/error when approval is null - Focus restoration: return focus to trigger element on drawer close - A11y: urgency badge accessible to screen readers, label elements fixed - Design tokens: shadow-lg, min-h-[60px], text-text-muted replaced - Type safety: precise Record keys, satisfies constraints, URL validation - Component reuse: shared DOT_COLOR_CLASSES/URGENCY_BADGE_CLASSES in utils - Stories: a11y.test:'error' on all 6 story files - Tests: 4 new test files (Drawer, FilterBar, Timeline, Skeleton), 31 new tests - Test quality: shared factory, precise assertions, argument checks - Docs: updated page-structure.md and brand-and-ux.md to match implementation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rtion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Metadata values coerced via String() to prevent crash on non-string values - Focus trap test now verifies Tab wraps last->first and Shift+Tab first->last - Skipped: replacing custom drawer with shared Drawer primitive (shared Drawer lacks support for custom header, conditional footer, nested ConfirmDialogs, and null-approval loading/error states that this component needs) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…etadata - Test successful approve with comment and reject with reason - Test that Escape closes Radix dialog but not the drawer itself - Metadata values: use JSON.stringify for objects instead of String() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…for drawer - Test decided approval renders Decided By/At, expiry, reason, task link, metadata - Test nested metadata object renders as JSON.stringify output - Test approve error path shows 'Failed to approve' toast - Test reject error path shows 'Failed to reject' toast Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…t coverage - Store: prune selectedIds in upsertApproval when status leaves pending - Store: request counter in fetchApproval to discard stale responses - Store: close confirm dialogs when approval leaves pending (WS race) - Hook: extract APPROVAL_FETCH_LIMIT constant (was hardcoded 200) - Page: include failedReasons in batch warning toasts - Page: remove duplicate fetchApproval from handleSelectApproval - Page: extract ApprovalRiskGroupSection from .map() body - Card: import shared DOT_COLOR_CLASSES/URGENCY_BADGE_CLASSES from utils - FilterBar: named FilterPillProps interface - Tests: MAX_BATCH_SIZE enforcement, pendingTransitions merge preservation - Tests: locale-independent timeline timestamps, icon identity assertions - Story: Error state for ApprovalDetailDrawer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tch guard - Store: prune selectedIds against fetched data in fetchApprovals - Drawer: reset dialogs/inputs when approval?.id changes - Page: close batch dialogs when selectedIds empties (WS edge case) - Page: simplify drawer fetch effect to always refetch on selectedId change - Test: upsertApproval prunes selectedIds when status leaves pending Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Page: only show WS disconnect banner after a prior successful connection (or when wsSetupError is present), preventing flash on initial load - Drawer: split focus effects -- opener save/restore on open, focus-trap re-evaluates when loading/approval changes; panel gets tabindex=-1 fallback when no focusable descendants exist (loading spinner state) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New test: WS banner not shown on initial load when wsConnected=false - Fix: metric cards test renamed, assertions target risk group headings to avoid collision with filter bar option elements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Metric cards test now verifies rendered count values (2 and 1) - Risk group test uses within() to verify cards are inside correct sections Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Metric cards: query text-metric elements and assert ordered values [2,1,0,0]
- Risk groups: use getByRole('heading') + closest container for within() scope
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ty tests - Store: optimistic rollback patches single item instead of whole array - Store: batch clearSelection only on full success (failed IDs stay selected) - Store: fetchApprovals prunes selectedIds against pending IDs only - BatchActionBar: shadow-lg replaced with design token - Tests: fast-check property tests for ApprovalCard and ApprovalTimeline - Fix: install @types/js-yaml for type-check (pre-existing from main) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… + selectedApproval sync - Test: assert reject button also hidden for non-pending cards - BatchActionBar: add role="toolbar" aria-label="Batch actions" - Store: sequence guard on fetchApprovals to discard stale poll responses - Store: sync selectedApproval with fresh data from fetchApprovals Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… edge case - Store: optimisticApprove/Reject now update selectedApproval when drawer is open for the same ID; rollback restores it too - Test: budget computeCostBreakdown property test handles zero total cost (percentages are 0, not 100, when all costs are 0) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
a675179 to
00740ea
Compare
There was a problem hiding this comment.
Actionable comments posted: 11
♻️ Duplicate comments (7)
web/src/pages/ApprovalsPage.tsx (1)
307-308:⚠️ Potential issue | 🟠 MajorDrawer approve bypasses the optimistic flow used by card actions.
Lines 121-130 centralize single-item approval with
optimisticApprove()+ rollback + toast, but the drawer'sonApprovedirectly callsapproveOne(id, data). This creates a divergent mutation path that misses the same selection cleanup, toast feedback, and failure handling.🔧 Suggested fix
Create a drawer-specific wrapper that uses the same optimistic flow:
- onApprove={async (id, data) => { await approveOne(id, data) }} - onReject={async (id, data) => { await rejectOne(id, data) }} + onApprove={async (id, data) => { + const rollback = optimisticApprove(id) + try { + await approveOne(id, data) + useToastStore.getState().add({ variant: 'success', title: 'Approval granted' }) + handleCloseDrawer() + } catch (err) { + rollback() + useToastStore.getState().add({ variant: 'error', title: 'Failed to approve', description: getErrorMessage(err) }) + } + }} + onReject={async (id, data) => { + // Similar pattern for reject + await rejectOne(id, data) + handleCloseDrawer() + }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/ApprovalsPage.tsx` around lines 307 - 308, The drawer's onApprove/onReject currently call approveOne/rejectOne directly and bypass the centralized optimistic flow; replace these handlers to call the same wrapper used by the card actions (e.g., optimisticApprove / optimisticReject or the single-item wrapper that performs optimistic mutation, rollback on failure, shows the toast, and clears selection) instead of approveOne/rejectOne directly so the drawer benefits from the same selection cleanup, toast feedback, and failure handling; locate the drawer's onApprove/onReject props and wire them to that wrapper (or extract a small drawerApprove/drawerReject wrapper that delegates to optimisticApprove/optimisticReject with the same id and data).web/src/pages/approvals/ApprovalRiskGroupSection.tsx (1)
42-57:⚠️ Potential issue | 🟡 MinorExpose indeterminate state on the group checkbox.
When
selectedIdscontains some but not allpendingIds, the checkbox shows as fully unchecked. Set theindeterminateproperty andaria-checked="mixed"for partial selection.🔧 Suggested fix
+import { useEffect, useRef } from 'react' export function ApprovalRiskGroupSection({ ... }: ApprovalRiskGroupSectionProps) { // ... const allSelected = pendingIds.length > 0 && pendingIds.every((id) => selectedIds.has(id)) + const someSelected = pendingIds.some((id) => selectedIds.has(id)) + const isIndeterminate = someSelected && !allSelected + const checkboxRef = useRef<HTMLInputElement>(null) + + useEffect(() => { + if (checkboxRef.current) { + checkboxRef.current.indeterminate = isIndeterminate + } + }, [isIndeterminate]) // In the checkbox: <input + ref={checkboxRef} type="checkbox" checked={allSelected} + aria-checked={isIndeterminate ? 'mixed' : allSelected} onChange={() => { ... }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/approvals/ApprovalRiskGroupSection.tsx` around lines 42 - 57, The group checkbox in ApprovalRiskGroupSection doesn't show a partial state; compute a partialSelected boolean (e.g., selectedIds.length > 0 && selectedIds.length < pendingIds.length) and apply it: attach a ref to the input element, set inputRef.current.indeterminate = partialSelected inside an effect or after render, and set the input's aria-checked to "mixed" when partialSelected (otherwise "true"/"false" based on allSelected); keep the existing checked/onChange logic with allSelected, onSelectAll, and onDeselectAll.web/src/pages/approvals/ApprovalCard.tsx (2)
44-48:⚠️ Potential issue | 🟠 MajorMove countdown sync out of render phase.
Lines 45-48 mutate
prevSecondsRefand callsetCountdown()during render, violating React's render phase purity rules. In React 19 with concurrent features, aborted renders can leave state inconsistent.🔧 Suggested fix
const [countdown, setCountdown] = useState(approval.seconds_remaining) - const prevSecondsRef = useRef(approval.seconds_remaining) - if (approval.seconds_remaining !== prevSecondsRef.current) { - prevSecondsRef.current = approval.seconds_remaining - setCountdown(approval.seconds_remaining) - } + useEffect(() => { + setCountdown(approval.seconds_remaining) + }, [approval.seconds_remaining])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/approvals/ApprovalCard.tsx` around lines 44 - 48, The render currently mutates prevSecondsRef and calls setCountdown when approval.seconds_remaining changes (prevSecondsRef, approval.seconds_remaining, setCountdown) which violates render purity; move that synchronization into a useEffect that depends on approval.seconds_remaining (and any identity-stable approval or id) so the ref update and setCountdown run after render, e.g., in useEffect update prevSecondsRef.current and call setCountdown(approval.seconds_remaining) only when the value changes.
30-31:⚠️ Potential issue | 🟠 MajorPending controls remain visible after local countdown expires.
isPendingonly checksapproval.status === 'pending', so the checkbox and approve/reject buttons stay interactive even when the localcountdownreaches 0. Consider deriving alocallyExpiredflag and hiding/disabling controls.🔧 Suggested approach
const isPending = approval.status === 'pending' +const locallyExpired = countdown !== null && countdown <= 0 +const canAct = isPending && !locallyExpired // Use canAct instead of isPending for controls: -{isPending && ( +{canAct && ( <input type="checkbox" ... /> )} -{isPending && ( +{canAct && ( <div className="mt-3 flex items-center gap-2"> <Button ... onClick={() => onApprove(approval.id)}>Approve</Button> <Button ... onClick={() => onReject(approval.id)}>Reject</Button> </div> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/approvals/ApprovalCard.tsx` around lines 30 - 31, The controls remain interactive because isPending only checks approval.status; update ApprovalCard to derive a locallyExpired flag (e.g., locallyExpired = countdown <= 0) and then compute an effective pending state (e.g., effectivePending = isPending && !locallyExpired); use effectivePending to conditionally hide or disable the checkbox and approve/reject buttons (and set appropriate aria-disabled attributes) so the UI becomes non-interactive once the local countdown expires while still respecting approval.status.web/src/__tests__/pages/ApprovalsPage.test.tsx (1)
35-38: 🧹 Nitpick | 🔵 TrivialSimplify the mock pattern.
The computed property name
[hookName]works but is unconventional. The simpler direct pattern is clearer:vi.mock('@/hooks/useApprovalsData', () => { - const hookName = 'useApprovalsData' - return { [hookName]: () => getApprovalsData() } + return { useApprovalsData: () => getApprovalsData() } })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/__tests__/pages/ApprovalsPage.test.tsx` around lines 35 - 38, The vi.mock callback uses an unnecessary computed property name; inside the vi.mock for useApprovalsData replace the object return { [hookName]: () => getApprovalsData() } with a direct property key so it returns { useApprovalsData: () => getApprovalsData() } to make the mock clearer and more conventional (locate the vi.mock block that references hookName, useApprovalsData, and getApprovalsData).web/src/pages/approvals/ApprovalDetailDrawer.tsx (1)
220-221:⚠️ Potential issue | 🟡 MinorStale approval content may render alongside error state.
Line 220 renders approval content when
!showLoadingState && approval, but doesn't checkdetailError. If a refetch fails while a previous approval is cached, both the error view (lines 212-218) and the stale approval content will render simultaneously.🔧 Suggested fix
- {!showLoadingState && approval && ( + {!detailError && !showLoadingState && approval && (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx` around lines 220 - 221, The approval detail panel can show stale approval content alongside an error because the render condition only checks !showLoadingState && approval; update the render guard in ApprovalDetailDrawer to also ensure no detailError is present (e.g., change the condition to !showLoadingState && approval && !detailError or return the error view early), so that when detailError is truthy the approval content block (the fragment rendered under {!showLoadingState && approval}) is not rendered.web/src/__tests__/stores/approvals.test.ts (1)
43-99: 🧹 Nitpick | 🔵 TrivialConsider adding out-of-order response race condition test.
The suite tests optimistic state preservation but doesn't explicitly test that older
fetchApprovalsresponses resolving after newer ones are discarded by the sequence guard. This would protect against regressions in the stale-response prevention logic.💡 Suggested regression test
it('discards stale fetchApprovals response when newer request completes first', async () => { const api = await importApi() let resolveFirst!: (value: { data: ApprovalResponse[]; total: number; offset: number; limit: number }) => void let resolveSecond!: (value: { data: ApprovalResponse[]; total: number; offset: number; limit: number }) => void vi.mocked(api.listApprovals) .mockImplementationOnce(() => new Promise((resolve) => { resolveFirst = resolve })) .mockImplementationOnce(() => new Promise((resolve) => { resolveSecond = resolve })) const first = useApprovalsStore.getState().fetchApprovals() const second = useApprovalsStore.getState().fetchApprovals() // Resolve second (newer) request first resolveSecond({ data: [makeApproval('new')], total: 1, offset: 0, limit: 200 }) await second // Then resolve first (older) request resolveFirst({ data: [makeApproval('old')], total: 1, offset: 0, limit: 200 }) await first // Store should contain the newer result expect(useApprovalsStore.getState().approvals[0]!.id).toBe('new') })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/__tests__/stores/approvals.test.ts` around lines 43 - 99, Add a regression test for out-of-order responses to ensure fetchApprovals' sequence-guarding discards stale results: in the approvals.test.ts suite, create two pending promises for vi.mocked(api.listApprovals) (using mockImplementationOnce returning new Promise and capturing resolve functions), call useApprovalsStore.getState().fetchApprovals() twice to produce a "first" and "second" request, resolve the second promise first with data for "new", await the second, then resolve the first with data for "old" and await the first, and finally assert that useApprovalsStore.getState().approvals contains the "new" item (reference fetchApprovals, useApprovalsStore, and api.listApprovals to locate code).
🤖 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/src/__tests__/helpers/factories.ts`:
- Line 128: Replace the non-deterministic Date.now() usage in the test factory
default for created_at with a fixed baseline timestamp constant (e.g.,
BASE_TIMESTAMP) defined in the same module; update the created_at default to
BASE_TIMESTAMP.toISOString() (and export BASE_TIMESTAMP) so tests that need
relative times can explicitly compute offsets against this stable baseline
instead of relying on Date.now(). Ensure you update the factory's created_at
reference and add the new BASE_TIMESTAMP symbol in
web/src/__tests__/helpers/factories.ts.
In `@web/src/__tests__/hooks/useApprovalsData.test.ts`:
- Around line 30-42: The resetStore helper currently sets only a subset of
useApprovalsStore properties; add initialization for pendingTransitions: new
Set<string>() and any optimistic update methods (e.g., the store's optimistic
add/remove/commit/rollback handlers) so the test store shape matches the real
store; update the resetStore function to set pendingTransitions and stub or
assign the optimistic methods used by the real store (referencing resetStore,
useApprovalsStore, and pendingTransitions) to prevent future tests from breaking
if the hook depends on them.
In `@web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx`:
- Around line 102-113: Add a test to ApprovalDetailDrawer.test.tsx that verifies
when an error prop is provided the drawer shows only the error and not any stale
approval content: render ApprovalDetailDrawer with an approval object (e.g.,
created via makeApproval), open={true}, and error="Refetch failed", then assert
screen.getByText('Refetch failed') is present and screen.queryByText('<stale
title>') is null/ not in the document; use the ApprovalDetailDrawer component
and makeApproval helper from the test suite and queryByText to confirm stale
content is not rendered.
In `@web/src/__tests__/pages/approvals/BatchActionBar.test.tsx`:
- Around line 17-45: Add a regression test to assert the toolbar accessibility
semantics for the BatchActionBar component: after rendering <BatchActionBar
{...defaultProps} /> (same pattern as other tests), call
screen.getByRole('toolbar', { name: /Batch actions/i }) and assert it is in the
document to ensure role="toolbar" and aria-label="Batch actions" remain present;
add this near the other render/assertion tests in BatchActionBar.test.tsx.
In `@web/src/__tests__/pages/ApprovalsPage.test.tsx`:
- Around line 117-124: The test uses a brittle DOM traversal via
closest('[class*="rounded"]') to scope items; update the component
ApprovalRiskGroupSection to include a stable identifier (e.g., data-testid or
role/aria-label) and change the test in ApprovalsPage.test.tsx to query that
stable attribute (e.g., getByTestId('approval-riskgroup-critical') or
getByRole/getByLabelText) and then use within(...) on that element to assert
presence of 'Deploy prod' and 'Push to main' instead of relying on class name
matching.
In `@web/src/__tests__/stores/approvals.test.ts`:
- Around line 413-427: Add an assertion to the test so it verifies that failed
IDs remain selected after a partial failure: after calling
useApprovalsStore.getState().batchApprove(['1','2']) and asserting result and
rollback, also check useApprovalsStore.getState().selectedIds contains '2' (and
does not contain '1' if the store clears successful items) to ensure
batchApprove preserves selection for failed approvals; locate this test around
the it('rolls back failed items and returns mixed counts') block and update
expectations accordingly.
In `@web/src/pages/approvals/ApprovalTimeline.tsx`:
- Line 75: The span rendering step labels in ApprovalTimeline.tsx currently uses
the incorrect CSS token "text-text-secondary"; update the className on the span
that displays step.label to use the correct design token "text-secondary" so it
matches the project design tokens and styling conventions (locate the span with
step.label inside the ApprovalTimeline component and replace the token).
- Around line 52-56: The OUTCOME_CLASSES mapping contains an inconsistent value:
the entry with key 'text-secondary' currently maps to 'text-text-secondary'
(double prefix); update the mapping in OUTCOME_CLASSES so the value is the
correct CSS token 'text-secondary' (i.e., change 'text-text-secondary' to
'text-secondary') to match the codebase naming pattern and other entries.
In `@web/src/pages/approvals/BatchActionBar.tsx`:
- Around line 36-38: The selection count span currently updates visually but
isn’t announced to screen readers; update the element in BatchActionBar (the
span rendering {selectedCount} selected) to be a polite live region so changes
are announced by assistive tech — e.g., add aria-live="polite" (or
role="status") to that span or add a nearby visually-hidden element with
aria-live="polite" that outputs selectedCount, ensuring the dynamic count is
read without altering visual layout.
In `@web/src/pages/ApprovalsPage.tsx`:
- Around line 138-146: The four separate state updates (setBatchApproveOpen,
setBatchRejectOpen, setBatchComment, setBatchReason) triggered in the useEffect
watching selectedIds.size should be consolidated to avoid multiple renders:
replace those individual states with a single batchDialog state (e.g., const
[batchDialog, setBatchDialog] = useState<{ type: 'approve'|'reject'|null;
comment: string; reason: string }>(...)) and change the effect to call
setBatchDialog({ type: null, comment: '', reason: '' }) when selectedIds.size
=== 0; alternatively, if you must keep the separate setters, wrap the four calls
in ReactDOM.flushSync to batch them — update all references to the old state
variables (batch open flags, batch comment, batch reason) to use the new
batchDialog and its type/comment/reason fields.
- Around line 55-61: The effect that sets wasConnected via setWasConnected (and
the batch dialog reset handlers that call setBatchLoading/setWasConnected)
creates derived state and causes lint warnings about synchronous setState in
effects; replace wasConnected state with a ref (useRef) and update that ref
inside the useEffect listening to wsConnected (update ref.current instead of
calling setWasConnected), and for batch dialog resets consolidate or use refs
for batchLoading as well (or reset both inside a single callback) so you avoid
conditional setState-in-effect calls; locate and change uses of
wasConnected/setWasConnected and batchLoading/setBatchLoading in the component
(including the useEffect that references wsConnected and the batch dialog reset
code) to read/write the new refs or a single consolidated state updater.
---
Duplicate comments:
In `@web/src/__tests__/pages/ApprovalsPage.test.tsx`:
- Around line 35-38: The vi.mock callback uses an unnecessary computed property
name; inside the vi.mock for useApprovalsData replace the object return {
[hookName]: () => getApprovalsData() } with a direct property key so it returns
{ useApprovalsData: () => getApprovalsData() } to make the mock clearer and more
conventional (locate the vi.mock block that references hookName,
useApprovalsData, and getApprovalsData).
In `@web/src/__tests__/stores/approvals.test.ts`:
- Around line 43-99: Add a regression test for out-of-order responses to ensure
fetchApprovals' sequence-guarding discards stale results: in the
approvals.test.ts suite, create two pending promises for
vi.mocked(api.listApprovals) (using mockImplementationOnce returning new Promise
and capturing resolve functions), call
useApprovalsStore.getState().fetchApprovals() twice to produce a "first" and
"second" request, resolve the second promise first with data for "new", await
the second, then resolve the first with data for "old" and await the first, and
finally assert that useApprovalsStore.getState().approvals contains the "new"
item (reference fetchApprovals, useApprovalsStore, and api.listApprovals to
locate code).
In `@web/src/pages/approvals/ApprovalCard.tsx`:
- Around line 44-48: The render currently mutates prevSecondsRef and calls
setCountdown when approval.seconds_remaining changes (prevSecondsRef,
approval.seconds_remaining, setCountdown) which violates render purity; move
that synchronization into a useEffect that depends on approval.seconds_remaining
(and any identity-stable approval or id) so the ref update and setCountdown run
after render, e.g., in useEffect update prevSecondsRef.current and call
setCountdown(approval.seconds_remaining) only when the value changes.
- Around line 30-31: The controls remain interactive because isPending only
checks approval.status; update ApprovalCard to derive a locallyExpired flag
(e.g., locallyExpired = countdown <= 0) and then compute an effective pending
state (e.g., effectivePending = isPending && !locallyExpired); use
effectivePending to conditionally hide or disable the checkbox and
approve/reject buttons (and set appropriate aria-disabled attributes) so the UI
becomes non-interactive once the local countdown expires while still respecting
approval.status.
In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx`:
- Around line 220-221: The approval detail panel can show stale approval content
alongside an error because the render condition only checks !showLoadingState &&
approval; update the render guard in ApprovalDetailDrawer to also ensure no
detailError is present (e.g., change the condition to !showLoadingState &&
approval && !detailError or return the error view early), so that when
detailError is truthy the approval content block (the fragment rendered under
{!showLoadingState && approval}) is not rendered.
In `@web/src/pages/approvals/ApprovalRiskGroupSection.tsx`:
- Around line 42-57: The group checkbox in ApprovalRiskGroupSection doesn't show
a partial state; compute a partialSelected boolean (e.g., selectedIds.length > 0
&& selectedIds.length < pendingIds.length) and apply it: attach a ref to the
input element, set inputRef.current.indeterminate = partialSelected inside an
effect or after render, and set the input's aria-checked to "mixed" when
partialSelected (otherwise "true"/"false" based on allSelected); keep the
existing checked/onChange logic with allSelected, onSelectAll, and
onDeselectAll.
In `@web/src/pages/ApprovalsPage.tsx`:
- Around line 307-308: The drawer's onApprove/onReject currently call
approveOne/rejectOne directly and bypass the centralized optimistic flow;
replace these handlers to call the same wrapper used by the card actions (e.g.,
optimisticApprove / optimisticReject or the single-item wrapper that performs
optimistic mutation, rollback on failure, shows the toast, and clears selection)
instead of approveOne/rejectOne directly so the drawer benefits from the same
selection cleanup, toast feedback, and failure handling; locate the drawer's
onApprove/onReject props and wire them to that wrapper (or extract a small
drawerApprove/drawerReject wrapper that delegates to
optimisticApprove/optimisticReject with the same id and data).
🪄 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: 50aaa513-c2b7-4a13-9226-5fba3148495c
📒 Files selected for processing (31)
docs/design/brand-and-ux.mddocs/design/page-structure.mdweb/src/__tests__/helpers/factories.tsweb/src/__tests__/hooks/useApprovalsData.test.tsweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalCard.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/pages/approvals/ApprovalFilterBar.test.tsxweb/src/__tests__/pages/approvals/ApprovalTimeline.test.tsxweb/src/__tests__/pages/approvals/ApprovalsSkeleton.test.tsxweb/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/__tests__/stores/approvals.test.tsweb/src/__tests__/utils/approvals.test.tsweb/src/__tests__/utils/budget.property.test.tsweb/src/hooks/useApprovalsData.tsweb/src/pages/ApprovalsPage.tsxweb/src/pages/approvals/ApprovalCard.stories.tsxweb/src/pages/approvals/ApprovalCard.tsxweb/src/pages/approvals/ApprovalDetailDrawer.stories.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/pages/approvals/ApprovalFilterBar.stories.tsxweb/src/pages/approvals/ApprovalFilterBar.tsxweb/src/pages/approvals/ApprovalRiskGroupSection.tsxweb/src/pages/approvals/ApprovalTimeline.stories.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/pages/approvals/ApprovalsSkeleton.stories.tsxweb/src/pages/approvals/ApprovalsSkeleton.tsxweb/src/pages/approvals/BatchActionBar.stories.tsxweb/src/pages/approvals/BatchActionBar.tsxweb/src/stores/approvals.tsweb/src/utils/approvals.ts
| metadata: {}, | ||
| decided_by: null, | ||
| decision_reason: null, | ||
| created_at: new Date(Date.now() - 3600_000).toISOString(), // 1 hour ago |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Make fixture time deterministic by default.
Using Date.now() in a shared test factory makes defaults vary between runs and can cause
time-sensitive tests to become brittle. Prefer a fixed baseline timestamp and override it
explicitly in tests that need relative-time behavior.
♻️ Suggested patch
- created_at: new Date(Date.now() - 3600_000).toISOString(), // 1 hour ago
+ created_at: '2026-03-27T18:00:00Z',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/__tests__/helpers/factories.ts` at line 128, Replace the
non-deterministic Date.now() usage in the test factory default for created_at
with a fixed baseline timestamp constant (e.g., BASE_TIMESTAMP) defined in the
same module; update the created_at default to BASE_TIMESTAMP.toISOString() (and
export BASE_TIMESTAMP) so tests that need relative times can explicitly compute
offsets against this stable baseline instead of relying on Date.now(). Ensure
you update the factory's created_at reference and add the new BASE_TIMESTAMP
symbol in web/src/__tests__/helpers/factories.ts.
| function resetStore() { | ||
| useApprovalsStore.setState({ | ||
| approvals: [], | ||
| selectedApproval: null, | ||
| total: 0, | ||
| loading: false, | ||
| loadingDetail: false, | ||
| error: null, | ||
| detailError: null, | ||
| selectedIds: new Set(), | ||
| fetchApprovals: mockFetchApprovals, | ||
| handleWsEvent: mockHandleWsEvent, | ||
| }) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
resetStore omits pendingTransitions and optimistic methods.
The helper only sets 9 of the store's 13+ properties. While this may be fine for testing useApprovalsData (which doesn't directly expose these), tests could break if the hook implementation changes to depend on pendingTransitions or optimistic update methods.
Consider initializing pendingTransitions: new Set<string>() to match the actual store's initial state.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/__tests__/hooks/useApprovalsData.test.ts` around lines 30 - 42, The
resetStore helper currently sets only a subset of useApprovalsStore properties;
add initialization for pendingTransitions: new Set<string>() and any optimistic
update methods (e.g., the store's optimistic add/remove/commit/rollback
handlers) so the test store shape matches the real store; update the resetStore
function to set pendingTransitions and stub or assign the optimistic methods
used by the real store (referencing resetStore, useApprovalsStore, and
pendingTransitions) to prevent future tests from breaking if the hook depends on
them.
| // Scope card assertions to each risk group via its heading | ||
| const criticalHeading = screen.getByRole('heading', { name: 'Critical Approvals' }) | ||
| const criticalSection = criticalHeading.closest('[class*="rounded"]')! | ||
| expect(within(criticalSection as HTMLElement).getByText('Deploy prod')).toBeInTheDocument() | ||
| const highHeading = screen.getByRole('heading', { name: 'High Approvals' }) | ||
| const highSection = highHeading.closest('[class*="rounded"]')! | ||
| expect(within(highSection as HTMLElement).getByText('Push to main')).toBeInTheDocument() | ||
| }) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Fragile section selector relies on class name substring.
.closest('[class*="rounded"]')! is brittle—it will break if the section's styling changes. Consider adding a data-testid to ApprovalRiskGroupSection or querying by role/landmark instead.
♻️ More robust pattern
// In ApprovalRiskGroupSection.tsx, add data-testid to the wrapper
+<SectionCard data-testid={`risk-section-${riskLevel}`} ...>
// In test
-const criticalSection = criticalHeading.closest('[class*="rounded"]')!
+const criticalSection = screen.getByTestId('risk-section-critical')🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/__tests__/pages/ApprovalsPage.test.tsx` around lines 117 - 124, The
test uses a brittle DOM traversal via closest('[class*="rounded"]') to scope
items; update the component ApprovalRiskGroupSection to include a stable
identifier (e.g., data-testid or role/aria-label) and change the test in
ApprovalsPage.test.tsx to query that stable attribute (e.g.,
getByTestId('approval-riskgroup-critical') or getByRole/getByLabelText) and then
use within(...) on that element to assert presence of 'Deploy prod' and 'Push to
main' instead of relying on class name matching.
| const [batchLoading, setBatchLoading] = useState(false) | ||
| const [wasConnected, setWasConnected] = useState(false) | ||
|
|
||
| // Track whether WS was ever connected to avoid flash on initial load | ||
| useEffect(() => { | ||
| if (wsConnected) setWasConnected(true) | ||
| }, [wsConnected]) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider using refs for derived state to avoid lint warnings.
The wasConnected state update in the effect (line 60) and batch dialog resets (lines 141-144) trigger lint warnings about synchronous setState in effects. While the conditional guards prevent infinite loops, consider using refs or consolidating state to silence these warnings.
♻️ Alternative pattern for wasConnected
- const [wasConnected, setWasConnected] = useState(false)
-
- // Track whether WS was ever connected to avoid flash on initial load
- useEffect(() => {
- if (wsConnected) setWasConnected(true)
- }, [wsConnected])
+ const wasConnectedRef = useRef(false)
+ if (wsConnected) wasConnectedRef.current = true
+ const wasConnected = wasConnectedRef.current🧰 Tools
🪛 GitHub Check: Dashboard Lint
[warning] 60-60:
Do not call the 'set' function 'setWasConnected' of 'useState' synchronously in an effect. This can lead to unnecessary re-renders and performance issues
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/pages/ApprovalsPage.tsx` around lines 55 - 61, The effect that sets
wasConnected via setWasConnected (and the batch dialog reset handlers that call
setBatchLoading/setWasConnected) creates derived state and causes lint warnings
about synchronous setState in effects; replace wasConnected state with a ref
(useRef) and update that ref inside the useEffect listening to wsConnected
(update ref.current instead of calling setWasConnected), and for batch dialog
resets consolidate or use refs for batchLoading as well (or reset both inside a
single callback) so you avoid conditional setState-in-effect calls; locate and
change uses of wasConnected/setWasConnected and batchLoading/setBatchLoading in
the component (including the useEffect that references wsConnected and the batch
dialog reset code) to read/write the new refs or a single consolidated state
updater.
| // Close batch dialogs when selection is emptied (e.g., by WS updates or optimistic transitions) | ||
| useEffect(() => { | ||
| if (selectedIds.size === 0) { | ||
| setBatchApproveOpen(false) | ||
| setBatchRejectOpen(false) | ||
| setBatchComment('') | ||
| setBatchReason('') | ||
| } | ||
| }, [selectedIds.size]) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Batch state reset triggers multiple renders.
The effect resets four state variables synchronously when selectedIds.size === 0. Consider consolidating into a single object state or using flushSync if the sequential updates cause visual flicker.
♻️ Consolidated batch dialog state
const [batchDialog, setBatchDialog] = useState<{
type: 'approve' | 'reject' | null
comment: string
reason: string
}>({ type: null, comment: '', reason: '' })
useEffect(() => {
if (selectedIds.size === 0) {
setBatchDialog({ type: null, comment: '', reason: '' })
}
}, [selectedIds.size])🧰 Tools
🪛 GitHub Check: Dashboard Lint
[warning] 144-144:
Do not call the 'set' function 'setBatchReason' of 'useState' synchronously in an effect. This can lead to unnecessary re-renders and performance issues
[warning] 143-143:
Do not call the 'set' function 'setBatchComment' of 'useState' synchronously in an effect. This can lead to unnecessary re-renders and performance issues
[warning] 142-142:
Do not call the 'set' function 'setBatchRejectOpen' of 'useState' synchronously in an effect. This can lead to unnecessary re-renders and performance issues
[warning] 141-141:
Do not call the 'set' function 'setBatchApproveOpen' of 'useState' synchronously in an effect. This can lead to unnecessary re-renders and performance issues
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/pages/ApprovalsPage.tsx` around lines 138 - 146, The four separate
state updates (setBatchApproveOpen, setBatchRejectOpen, setBatchComment,
setBatchReason) triggered in the useEffect watching selectedIds.size should be
consolidated to avoid multiple renders: replace those individual states with a
single batchDialog state (e.g., const [batchDialog, setBatchDialog] = useState<{
type: 'approve'|'reject'|null; comment: string; reason: string }>(...)) and
change the effect to call setBatchDialog({ type: null, comment: '', reason: ''
}) when selectedIds.size === 0; alternatively, if you must keep the separate
setters, wrap the four calls in ReactDOM.flushSync to batch them — update all
references to the old state variables (batch open flags, batch comment, batch
reason) to use the new batchDialog and its type/comment/reason fields.
- Timeline: fix text-text-secondary and text-text-muted tokens - BatchActionBar: aria-live="polite" on selection count - RiskGroupSection: data-testid for stable test queries - Drawer: !detailError guard prevents stale content showing with error - Tests: failed IDs stay selected, toolbar a11y, error-hides-content, testid queries Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (7)
web/src/pages/approvals/ApprovalRiskGroupSection.tsx (2)
43-58:⚠️ Potential issue | 🟡 MinorExpose the indeterminate state on the group checkbox.
The checkbox renders as fully unchecked when
selectedIdscontains some but not all ofpendingIds. Set theindeterminateproperty via a ref and usearia-checked="mixed"to surface partial selection to assistive tech.🛠️ Suggested implementation
+import { useRef, useEffect } from 'react' + export function ApprovalRiskGroupSection({ ... }: ApprovalRiskGroupSectionProps) { const Icon = getRiskLevelIcon(riskLevel) const pendingInGroup = items.filter((a) => a.status === 'pending') const pendingIds = pendingInGroup.map((a) => a.id) const allSelected = pendingIds.length > 0 && pendingIds.every((id) => selectedIds.has(id)) + const someSelected = pendingIds.some((id) => selectedIds.has(id)) + const isIndeterminate = someSelected && !allSelected + const checkboxRef = useRef<HTMLInputElement>(null) + + useEffect(() => { + if (checkboxRef.current) { + checkboxRef.current.indeterminate = isIndeterminate + } + }, [isIndeterminate]) // In the checkbox: - <input - type="checkbox" - checked={allSelected} - onChange={...} - className="size-3.5 accent-accent" - /> + <input + ref={checkboxRef} + type="checkbox" + checked={allSelected} + aria-checked={isIndeterminate ? 'mixed' : allSelected} + onChange={...} + className="size-3.5 accent-accent" + />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/approvals/ApprovalRiskGroupSection.tsx` around lines 43 - 58, The group checkbox currently only uses checked={allSelected} so partial selection isn't exposed; add a ref (HTMLInputElement) for the input in ApprovalRiskGroupSection, compute a boolean partialSelected = selectedIds.length > 0 && selectedIds.length < pendingIds.length, and in a useEffect set ref.current!.indeterminate = partialSelected; also set aria-checked={partialSelected ? "mixed" : allSelected ? "true" : "false"} to surface the mixed state to assistive tech, keeping the existing onSelectAll/onDeselectAll logic.
44-62: 🛠️ Refactor suggestion | 🟠 MajorReplace hardcoded spacing/sizing utilities with semantic tokens.
gap-1.5,text-xs,size-3.5, andspace-y-3bypass the project's design token system. Replace with the semantic classes used elsewhere inweb/src(e.g.,gap-sm,text-caption,size-control-sm,space-y-section).As per coding guidelines:
web/src/**/*.{ts,tsx}: Use Tailwind semantic classes or CSS variables. NEVER hardcode spacing in.tsx/.tsfiles.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/approvals/ApprovalRiskGroupSection.tsx` around lines 44 - 62, Replace hardcoded Tailwind utilities in the ApprovalRiskGroupSection JSX with the project's semantic tokens: change the label's className from "flex items-center gap-1.5 text-xs text-secondary cursor-pointer" to use "gap-sm" and "text-caption" (preserving "flex items-center text-secondary cursor-pointer"), change the input's className from "size-3.5 accent-accent" to "size-control-sm accent-accent", and replace the StaggerGroup className "space-y-3" with "space-y-section"; update only these className strings (elements referenced: the label element wrapping the checkbox, the input element, and the StaggerGroup component) so the component uses the semantic spacing/size tokens used across web/src.web/src/__tests__/stores/approvals.test.ts (1)
78-98: 🧹 Nitpick | 🔵 TrivialConsider adding a regression test for out-of-order
fetchApprovalsresponses.This test verifies optimistic state preservation during a single fetch, but doesn't cover the race condition where an older request resolves after a newer one. The store's request-counter guard should discard stale results, but this behavior isn't explicitly tested.
💡 Suggested regression test
it('keeps the newest fetchApprovals result when requests resolve out of order', async () => { const api = await importApi() let resolveFirst!: (value: { data: ApprovalResponse[]; total: number; offset: number; limit: number }) => void let resolveSecond!: (value: { data: ApprovalResponse[]; total: number; offset: number; limit: number }) => void vi.mocked(api.listApprovals) .mockImplementationOnce(() => new Promise((resolve) => { resolveFirst = resolve })) .mockImplementationOnce(() => new Promise((resolve) => { resolveSecond = resolve })) const first = useApprovalsStore.getState().fetchApprovals({ status: 'pending' }) const second = useApprovalsStore.getState().fetchApprovals({ status: 'approved' }) // Resolve second (newer) first resolveSecond({ data: [makeApproval('new')], total: 1, offset: 0, limit: 200 }) await second // Then resolve first (stale) resolveFirst({ data: [makeApproval('old')], total: 1, offset: 0, limit: 200 }) await first // Should keep newer result expect(useApprovalsStore.getState().approvals[0]!.id).toBe('new') })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/__tests__/stores/approvals.test.ts` around lines 78 - 98, Add a regression test that ensures fetchApprovals ignores stale responses by simulating two out-of-order API resolutions: mock api.listApprovals to return two promises (capture resolve functions), call useApprovalsStore.getState().fetchApprovals twice with different query params (e.g., status:'pending' then status:'approved'), resolve the second (newer) promise first and await it, then resolve the first (older) promise and await it, and finally assert that the store's approvals reflect the newer result (use fetchApprovals and the store's request-counter guard to locate behavior to verify).web/src/pages/approvals/ApprovalDetailDrawer.tsx (3)
314-325: 🛠️ Refactor suggestion | 🟠 MajorExtract the metadata entry renderer out of this
.map().The inline
<dt>/<dd>block is still over the repo’s map-body limit and now mixes serialization with layout. A small page-scopedApprovalMetadataEntrycomponent would keep that logic isolated and easier to test. As per coding guidelines, "React: place feature-scoped sub-components in subdirectories (e.g., pages// for page-scoped components). Extract complex (>8 line) JSX from.map()blocks into shared components."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx` around lines 314 - 325, The JSX block rendering approval.metadata inside ApprovalDetailDrawer is too large for an inline .map and mixes serialization with layout; extract it into a small page-scoped component named ApprovalMetadataEntry (e.g., pages/approvals/ApprovalMetadataEntry) that takes props (key: string, value: unknown), handles the typeof/JSON.stringify/String(value ?? '') logic, and returns the <dt>/<dd> layout; then replace the inline map body in ApprovalDetailDrawer with a call to <ApprovalMetadataEntry key={key} .../> so the map only maps to the component.
111-145:⚠️ Potential issue | 🟠 MajorKeep the loading/error states inside the drawer’s tab trap.
When the spinner is the only content, this effect focuses
panel, buthandleTabimmediately returns onnodes.length === 0, soTabcan still escape the modal until real content mounts. If the body is replaced by the error panel, the effect also never re-runs becausedetailErroris not tracked here. Reusing the sharedDrawerwould avoid maintaining these modal edge cases locally.Minimal fix if you keep the local trap
- if (nodes.length === 0) return + if (nodes.length === 0) { + e.preventDefault() + panel.focus() + return + }- }, [open, loading, approval]) // eslint-disable-line `@eslint-react/exhaustive-deps` -- re-evaluate focus when content loads + }, [open, loading, approval, detailError]) // eslint-disable-line `@eslint-react/exhaustive-deps` -- re-evaluate focus when content loadsAs per coding guidelines, "Use
Drawerfor right-side slide-in panel with overlay, spring animation, focus trap, Escape-to-close."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx` around lines 111 - 145, The focus-trap effect allows Tab to escape when there are zero focusable nodes and doesn't re-run when the error panel mounts; update the useEffect that references panelRef, handleTab and deps (open, loading, approval) to also include detailError in the dependency array, and change the tabable-node logic so that when querySelectorAll returns zero nodes you include the panel element itself (make it temporarily focusable with tabindex='-1') into the nodes list so handleTab will trap Tab/Shift+Tab to the panel; alternatively replace this local trap with the shared Drawer component which already implements correct focus trapping and Escape-to-close.
231-232: 🛠️ Refactor suggestion | 🟠 MajorUse semantic typography classes throughout this drawer.
text-[10px],text-[11px], andfont-monoare still hardcoded across badges, section labels, metadata, andMetaField. These should be replaced with the dashboard’s semantic type tokens before more one-off typography gets baked into this surface. As per coding guidelines, "Use Tailwind semantic classes (text-foreground,bg-card,text-accent, etc.) or CSS variables (var(--so-accent)). NEVER hardcode hex values in.tsx/.tsfiles. Use design token variables for colors, typography, spacing, shadows, borders."Also applies to: 254-255, 263-264, 289-290, 301-304, 311-317, 408-409
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx` around lines 231 - 232, Replace all hardcoded typography utility classes in ApprovalDetailDrawer (e.g., text-[10px], text-[11px], font-mono) with the dashboard’s semantic type tokens and design tokens: use Tailwind semantic classes (e.g., text-xs/text-sm or your project’s semantic tokens) and color/token classes (text-foreground, text-accent, bg-card) or CSS variables (var(--so-accent)) instead; update RISK_BADGE_CLASSES, badge class arrays, section labels, MetaField usage, and any inline badge/metadata classNames referenced around the listed occurrences so they use the semantic typography and color tokens rather than one-off hardcoded sizes or fonts. Ensure consistency across all mentions (including the duplicates) so badges, labels, and metadata adopt the same tokenized classes.web/src/pages/approvals/ApprovalTimeline.tsx (1)
63-105: 🛠️ Refactor suggestion | 🟠 MajorExtract the step renderer out of this
.map()body.This block is still carrying all of the step rendering concerns inline. Pulling it into a page-scoped
TimelineStepcomponent will keep the page within the repo’s map-complexity rule and make the state permutations easier to test. As per coding guidelines, "React: place feature-scoped sub-components in subdirectories (e.g., pages// for page-scoped components). Extract complex (>8 line) JSX from.map()blocks into shared components."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/approvals/ApprovalTimeline.tsx` around lines 63 - 105, Extract the inline step renderer into a page-scoped React component named TimelineStep and replace the body of steps.map with <TimelineStep key={step.label} step={step} idx={idx} isLast={idx === steps.length - 1} approvalStatus={approval.status} />; the new TimelineStep should accept props (step, idx, isLast, approvalStatus), render the dot (using DOT_CLASSES[step.state]), label, optional timestamp (formatDate), optional outcome badge (OUTCOME_CLASSES[getApprovalStatusColor(approvalStatus)]), and the connecting line (LINE_CLASSES[step.state]) preserving aria-hidden and className usage and the cn helper; keep all referenced symbols (DOT_CLASSES, LINE_CLASSES, OUTCOME_CLASSES, getApprovalStatusColor, formatDate, cn) in scope and export/define TimelineStep in the same page directory so the map no longer contains complex JSX.
🤖 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/src/__tests__/pages/ApprovalsPage.test.tsx`:
- Around line 100-104: The test uses a fragile class selector for metric values;
update the MetricCard component to add a stable attribute (e.g.,
data-testid="metric-value") on the element that renders the numeric value, then
change the test in ApprovalsPage.test.tsx to select those values via a test id
or role (e.g., container.querySelectorAll('[data-testid="metric-value"]') or
screen.getAllByTestId('metric-value')) instead of using [class*="text-metric"];
update the metricValues/value mapping and the expect assertion to use that new
selector so the test no longer depends on styling classes.
In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx`:
- Around line 369-376: Replace the hand-built textarea elements in
ApprovalDetailDrawer.tsx with the shared InputField component in multiline mode:
locate the textarea usages bound to comment / setComment (the first occurrence
around the block using value={comment} onChange={(e) =>
setComment(e.target.value)} and the second similar block later) and swap them to
<InputField multiline ...> so label/focus/error/hint behavior and
maxLength/aria-label props are preserved; ensure you pass the same placeholder,
aria-label, value, onChange handler, className/custom styling as props supported
by InputField and include maxLength={2000} so the component behaves
equivalently.
In `@web/src/pages/approvals/ApprovalTimeline.tsx`:
- Around line 75-79: The hardcoded typography in ApprovalTimeline (the spans
rendering step.label and the timestamp using text-[10px], text-[9px], and
font-mono) should be replaced with the design-system semantic typography tokens;
update the span for step.label and the timestamp span (and the similar
occurrences at the other noted block) to use the dashboard’s semantic classes
(eg. appropriate text-*-* and font-* tokens such as
text-foreground/text-muted-foreground and the designated small/body typography
token) so the component aligns with the design system and remove the raw
text-[..] and font-mono utilities.
---
Duplicate comments:
In `@web/src/__tests__/stores/approvals.test.ts`:
- Around line 78-98: Add a regression test that ensures fetchApprovals ignores
stale responses by simulating two out-of-order API resolutions: mock
api.listApprovals to return two promises (capture resolve functions), call
useApprovalsStore.getState().fetchApprovals twice with different query params
(e.g., status:'pending' then status:'approved'), resolve the second (newer)
promise first and await it, then resolve the first (older) promise and await it,
and finally assert that the store's approvals reflect the newer result (use
fetchApprovals and the store's request-counter guard to locate behavior to
verify).
In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx`:
- Around line 314-325: The JSX block rendering approval.metadata inside
ApprovalDetailDrawer is too large for an inline .map and mixes serialization
with layout; extract it into a small page-scoped component named
ApprovalMetadataEntry (e.g., pages/approvals/ApprovalMetadataEntry) that takes
props (key: string, value: unknown), handles the
typeof/JSON.stringify/String(value ?? '') logic, and returns the <dt>/<dd>
layout; then replace the inline map body in ApprovalDetailDrawer with a call to
<ApprovalMetadataEntry key={key} .../> so the map only maps to the component.
- Around line 111-145: The focus-trap effect allows Tab to escape when there are
zero focusable nodes and doesn't re-run when the error panel mounts; update the
useEffect that references panelRef, handleTab and deps (open, loading, approval)
to also include detailError in the dependency array, and change the tabable-node
logic so that when querySelectorAll returns zero nodes you include the panel
element itself (make it temporarily focusable with tabindex='-1') into the nodes
list so handleTab will trap Tab/Shift+Tab to the panel; alternatively replace
this local trap with the shared Drawer component which already implements
correct focus trapping and Escape-to-close.
- Around line 231-232: Replace all hardcoded typography utility classes in
ApprovalDetailDrawer (e.g., text-[10px], text-[11px], font-mono) with the
dashboard’s semantic type tokens and design tokens: use Tailwind semantic
classes (e.g., text-xs/text-sm or your project’s semantic tokens) and
color/token classes (text-foreground, text-accent, bg-card) or CSS variables
(var(--so-accent)) instead; update RISK_BADGE_CLASSES, badge class arrays,
section labels, MetaField usage, and any inline badge/metadata classNames
referenced around the listed occurrences so they use the semantic typography and
color tokens rather than one-off hardcoded sizes or fonts. Ensure consistency
across all mentions (including the duplicates) so badges, labels, and metadata
adopt the same tokenized classes.
In `@web/src/pages/approvals/ApprovalRiskGroupSection.tsx`:
- Around line 43-58: The group checkbox currently only uses
checked={allSelected} so partial selection isn't exposed; add a ref
(HTMLInputElement) for the input in ApprovalRiskGroupSection, compute a boolean
partialSelected = selectedIds.length > 0 && selectedIds.length <
pendingIds.length, and in a useEffect set ref.current!.indeterminate =
partialSelected; also set aria-checked={partialSelected ? "mixed" : allSelected
? "true" : "false"} to surface the mixed state to assistive tech, keeping the
existing onSelectAll/onDeselectAll logic.
- Around line 44-62: Replace hardcoded Tailwind utilities in the
ApprovalRiskGroupSection JSX with the project's semantic tokens: change the
label's className from "flex items-center gap-1.5 text-xs text-secondary
cursor-pointer" to use "gap-sm" and "text-caption" (preserving "flex
items-center text-secondary cursor-pointer"), change the input's className from
"size-3.5 accent-accent" to "size-control-sm accent-accent", and replace the
StaggerGroup className "space-y-3" with "space-y-section"; update only these
className strings (elements referenced: the label element wrapping the checkbox,
the input element, and the StaggerGroup component) so the component uses the
semantic spacing/size tokens used across web/src.
In `@web/src/pages/approvals/ApprovalTimeline.tsx`:
- Around line 63-105: Extract the inline step renderer into a page-scoped React
component named TimelineStep and replace the body of steps.map with
<TimelineStep key={step.label} step={step} idx={idx} isLast={idx ===
steps.length - 1} approvalStatus={approval.status} />; the new TimelineStep
should accept props (step, idx, isLast, approvalStatus), render the dot (using
DOT_CLASSES[step.state]), label, optional timestamp (formatDate), optional
outcome badge (OUTCOME_CLASSES[getApprovalStatusColor(approvalStatus)]), and the
connecting line (LINE_CLASSES[step.state]) preserving aria-hidden and className
usage and the cn helper; keep all referenced symbols (DOT_CLASSES, LINE_CLASSES,
OUTCOME_CLASSES, getApprovalStatusColor, formatDate, cn) in scope and
export/define TimelineStep in the same page directory so the map no longer
contains complex JSX.
🪄 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: d5daef8c-dda5-4174-bf61-f07433a6acac
📒 Files selected for processing (8)
web/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/__tests__/stores/approvals.test.tsweb/src/pages/approvals/ApprovalDetailDrawer.tsxweb/src/pages/approvals/ApprovalRiskGroupSection.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/pages/approvals/BatchActionBar.tsx
📜 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)
- GitHub Check: Dashboard Test
- GitHub Check: Build Web
- GitHub Check: Build Backend
- GitHub Check: Dependency Review
- GitHub Check: Analyze (go)
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{py,ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Line length: 88 characters (ruff enforced).
Files:
web/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalRiskGroupSection.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/stores/approvals.test.tsweb/src/pages/approvals/ApprovalDetailDrawer.tsx
web/src/**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
web/src/**/*.{tsx,ts}: Use Tailwind semantic classes (text-foreground,bg-card,text-accent, etc.) or CSS variables (var(--so-accent)). NEVER hardcode hex values in.tsx/.tsfiles. Use design token variables for colors, typography, spacing, shadows, borders.
Importcnfrom@/lib/utilsfor conditional class merging in React components.
Do NOT recreate status dots inline -- use<StatusBadge>. Do NOT build card-with-header layouts from scratch -- use<SectionCard>. Do NOT create metric displays inline -- use<MetricCard>. Do NOT render initials circles manually -- use<Avatar>. Do NOT create complex (>8 line) JSX inside.map()-- extract to a shared component.
UseStatusBadgefor agent/task/system status indicators (colored dot + optional built-in label toggle).
UseMetricCardfor numeric KPIs with sparkline, change badge, progress bar.
UseSectionCardfor titled card wrapper with icon and action slot.
UseAgentCardfor agent display: avatar, name, role, status, current task.
UseDeptHealthBarfor department health: animated fill bar +health(required) +agentCount/taskCount(required).
UseProgressGaugefor circular gauge for budget/utilization (max?defaults to 100).
UseAvatarfor circular initials avatar with optionalborderColor?prop.
UseToast/ToastContainerfor success/error/warning/info notifications with auto-dismiss queue. MountToastContaineronce in AppLayout.
UseSkeleton/SkeletonCard/SkeletonMetric/SkeletonTable/SkeletonTextfor loading placeholders matching component shapes (shimmer animation, respectsprefers-reduced-motion).
UseErrorBoundaryReact error boundary with retry --levelprop:page/section/component.
UseConfirmDialogfor confirmation modal (Radix AlertDialog) withdefault/destructivevariants andloadingstate.
UseCommandPalettefor global Cmd+K search (cmdk + React Router) -- mount once in AppLayout, register commands via `useComma...
Files:
web/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalRiskGroupSection.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/stores/approvals.test.tsweb/src/pages/approvals/ApprovalDetailDrawer.tsx
web/src/__tests__/**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Web dashboard: test with Vitest unit + property tests (
fast-check). Mirrorsrc/structure in__tests__/. Property tests usefc.assert+fc.property.
Files:
web/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/stores/approvals.test.ts
🧠 Learnings (35)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: When review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes), fix them all. No deferring, no 'out of scope' skipping.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `InlineEdit` for click-to-edit text with Enter/Escape, validation, optimistic save with rollback.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.stories.tsx : Post-Training Reference: Storybook 10 - ESM-only; `storybook/addon-docs` is separate; import from `storybook/test` and `storybook/actions`; use `defineMain` and `definePreview`; backgrounds API uses `parameters.backgrounds.options` (object) + `initialGlobals.backgrounds.value`; a11y uses `parameters.a11y.test`; minimum versions: Node 20.19+, Vite 5+, Vitest 3+, TypeScript 4.9+.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/stores/**/*.ts : React: use Zustand stores for global state (auth, WebSocket, toast, analytics, setup wizard, company, agents, budget, tasks, settings, providers). Create per-domain stores for page data.
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/).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `Drawer` for right-side slide-in panel with overlay, spring animation, focus trap, Escape-to-close.
📚 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:
web/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/stores/approvals.test.ts
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/__tests__/**/*.test.{ts,tsx} : Web dashboard: test with Vitest unit + property tests (`fast-check`). Mirror `src/` structure in `__tests__/`. Property tests use `fc.assert` + `fc.property`.
Applied to files:
web/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/stores/approvals.test.ts
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : React: use Linting (ESLint) and type-checking (TypeScript). Run tests with `npm --prefix web run test` (Vitest, coverage scoped to changed files vs origin/main).
Applied to files:
web/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/stores/approvals.test.ts
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/components/ui/*.tsx : When creating new shared React components: place in `web/src/components/ui/` with descriptive kebab-case filename. Create `.stories.tsx` file with all states. Export props as TypeScript interface. Use design tokens exclusively -- no hardcoded colors, fonts, or spacing.
Applied to files:
web/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalRiskGroupSection.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.stories.tsx : Post-Training Reference: Storybook 10 - ESM-only; `storybook/addon-docs` is separate; import from `storybook/test` and `storybook/actions`; use `defineMain` and `definePreview`; backgrounds API uses `parameters.backgrounds.options` (object) + `initialGlobals.backgrounds.value`; a11y uses `parameters.a11y.test`; minimum versions: Node 20.19+, Vite 5+, Vitest 3+, TypeScript 4.9+.
Applied to files:
web/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/stores/approvals.test.ts
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `ConfirmDialog` for confirmation modal (Radix AlertDialog) with `default` / `destructive` variants and `loading` state.
Applied to files:
web/src/__tests__/pages/approvals/BatchActionBar.test.tsxweb/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `DeptHealthBar` for department health: animated fill bar + `health` (required) + `agentCount`/`taskCount` (required).
Applied to files:
web/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalRiskGroupSection.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `SectionCard` for titled card wrapper with icon and action slot.
Applied to files:
web/src/pages/approvals/BatchActionBar.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalRiskGroupSection.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `Drawer` for right-side slide-in panel with overlay, spring animation, focus trap, Escape-to-close.
Applied to files:
web/src/pages/approvals/BatchActionBar.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT recreate status dots inline -- use `<StatusBadge>`. Do NOT build card-with-header layouts from scratch -- use `<SectionCard>`. Do NOT create metric displays inline -- use `<MetricCard>`. Do NOT render initials circles manually -- use `<Avatar>`. Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component.
Applied to files:
web/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalRiskGroupSection.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `PriorityBadge` for task priority colored pill badge (critical/high/medium/low).
Applied to files:
web/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/pages/approvals/ApprovalRiskGroupSection.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, etc.) or CSS variables (`var(--so-accent)`). NEVER hardcode hex values in `.tsx`/`.ts` files. Use design token variables for colors, typography, spacing, shadows, borders.
Applied to files:
web/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/pages/approvals/ApprovalRiskGroupSection.tsxweb/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `StatusBadge` for agent/task/system status indicators (colored dot + optional built-in label toggle).
Applied to files:
web/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/pages/approvals/ApprovalRiskGroupSection.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `ToggleField` for labeled toggle switch (role="switch") with optional description text.
Applied to files:
web/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalTimeline.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `ProviderHealthBadge` for provider health status indicator (up/degraded/down colored dot + optional label).
Applied to files:
web/src/pages/approvals/BatchActionBar.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : React: place feature-scoped sub-components in subdirectories (e.g., pages/<page-name>/ for page-scoped components). Extract complex (>8 line) JSX from `.map()` blocks into shared components.
Applied to files:
web/src/pages/approvals/ApprovalTimeline.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalRiskGroupSection.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/router/**/*.{ts,tsx} : React: use React Router for routing. Lazy-load page components (one per route). Use route guards for auth and setup.
Applied to files:
web/src/__tests__/pages/ApprovalsPage.test.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `ErrorBoundary` React error boundary with retry -- `level` prop: `page` / `section` / `component`.
Applied to files:
web/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalRiskGroupSection.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `MetricCard` for numeric KPIs with sparkline, change badge, progress bar.
Applied to files:
web/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `StaggerGroup` / `StaggerItem` for card entrance stagger container with configurable delay.
Applied to files:
web/src/pages/approvals/ApprovalRiskGroupSection.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/components/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones. These are the shared building blocks.
Applied to files:
web/src/pages/approvals/ApprovalRiskGroupSection.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `AnimatedPresence` for page transition wrapper (Framer Motion AnimatePresence keyed by route).
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `Toast` / `ToastContainer` for success/error/warning/info notifications with auto-dismiss queue. Mount `ToastContainer` once in AppLayout.
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/stores/**/*.ts : React: use Zustand stores for global state (auth, WebSocket, toast, analytics, setup wizard, company, agents, budget, tasks, settings, providers). Create per-domain stores for page data.
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsxweb/src/__tests__/stores/approvals.test.ts
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `TaskStatusIndicator` for task status dot with optional label and pulse animation (accepts `TaskStatus`).
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to tests/**/*.py : 80% minimum code coverage (enforced in CI). Never skip, dismiss, or ignore flaky tests -- fix them fully and fundamentally.
Applied to files:
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `InlineEdit` for click-to-edit text with Enter/Escape, validation, optimistic save with rollback.
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `AgentCard` for agent display: avatar, name, role, status, current task.
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `InputField` for labeled text input with error/hint display, optional multiline textarea mode.
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `Skeleton` / `SkeletonCard` / `SkeletonMetric` / `SkeletonTable` / `SkeletonText` for loading placeholders matching component shapes (shimmer animation, respects `prefers-reduced-motion`).
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-15T21:49:53.264Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:49:53.264Z
Learning: Fix everything valid — never skip when review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes). No deferring, no 'out of scope' skipping.
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: When review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes), fix them all. No deferring, no 'out of scope' skipping.
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 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/src/pages/approvals/ApprovalDetailDrawer.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Pre-commit hook: A PostToolUse hook (`scripts/check_web_design_system.py`) runs automatically on every Edit/Write to `web/src/` files. Catches hardcoded colors, hardcoded fonts, missing Storybook stories, duplicate patterns. Fix all violations -- do not suppress.
Applied to files:
web/src/pages/approvals/ApprovalDetailDrawer.tsx
🔇 Additional comments (6)
web/src/__tests__/pages/ApprovalsPage.test.tsx (2)
35-38: Unusual mock pattern persists but is functional.The computed property
[hookName]pattern is unconventional but works correctly. This was previously noted as a trivial nitpick.
53-131: Good test coverage for ApprovalsPage UI states.The suite covers loading, error, WebSocket disconnection, empty state, metric rendering, and risk-level grouping. The grouping test now uses stable
data-testidattributes (riskgroup-critical,riskgroup-high) which addresses the previous fragility concern.web/src/pages/approvals/ApprovalRiskGroupSection.tsx (1)
63-74: Callback wiring looks correct.
onToggleSelectis wired for checkbox selection toggling whileonSelectopens the detail drawer—matching theApprovalCardinterface contract from the context snippet.web/src/__tests__/pages/approvals/BatchActionBar.test.tsx (1)
1-51: Comprehensive test coverage for BatchActionBar.The suite covers selection count display, all three action button handlers, the loading/disabled state, and toolbar accessibility semantics. The previous suggestion for a
role="toolbar"regression test has been addressed at lines 47-50.web/src/pages/approvals/BatchActionBar.tsx (1)
1-79: Well-implemented component with all previous feedback addressed.The implementation correctly uses:
- Semantic Tailwind classes (
bg-surface,text-foreground,text-success,text-danger,text-muted-foreground,border-border)- Tokenized shadow via CSS variable
- Reused
Buttoncomponent from shared UI- Proper accessibility (
role="toolbar",aria-label,aria-live="polite")- Animation variants defined outside the component
web/src/__tests__/stores/approvals.test.ts (1)
1-465: Comprehensive store test coverage.The suite thoroughly tests:
- CRUD operations (
fetchApprovals,fetchApproval,approveOne,rejectOne)upsertApprovalsemantics includingselectedIdspruning when approvals leave pending- Optimistic updates with rollback closures
- WebSocket event handling and validation
- Batch selection helpers and operations
MAX_BATCH_SIZEenforcement and partial failure rollback with selection retentionPrevious feedback about
MAX_BATCH_SIZEtesting,selectedIdspruning, and failed ID selection retention has been addressed.
| // MetricCard values render with text-metric class; extract values | ||
| const metricValues = container.querySelectorAll('[class*="text-metric"]') | ||
| const values = [...metricValues].map((el) => el.textContent) | ||
| // Order: Critical=2, High=1, Medium=0, Low=0 | ||
| expect(values).toEqual(['2', '1', '0', '0']) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider a more stable selector for metric values.
The query [class*="text-metric"] is fragile—it will break if the metric styling changes. Consider adding a data-testid to the metric value element or using a role-based query if the component structure allows.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/__tests__/pages/ApprovalsPage.test.tsx` around lines 100 - 104, The
test uses a fragile class selector for metric values; update the MetricCard
component to add a stable attribute (e.g., data-testid="metric-value") on the
element that renders the numeric value, then change the test in
ApprovalsPage.test.tsx to select those values via a test id or role (e.g.,
container.querySelectorAll('[data-testid="metric-value"]') or
screen.getAllByTestId('metric-value')) instead of using [class*="text-metric"];
update the metricValues/value mapping and the expect assertion to use that new
selector so the test no longer depends on styling classes.
| <textarea | ||
| value={comment} | ||
| onChange={(e) => setComment(e.target.value)} | ||
| placeholder="Optional comment..." | ||
| className="mt-2 w-full rounded-md border border-border bg-surface px-2 py-1.5 text-sm text-foreground outline-none resize-y focus:ring-2 focus:ring-accent min-h-16" | ||
| aria-label="Approval comment" | ||
| maxLength={2000} | ||
| /> |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use the shared InputField in multiline mode for these dialog forms.
Both confirmation flows are rebuilding textarea labeling and focus styling by hand instead of using the dashboard’s field primitive. Swapping these to InputField keeps form behavior and validation affordances consistent with the rest of web/src. As per coding guidelines, "Use InputField for labeled text input with error/hint display, optional multiline textarea mode."
Also applies to: 390-397
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx` around lines 369 - 376,
Replace the hand-built textarea elements in ApprovalDetailDrawer.tsx with the
shared InputField component in multiline mode: locate the textarea usages bound
to comment / setComment (the first occurrence around the block using
value={comment} onChange={(e) => setComment(e.target.value)} and the second
similar block later) and swap them to <InputField multiline ...> so
label/focus/error/hint behavior and maxLength/aria-label props are preserved;
ensure you pass the same placeholder, aria-label, value, onChange handler,
className/custom styling as props supported by InputField and include
maxLength={2000} so the component behaves equivalently.
| <span className="mt-1.5 text-[10px] font-medium text-secondary">{step.label}</span> | ||
| {/* Timestamp */} | ||
| {step.timestamp && ( | ||
| <span className="mt-0.5 font-mono text-[9px] text-muted-foreground"> | ||
| {formatDate(step.timestamp)} |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Replace the arbitrary text-size/font utilities with semantic typography tokens.
text-[10px], text-[9px], and font-mono are still hardcoded type choices in a web/src surface. These should be swapped to the dashboard’s semantic text/font tokens so this component stays aligned with the design system. As per coding guidelines, "Use Tailwind semantic classes (text-foreground, bg-card, text-accent, etc.) or CSS variables (var(--so-accent)). NEVER hardcode hex values in .tsx/.ts files. Use design token variables for colors, typography, spacing, shadows, borders."
Also applies to: 85-87
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/pages/approvals/ApprovalTimeline.tsx` around lines 75 - 79, The
hardcoded typography in ApprovalTimeline (the spans rendering step.label and the
timestamp using text-[10px], text-[9px], and font-mono) should be replaced with
the design-system semantic typography tokens; update the span for step.label and
the timestamp span (and the similar occurrences at the other noted block) to use
the dashboard’s semantic classes (eg. appropriate text-*-* and font-* tokens
such as text-foreground/text-muted-foreground and the designated small/body
typography token) so the component aligns with the design system and remove the
raw text-[..] and font-mono utilities.
… test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
web/src/__tests__/pages/ApprovalsPage.test.tsx (1)
35-38: 🧹 Nitpick | 🔵 TrivialUse a direct mock export instead of computed property.
This regresses to the previously flagged non-idiomatic pattern; return
useApprovalsDatadirectly for readability and consistency.♻️ Suggested change
vi.mock('@/hooks/useApprovalsData', () => { - const hookName = 'useApprovalsData' - return { [hookName]: () => getApprovalsData() } + return { useApprovalsData: () => getApprovalsData() } })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/__tests__/pages/ApprovalsPage.test.tsx` around lines 35 - 38, The mock factory currently returns a computed property ({ [hookName]: () => getApprovalsData() }) which is non-idiomatic; update the vi.mock(...) factory to export useApprovalsData directly by returning an object with the literal key useApprovalsData (e.g., return { useApprovalsData: () => getApprovalsData() } or return { useApprovalsData: getApprovalsData } if appropriate) so the test mock uses the direct export name instead of a computed property.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@web/src/__tests__/pages/ApprovalsPage.test.tsx`:
- Around line 35-38: The mock factory currently returns a computed property ({
[hookName]: () => getApprovalsData() }) which is non-idiomatic; update the
vi.mock(...) factory to export useApprovalsData directly by returning an object
with the literal key useApprovalsData (e.g., return { useApprovalsData: () =>
getApprovalsData() } or return { useApprovalsData: getApprovalsData } if
appropriate) so the test mock uses the direct export name instead of a computed
property.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 1e8e9b07-7688-4fe1-afbd-8e0f0b7363c5
📒 Files selected for processing (2)
web/src/__tests__/pages/ApprovalsPage.test.tsxweb/src/components/ui/metric-card.tsx
📜 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)
- GitHub Check: Dashboard Test
- GitHub Check: Build Sandbox
- GitHub Check: Build Web
- GitHub Check: Build Backend
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{py,ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Line length: 88 characters (ruff enforced).
Files:
web/src/components/ui/metric-card.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsx
web/src/components/**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
ALWAYS reuse existing components from
web/src/components/ui/before creating new ones. These are the shared building blocks.
Files:
web/src/components/ui/metric-card.tsx
web/src/**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
web/src/**/*.{tsx,ts}: Use Tailwind semantic classes (text-foreground,bg-card,text-accent, etc.) or CSS variables (var(--so-accent)). NEVER hardcode hex values in.tsx/.tsfiles. Use design token variables for colors, typography, spacing, shadows, borders.
Importcnfrom@/lib/utilsfor conditional class merging in React components.
Do NOT recreate status dots inline -- use<StatusBadge>. Do NOT build card-with-header layouts from scratch -- use<SectionCard>. Do NOT create metric displays inline -- use<MetricCard>. Do NOT render initials circles manually -- use<Avatar>. Do NOT create complex (>8 line) JSX inside.map()-- extract to a shared component.
UseStatusBadgefor agent/task/system status indicators (colored dot + optional built-in label toggle).
UseMetricCardfor numeric KPIs with sparkline, change badge, progress bar.
UseSectionCardfor titled card wrapper with icon and action slot.
UseAgentCardfor agent display: avatar, name, role, status, current task.
UseDeptHealthBarfor department health: animated fill bar +health(required) +agentCount/taskCount(required).
UseProgressGaugefor circular gauge for budget/utilization (max?defaults to 100).
UseAvatarfor circular initials avatar with optionalborderColor?prop.
UseToast/ToastContainerfor success/error/warning/info notifications with auto-dismiss queue. MountToastContaineronce in AppLayout.
UseSkeleton/SkeletonCard/SkeletonMetric/SkeletonTable/SkeletonTextfor loading placeholders matching component shapes (shimmer animation, respectsprefers-reduced-motion).
UseErrorBoundaryReact error boundary with retry --levelprop:page/section/component.
UseConfirmDialogfor confirmation modal (Radix AlertDialog) withdefault/destructivevariants andloadingstate.
UseCommandPalettefor global Cmd+K search (cmdk + React Router) -- mount once in AppLayout, register commands via `useComma...
Files:
web/src/components/ui/metric-card.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsx
web/src/components/ui/*.tsx
📄 CodeRabbit inference engine (CLAUDE.md)
When creating new shared React components: place in
web/src/components/ui/with descriptive kebab-case filename. Create.stories.tsxfile with all states. Export props as TypeScript interface. Use design tokens exclusively -- no hardcoded colors, fonts, or spacing.
Files:
web/src/components/ui/metric-card.tsx
web/src/__tests__/**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Web dashboard: test with Vitest unit + property tests (
fast-check). Mirrorsrc/structure in__tests__/. Property tests usefc.assert+fc.property.
Files:
web/src/__tests__/pages/ApprovalsPage.test.tsx
🧠 Learnings (13)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: When review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes), fix them all. No deferring, no 'out of scope' skipping.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `InlineEdit` for click-to-edit text with Enter/Escape, validation, optimistic save with rollback.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/stores/**/*.ts : React: use Zustand stores for global state (auth, WebSocket, toast, analytics, setup wizard, company, agents, budget, tasks, settings, providers). Create per-domain stores for page data.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.stories.tsx : Post-Training Reference: Storybook 10 - ESM-only; `storybook/addon-docs` is separate; import from `storybook/test` and `storybook/actions`; use `defineMain` and `definePreview`; backgrounds API uses `parameters.backgrounds.options` (object) + `initialGlobals.backgrounds.value`; a11y uses `parameters.a11y.test`; minimum versions: Node 20.19+, Vite 5+, Vitest 3+, TypeScript 4.9+.
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/).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `Drawer` for right-side slide-in panel with overlay, spring animation, focus trap, Escape-to-close.
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `MetricCard` for numeric KPIs with sparkline, change badge, progress bar.
Applied to files:
web/src/components/ui/metric-card.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT recreate status dots inline -- use `<StatusBadge>`. Do NOT build card-with-header layouts from scratch -- use `<SectionCard>`. Do NOT create metric displays inline -- use `<MetricCard>`. Do NOT render initials circles manually -- use `<Avatar>`. Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component.
Applied to files:
web/src/components/ui/metric-card.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `SectionCard` for titled card wrapper with icon and action slot.
Applied to files:
web/src/components/ui/metric-card.tsxweb/src/__tests__/pages/ApprovalsPage.test.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : React: use Linting (ESLint) and type-checking (TypeScript). Run tests with `npm --prefix web run test` (Vitest, coverage scoped to changed files vs origin/main).
Applied to files:
web/src/__tests__/pages/ApprovalsPage.test.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/__tests__/**/*.test.{ts,tsx} : Web dashboard: test with Vitest unit + property tests (`fast-check`). Mirror `src/` structure in `__tests__/`. Property tests use `fc.assert` + `fc.property`.
Applied to files:
web/src/__tests__/pages/ApprovalsPage.test.tsx
📚 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:
web/src/__tests__/pages/ApprovalsPage.test.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/components/ui/*.tsx : When creating new shared React components: place in `web/src/components/ui/` with descriptive kebab-case filename. Create `.stories.tsx` file with all states. Export props as TypeScript interface. Use design tokens exclusively -- no hardcoded colors, fonts, or spacing.
Applied to files:
web/src/__tests__/pages/ApprovalsPage.test.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : React: place feature-scoped sub-components in subdirectories (e.g., pages/<page-name>/ for page-scoped components). Extract complex (>8 line) JSX from `.map()` blocks into shared components.
Applied to files:
web/src/__tests__/pages/ApprovalsPage.test.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/router/**/*.{ts,tsx} : React: use React Router for routing. Lazy-load page components (one per route). Use route guards for auth and setup.
Applied to files:
web/src/__tests__/pages/ApprovalsPage.test.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.stories.tsx : Post-Training Reference: Storybook 10 - ESM-only; `storybook/addon-docs` is separate; import from `storybook/test` and `storybook/actions`; use `defineMain` and `definePreview`; backgrounds API uses `parameters.backgrounds.options` (object) + `initialGlobals.backgrounds.value`; a11y uses `parameters.a11y.test`; minimum versions: Node 20.19+, Vite 5+, Vitest 3+, TypeScript 4.9+.
Applied to files:
web/src/__tests__/pages/ApprovalsPage.test.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `ErrorBoundary` React error boundary with retry -- `level` prop: `page` / `section` / `component`.
Applied to files:
web/src/__tests__/pages/ApprovalsPage.test.tsx
📚 Learning: 2026-03-28T10:20:33.391Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T10:20:33.391Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, etc.) or CSS variables (`var(--so-accent)`). NEVER hardcode hex values in `.tsx`/`.ts` files. Use design token variables for colors, typography, spacing, shadows, borders.
Applied to files:
web/src/__tests__/pages/ApprovalsPage.test.tsx
🔇 Additional comments (1)
web/src/components/ui/metric-card.tsx (1)
47-47: Stable test selector added correctly.
data-testid="metric-value"is a good, low-risk addition for resilient tests without
impacting component behavior.
🤖 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
approvalschannel)docs/design/brand-and-ux.mdwith approval utility functions and typesdocs/design/page-structure.mdwith additional URL parametersTest plan
npm --prefix web run type-checkpassesnpm --prefix web run lintpasses (0 errors on changed files)npm --prefix web run testpasses (1222 tests, 101 files)npm --prefix web run storybook:buildbuilds all storiesPre-reviewed by 4 agents (docs-consistency, frontend-reviewer, issue-resolution-verifier, test-quality-reviewer), 15 findings addressed.
Closes #782
🤖 Generated with Claude Code