Skip to content

fix(web): audit cleanup E -- frontend & UX polish (#1710)#1718

Merged
Aureliolo merged 11 commits into
mainfrom
fix/audit-cleanup-e-frontend
May 2, 2026
Merged

fix(web): audit cleanup E -- frontend & UX polish (#1710)#1718
Aureliolo merged 11 commits into
mainfrom
fix/audit-cleanup-e-frontend

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Closes #1710.

Frontend slice of the 2026-05-01 codebase audit. Pure web/src/ -- zero backend overlap.

What changed

Wave 1 -- foundations (cross-cutting primitives)

  • New sanitizeWsEnum<T>(value, allowlist, fallback, { field }) in web/src/utils/ws-sanitize.ts validates a WS-sourced string against an enum allowlist and emits a structured ws.enum.unknown warning + supplied fallback on miss. Replaces 13 unsafe (sanitizeWsString(x, 64) ?? '') as EnumType cast sites in approvals / meetings / messages / tasks stores. Forward-compatible with rolling backend deploys -- a server can ship a new enum value before the client knows it without breaking UI.
  • New typed WS payload contract (web/src/api/types/websocket.ts) -- ~62 per-event payload interfaces + WsEventPayloadMap + WsTypedEvent<T> + WsEventOf<T> mirror of src/synthorg/api/ws_payloads/{_lifecycle,_domain}.py. Compile-time exhaustiveness guard catches drift. Pre-positions a stable contract for backend migration RFC: stringly-typed boundaries -- typed contract at MCP / JWT / WS / audit-chain / A2A / settings export #1711; the dispatch loop continues to deliver the existing untyped WsEvent so per-store migrations roll out incrementally.
  • New shared status-colour module (web/src/styles/status-colors.ts) -- typed Record<EnumValue, string> lookups for ROLE_BADGE_COLORS and ESCALATION_STATUS_BADGE_COLORS replace inline ROLE_PILL_CLASS / STATUS_BADGE_CLASS constants in UsersPage and EscalationQueuePage.

Wave 2 -- a11y + responsive + perf

  • ProgressGauge SVG <title> for screen-reader context.
  • ErrorBoundary's component-level "Try Again" swapped from a bare <button> to <Button variant="link" size="sm"> (inherits design-system contrast + focus ring).
  • min-w-[40rem] on AgentSpendingTable and w-56 on SettingsActionCard are now responsive (md: guards) so 768-1024px tablets don't force horizontal scroll.
  • Recharts (SpendBurnChart, CostBreakdownChart, BudgetBurnChart) now React.lazy + <Suspense fallback={<SkeletonChart />}>. Production build splits a dedicated vendor-charts chunk (~378 KB / ~109 KB gzipped). New <SkeletonChart> variant in @/components/ui/skeleton.
  • React.memo wrappers on ApprovalCard, ChannelListItem, ActivityFeedItem (high-traffic list items).
  • TaskHistory sort + reduce wrapped in useMemo.

Wave 3 -- Storybook + UX consistency

  • New detail-nav-bar.stories.tsx (7 stories: Default, FirstItem, LastItem, PositionHidden, WithLabel, ShortcutsBound, BothDisabled).
  • New useEmptyStateProps hook (web/src/hooks/use-empty-state-props.ts) -- centralises the "no data ever" / "no data after filter" discriminator. Wired into ApprovalsPage and EscalationQueuePage.
  • ListHeader description= applied to WorkflowsPage / ConnectionsPage / BudgetPage / ProvidersPage / EscalationQueuePage / UsersPage / RequestQueuePage.
  • Container utility standardised to space-y-section-gap across ProvidersPage / ConnectionsPage / UsersPage.
  • ConnectionsPage error banner moved to immediately after <ListHeader> (matches OauthAppsPage convention).
  • UsersPage layout converted to a 1/2/3-column grid mirroring ClientListPage.

Wave 4 -- dashboard UX, onboarding, error messages

  • 9 dashboard UX items (agent 47): WorkflowsPage refocus on Clear filters, EscalationQueuePage sort dropdown, ConnectionCard "View webhook receipts" cross-link + ?connection= on WebhookReceiptsPage, ClientListPage SearchInput, ApprovalsPage clickable risk-filter MetricCards (aria-pressed), TaskBoardPage selected-column highlight, SettingsPage Advanced badge next to namespace tabs, RequestQueuePage description noting the legal lifecycle, ProvidersPage description (the <details> collapse already covers progressive disclosure).
  • 7 onboarding gaps (agent 59): createProviderFromPreset returns a { ok, warning? } | { ok: false, error } result-object; SetupAgentCard tracks in-flight name-save to disable Randomize; CompanyStep clears stale errors on unmount; CompleteStep error banner appends a recovery hint; SkipWizardForm distinguishes company-create vs setup-completion failures via the store snapshot; WizardNavigation accepts a nextDisabledReason caption; CompanyStep Company Name InputField gains a hint message.
  • 15 error message UX rewrites (agent 58): 409 message softened, artifacts partial-load restructured, model-discovery messages routed through getErrorMessage, cancellation/rejection reasons gain context, OauthAppCard titles disambiguated, validation-warning past-tense + actionable, TunnelCard clipboard error discrimination. Three audit-58 sub-findings deferred initially are now fully addressed in the post-review commit.

Pre-PR review fixes (final commit, 19 findings from 9 agents)

  • 3 CRITICAL silent-failure fixes: providersWarning slot replaces error-slot misuse, SkipWizardForm reads store snapshot instead of local race-prone flag, ProvidersStep logs fetch failure before toasting.
  • 6 MAJOR fixes: 4 new createProviderFromPreset return-shape tests, new ProvidersStep integration tests, audit comment cleanup, web/CLAUDE.md WS sanitization paragraph, web-package-structure.md hooks + styles inventory, MSW buildValidationError helper, ApprovalDetailDrawer inline-field error, new getCrudErrorTitle error-code routing wired into connections + providers crud-actions.
  • 7 MEDIUM fixes: sanitizeWsEnum.field mandatory, new useEmptyStateProps test, ws-sanitize surrogate-pair test tightened, TunnelCard clipboard DOMException routing, CompanyStep cleanup race guard, websocket.ts sanitisation contract comment, BudgetPage timeout rationale.

Test plan

  • npm --prefix web run lint -- zero warnings
  • npm --prefix web run type-check -- clean
  • npm --prefix web run test -- --run --detect-async-leaks -- 247 files, 2997 tests pass, 75 async leaks (under MAX_ASYNC_LEAKS=90 ceiling)
  • npm --prefix web run storybook:build -- detail-nav-bar story builds
  • npm --prefix web run build -- recharts splits into a vendor-charts chunk

Review coverage

Pre-reviewed by 9 agents (docs-consistency, comment-quality-rot, frontend-reviewer, design-token-audit, security-reviewer, api-contract-drift, test-quality-reviewer, silent-failure-hunter, issue-resolution-verifier). 19 findings surfaced across CRITICAL / MAJOR / MEDIUM / MINOR / INFO; all 19 addressed. Triage table at _audit/pre-pr-review/triage.md.

Out of scope

Deferred per the issue's "Pure web/src/, zero backend overlap" rule:

  • src/synthorg/budget/errors.py -- standardise "exhausted" vs "exceeded" terminology (audit 58 finding 11)
  • src/synthorg/core/domain_errors.py -- add retryable: ClassVar[bool] to ArtifactStorageFullError vs ArtifactContentTooLargeError (audit 58 finding 12)
  • src/synthorg/providers/errors.py -- audit default_message voice consistency (audit 58 finding 15)

Backend migration #1711 imports the typed WS contract from this PR.

@github-actions
Copy link
Copy Markdown
Contributor

ghost commented May 2, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9af35c1b-691d-4a76-add6-2d1d7a8e471f

📥 Commits

Reviewing files that changed from the base of the PR and between c450e68 and fa1e981.

📒 Files selected for processing (1)
  • web/src/pages/approvals/ApprovalCard.tsx
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Deploy Preview
  • GitHub Check: Build Web Assets (melange)
  • GitHub Check: Dashboard Test
  • GitHub Check: Lighthouse Dashboard
  • GitHub Check: Lighthouse Site
  • GitHub Check: CodSpeed Web benchmarks
  • GitHub Check: Analyze (go)
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{py,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{py,ts,tsx}: No default may privilege a single region, currency, or locale. Every user-facing format resolves from: user/company setting -> browser/system -> neutral fallback.
Currency: never hardcode ISO 4217 codes or symbols. Backend: DEFAULT_CURRENCY from synthorg.budget.currency or the runtime budget.currency setting. Frontend: DEFAULT_CURRENCY from @/utils/currencies or useSettingsStore().currency.
Field naming: no _usd suffix on money fields anywhere. The type carries money semantics; the value is in the operator's configured currency.
Locale: never hardcode BCP 47 tags or call bare .toLocaleString() / .toLocaleDateString() / .toLocaleTimeString(). Use helpers in @/utils/format which read getLocale() from @/utils/locale. The backend has no operator-tunable locale setting; backend Intl formatting uses the system locale plus the browser timezone. The company.name_locales list controls procedural-name generation only; it does not feed number / date / time formatting.
Timezone: store UTC; render via Intl without passing timeZone (browser tz wins).
Date / number format: always via Intl; no hand-rolled templates.

Files:

  • web/src/pages/approvals/ApprovalCard.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Units: metric only. Spelling: International / British English UI default (colour, behaviour, organise, centred, analyse, cancelled); document deviations. Spelling here is an editorial / UI-copy decision only; it does not affect runtime locale-sensitive formatting. Numbers, dates, times, currencies, and units still resolve via the user / company / browser / system fallback through @/utils/format, @/utils/locale, DEFAULT_CURRENCY, and useSettingsStore().currency, with no contradiction to the locale-neutral defaults above.

Files:

  • web/src/pages/approvals/ApprovalCard.tsx
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Reuse components from web/src/components/ui/ before creating new ones. Never hardcode hex colors, font-family, pixel spacing, Motion transitions, or BCP 47 locale strings; use design tokens, @/lib/motion presets, and the helpers in @/utils/format. Enforced by scripts/check_web_design_system.py (PostToolUse hook on every web/src/ edit).

Files:

  • web/src/pages/approvals/ApprovalCard.tsx
web/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Always use createLogger from @/lib/logger with variable name log (e.g., const log = createLogger('module-name')); never use bare console.warn/console.error/console.debug in application code except in logger.ts itself

For static messages in logs, pass dynamic/untrusted values as separate args (not interpolated into the message string) so they go through sanitizeArg; attacker-controlled fields inside structured objects must be wrapped in sanitizeForLog() before embedding

Files:

  • web/src/pages/approvals/ApprovalCard.tsx
web/**/{components,pages,src/{components,pages}}/**/*.{tsx,ts}

📄 CodeRabbit inference engine (web/CLAUDE.md)

ALWAYS reuse existing components from web/src/components/ui/ before creating new ones; NEVER hardcode hex colors, font-family declarations, pixel spacing, Motion transition durations, BCP 47 locale literals ('en-US'), or currency symbols/codes; use design tokens, @/lib/motion presets, helpers in @/utils/format, and DEFAULT_CURRENCY

Files:

  • web/src/pages/approvals/ApprovalCard.tsx
web/**/pages/**/*.{tsx,ts}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Use useEmptyStateProps({ filteredCount, totalCount, filterActive, empty, filtered }) from @/hooks/use-empty-state-props to return EmptyStateProps | null so pages branch on a single value instead of duplicating the 'no data ever' / 'no data after filter' discriminator

List-page root container should use space-y-section-gap (the majority pattern -- flex flex-col gap-section-gap is equivalent but discouraged); <ErrorBanner> lands immediately after <ListHeader>, before any filter/pagination row; pages with a one-line mission statement pass it via <ListHeader description="..." />

Files:

  • web/src/pages/approvals/ApprovalCard.tsx
web/**/{pages,components,src/{pages,components}}/**/*.{tsx,ts}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Reuse existing design system components: Status dots -> <StatusBadge>; KPI displays -> <MetricCard> / <Sparkline> / <ProgressGauge> / <TokenUsageBar>; Cards -> <SectionCard>, <AgentCard>, <DeptHealthBar>; Form fields -> <InputField> / <SelectField> / <SliderField> / <ToggleField> / <SegmentedControl> / <TagInput> / <SearchInput>; Slide-in panels -> <Drawer>; State feedback -> <Skeleton> family / <EmptyState> / <ErrorBoundary> / <ErrorBanner> / <ProgressIndicator>; List-page primitives -> <ListHeader> / <SearchFilterSort> / <Pagination> / <BulkActionBar> / <MetadataGrid> / <Breadcrumbs>; Confirmation/toasts -> <ConfirmDialog> / <Toast>; Cmd+K/shortcuts -> <CommandPalette> / <KeyboardShortcutHint> / <CommandCheatsheet>; Animation -> <AnimatedPresence> / <StaggerGroup> / <LiveRegion>

Status / role / risk / urgency badge classes must use STATUS_COLORS family from @/styles/status-colors (typed Record<EnumValue, string> lookups); no inline Record<EnumValue, string> constants per page

Files:

  • web/src/pages/approvals/ApprovalCard.tsx
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T15:51:31.490Z
Learning: List layout choice: use Kanban grouping for status-flow domains where each row's column conveys lifecycle phase (Tasks, Requests); use a flat scrollable list for queues without explicit phase semantics (Escalations, Approvals)
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T15:51:31.490Z
Learning: CI fails if `vitest --detect-async-leaks` reports more than `MAX_ASYNC_LEAKS` (currently 90); raise the ceiling only with documented per-PR justification; the structural floor is MSW 2.x + axios + tough-cookie
🔇 Additional comments (4)
web/src/pages/approvals/ApprovalCard.tsx (4)

1-1: LGTM!

Import additions (memo, useRef) align with the memoization wrapper and timer ref pattern introduced below.


49-109: Well-structured countdown logic addressing prior review findings.

The refactored implementation correctly:

  • Restarts the interval when approval.seconds_remaining changes (effect keyed on the prop)
  • Clears the interval when countdown reaches zero via the sibling effect
  • Uses the cancelled flag to guard against post-cleanup state updates in both the microtask and interval callback
  • Keeps the state updater pure by moving the clearInterval side effect to a separate effect

The detailed inline comment (lines 49-66) is helpful for future maintainers.


19-27: LGTM!

The *Impl suffix follows the established pattern for memoized components in this codebase (consistent with ActivityFeedItem, ChannelListItem).


242-248: No action required; memoization is already effective.

The parent component (ApprovalsPage.tsx line 212) already wraps groupByRiskLevel() in useMemo with filtered as the dependency. This means the approval object references remain stable across re-renders when the filtered data hasn't changed, so the memo() wrapper on ApprovalCard will prevent unnecessary re-renders as intended.

			> Likely an incorrect or invalid review comment.

Walkthrough

Adds a typed WebSocket payload contract and introduces sanitizeWsEnum (re-exported for stores); multiple stores were migrated to enum-safe sanitization with explicit fallbacks and some ingress allowlist checks relaxed. Introduces runtime enum value arrays and a SignatureAlgorithm type. Adds useEmptyStateProps and tests, a SkeletonChart component and lazy-loaded charts, memoization/useMemo optimizations across several components, centralized ROLE_BADGE_COLORS/ESCALATION_STATUS_BADGE_COLORS with tests, updated error/CRUD messaging helpers, numerous page/UI refinements, new Storybook stories, and many corresponding tests and documentation edits.

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview May 2, 2026 12:35 — with GitHub Actions Inactive
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request standardizes UI patterns across the dashboard by introducing a centralized useEmptyStateProps hook and shared status-colors configuration. It optimizes performance through lazy loading of chart components and strengthens security with enhanced WebSocket payload sanitization for strings and enums. Key functional updates include search and sorting capabilities for list pages, improved error reporting for API conflicts and clipboard actions, and expanded test suites for core stores and utilities. The review feedback correctly identifies a logical discrepancy in a new test case, a semantic inconsistency in filtered count reporting on the Approvals page, and a state management conflict where a store update would trigger an unwanted error banner.

const { result } = renderHook(() =>
useEmptyStateProps({
filteredCount: 0,
totalCount: 0,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The test description states totalCount > 0, but the test data provides totalCount: 0. To properly test the logic where a filter is inactive but the list is empty (or to test the 'out-of-sync' scenario mentioned in the comment), totalCount should be set to a positive value.

Suggested change
totalCount: 0,
totalCount: 5,

Comment thread web/src/pages/ApprovalsPage.tsx Outdated
// state below short-circuits before the empty-state branch ever
// matters.
const emptyStateProps = useEmptyStateProps({
filteredCount: grouped.size,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Inconsistency in filteredCount value: grouped.size represents the number of risk categories (keys in the map), not the total number of filtered items. While this works for the current hook implementation's > 0 check, it is semantically incorrect and differs from the usage in other pages (e.g., EscalationQueuePage.tsx).

Suggested change
filteredCount: grouped.size,
filteredCount: filteredApprovals.length,

preset: presetName,
error: fetchErrMsg,
})
useToastStore.getState().add({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment at line 112 states that the error banner stays clean, but fetchProviders() in the store automatically sets providersError upon failure, which triggers the global ErrorBanner. To ensure the banner stays clean as intended when only the refresh fails after a successful creation, providersError should be explicitly cleared here.

Suggested change
useToastStore.getState().add({
useSetupWizardStore.setState({ providersError: null })
useToastStore.getState().add({

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 2, 2026

Merging this PR will not alter performance

✅ 21 untouched benchmarks
⏩ 33 skipped benchmarks1


Comparing fix/audit-cleanup-e-frontend (fa1e981) with main (015d352)

Open in CodSpeed

Footnotes

  1. 33 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
web/src/pages/UsersPage.tsx (1)

109-136: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Place ErrorBanner directly after ListHeader on this list page

The current order renders SearchFilterSort before the load error banner. This breaks the list-page layout contract and weakens error discoverability.

As per coding guidelines: "web/src/pages/**/*.tsx: <ErrorBanner> lands immediately after <ListHeader>, before filter/pagination rows."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/UsersPage.tsx` around lines 109 - 136, Move the ErrorBanner so
it is rendered immediately after the ListHeader in UsersPage: currently
ErrorBanner is placed after the SearchFilterSort component; change the JSX order
so ListHeader is followed directly by ErrorBanner (keeping the same props:
severity, title, description, onRetry that calls fetchUsers), and then render
SearchFilterSort and the rest (SearchInput uses searchQuery and setSearchQuery)
to satisfy the project layout contract.
web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx (1)

170-183: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add an assertion for the new inline reject-reason error

This test now checks the toast title, but it should also assert the inline field error (the newly added primary UX signal) to protect against regressions.

Suggested assertion addition
   const rejectDialog = screen.getByRole('alertdialog')
   await user.click(within(rejectDialog).getByRole('button', { name: /reject/i }))
   // Should show toast error -- onReject should NOT have been called
   expect(defaultHandlers.onReject).not.toHaveBeenCalled()
+  expect(
+    within(rejectDialog).getByText(/rejection requires a reason for the approval record/i),
+  ).toBeInTheDocument()
   const toasts = useToastStore.getState().toasts
   expect(toasts.some((t) => t.title === 'Rejection reason required')).toBe(true)
🤖 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
170 - 183, Add an assertion that the inline field error is shown when attempting
to reject without a reason: after opening the alertdialog (rejectDialog) and
clicking its Reject button, query the reject reason input/field within
rejectDialog (e.g., by role textbox or label used in the component) and assert
that the inline validation message or error state is present (for example check
for the error text "Rejection reason required" or that the input has
aria-invalid/aria-errormessage pointing to the error). Keep the existing toast
and onReject assertions (defaultHandlers.onReject and useToastStore checks) and
scope the field lookup to rejectDialog (use within(rejectDialog)) so the check
targets the inline error, not the drawer button.
web/src/stores/messages.ts (1)

121-123: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

message.type and message.priority are still unsafe enum casts.

Line 176 and Line 177 cast sanitized strings to union types without allowlist validation. Unknown WS values can still enter state and break downstream UI assumptions; clamp these with sanitizeWsEnum just like attachments[].type.

Suggested fix pattern
-  const type = sanitizeWsString(c.type, 64) ?? ''
-  const priority = sanitizeWsString(c.priority, 64) ?? ''
+  const type = sanitizeWsEnum(c.type, MESSAGE_TYPE_VALUES, 'task_update', {
+    maxLen: 64,
+    field: 'message.type',
+  })
+  const priority = sanitizeWsEnum(c.priority, MESSAGE_PRIORITY_VALUES, 'normal', {
+    maxLen: 64,
+    field: 'message.priority',
+  })

@@
-    type: type as Message['type'],
-    priority: priority as Message['priority'],
+    type,
+    priority,
// web/src/api/types/messages.ts
+export const MESSAGE_TYPE_VALUES = [
+  'task_update', 'question', 'announcement', 'review_request',
+  'approval', 'delegation', 'status_report', 'escalation',
+  'meeting_contribution', 'hr_notification',
+] as const satisfies readonly MessageType[]
+
+export const MESSAGE_PRIORITY_VALUES = [
+  'low', 'normal', 'high', 'urgent',
+] as const satisfies readonly MessagePriority[]
As per coding guidelines: “Use `sanitizeWsEnum(value, allowlist, fallback, { field })` for enum validation” and “raw `(sanitizeWsString(x, n) ?? '') as EnumType` casts are forbidden”.

Also applies to: 176-177

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/messages.ts` around lines 121 - 123, Replace the unsafe enum
casts for message.type and message.priority (currently using
sanitizeWsString(...) as EnumType) with allowlist-validated calls to
sanitizeWsEnum; specifically, pass the sanitized string into
sanitizeWsEnum<T>(value, allowlist, fallback, { field: 'type'|'priority' })
using the correct enum allowlists and sensible fallback values, mirroring how
attachments[].type is validated, so unknown WS values are clamped instead of
being cast through.
web/src/pages/setup/SkipWizardForm.tsx (1)

30-55: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t rely on catch to detect submitCompany() failure.

submitCompany() already reports ordinary failures through store state, so this flow can still call wizardCompleteSetup() after company creation failed and then show the wrong error branch. Please gate the second step on the post-submitCompany() store state/return contract and let the store-owned error slots drive the banner instead of a local try/catch.

As per coding guidelines "Callers MUST NOT wrap store mutation calls in try / catch; the store owns the error UX".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/SkipWizardForm.tsx` around lines 30 - 55, The current
try/catch allows wizardCompleteSetup() to run even if submitCompany() failed;
you must instead check the post-submit store state or submitCompany's return
contract before proceeding and avoid wrapping store mutation calls in try/catch.
After calling submitCompany(), read
useSetupWizardStore.getState().companyResponse (or the store's explicit success
flag/err slot) and only call wizardCompleteSetup(),
setCompanyNameStore(trimmed), useSetupStore.setState({ setupComplete: true }),
show the success toast and navigate('/') when the store indicates the company
was actually created; if the store reports an error, surface that via the
store-owned error slots (and setError from those slots) rather than relying on
catch to choose the branch. Ensure no store mutation is wrapped in try/catch per
the guideline and rely on useSetupWizardStore state to drive the UI flow.
web/src/stores/setup-wizard/providers.ts (1)

64-74: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear providersWarning on a successful providers refresh.

providersWarning now backs a persistent banner, but fetchProviders() only resets providersError. After the user clicks Retry on the new warning banner and the reload succeeds, the warning still sticks around because nothing clears it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/setup-wizard/providers.ts` around lines 64 - 74,
fetchProviders currently clears providersError but not providersWarning, so a
successful refresh leaves the persistent warning banner visible; update the
async fetchProviders function to also clear providersWarning on success (e.g.,
call set with providersWarning: null alongside providers and
providersLoading:false) after listProviders resolves, while leaving the existing
error handling intact that sets providersError and providersLoading on catch;
locate the fetchProviders method and add providersWarning: null to the
successful set 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__/hooks/use-empty-state-props.test.ts`:
- Around line 68-75: The test for useEmptyStateProps currently sets totalCount:
0 but its name requires totalCount > 0; update the test input in the it(...)
block so filteredCount: 0, totalCount: a positive number (e.g., 5), and
filterActive: false to exercise the branch where items exist but the filter is
inactive; keep the rest of the assertion the same so the test validates the
"empty branch when totalCount > 0 but filter is NOT active" scenario.

In `@web/src/__tests__/pages/setup/ProvidersStep.test.tsx`:
- Around line 84-146: Tests call store methods directly
(createProviderFromPreset/fetchProviders) and thus bypass
ProvidersStep.handleAddLocal, so the component's toast-on-refresh-failure path
isn't exercised; modify the tests to drive the component UI instead of the
store: render ProvidersStep, locate and trigger the UI control that invokes
handleAddLocal (e.g., the PresetPicker add button or the same event the
PresetPickerSections would call), and then assert that a toast warning is
produced while providersError remains null; keep references to ProvidersStep,
handleAddLocal, createProviderFromPreset, fetchProviders, and
useSetupWizardStore to guide locating the affected logic.

In `@web/src/__tests__/utils/ws-sanitize.test.ts`:
- Around line 93-97: The test currently passes a plain 'completed' so it doesn't
exercise control-character stripping; update the input to include an embedded
control character (e.g., 'c\u0000ompleted' or 'comp\u0001leted') when calling
sanitizeWsEnum so that sanitizeWsEnum(TASK_STATUS, default 'created', FIELD)
actually strips the control char and returns 'completed', and ensure the
existing expectations (expect(...).toBe('completed') and
expect(warnSpy).not.toHaveBeenCalled()) remain unchanged; reference
sanitizeWsEnum, TASK_STATUS and FIELD to locate and modify the test input.

In `@web/src/components/ui/detail-nav-bar.stories.tsx`:
- Around line 10-14: The Storybook meta sets meta.args.bindShortcuts = false but
the ShortcutsBound story text claims bindShortcuts: true is "the component
default", causing a mismatch; update either meta.args.bindShortcuts to the
actual component default (set meta.args.bindShortcuts = true if the component
default is true) or change the ShortcutsBound story description to state the
actual default, and ensure any per-story args explicitly override meta.args
(e.g., ShortcutsBound.args = { bindShortcuts: true }) so the story and the
component default (refer to meta.args.bindShortcuts and the ShortcutsBound
story) are consistent.
- Around line 51-65: Add Storybook play() interaction tests for the navigation
stories: in the PositionHidden story (and the other affected stories around
lines 76-91) implement a play() that dispatches keyboard events (e.g., 'j', 'k',
'ArrowLeft', 'ArrowRight') on window and asserts the mock handlers
(onPrev/onNext) are called when position is set and not called when position is
null or when shortcuts are disabled; also dispatch events with
document.activeElement as an input/contenteditable to assert handlers are not
invoked. Locate the PositionHidden Story object and the other Story exports, add
a play() that uses window.dispatchEvent(new KeyboardEvent(...)) and
jest/expect-style assertions against the passed-in onPrev/onNext mocks to verify
listener binding and non-binding behavior.

In `@web/src/pages/approvals/ApprovalDetailDrawer.tsx`:
- Line 71: The inline validation state reasonError (managed by reasonError and
setReasonError) is only cleared on dialog close but not when the approval
context or pending state resets, causing stale errors to persist; update the
same reset logic that clears other local state when the approval or pending
flags change (the handler that resets form state when approval context resets or
pending state flips) to also call setReasonError(null) so reasonError is cleared
whenever the component resets its form/approval state.

In `@web/src/pages/ClientListPage.tsx`:
- Around line 73-85: The page is performing inline conditional rendering between
empty and filtered-empty states; replace this branching by using the shared hook
useEmptyStateProps to centralize behavior. Import useEmptyStateProps from
"@/hooks/use-empty-state-props", call it with the appropriate args (totalCount:
clients.length, filteredCount: filteredClients.length, filterActive:
Boolean(searchQuery), empty: clients.length === 0, filtered:
filteredClients.length === 0) and, if it returns props, render <EmptyState
{...props} /> and wire the action to call setSearchQuery('') where needed;
remove the existing clients/filteredClients nested ternary rendering and rely on
the hook's returned EmptyStateProps or null.

In `@web/src/pages/connections/TunnelCard.tsx`:
- Around line 20-21: Update the user-facing clipboard hint string used for
clipboard access errors (the NotAllowedError message in TunnelCard.tsx) to use a
cross-platform shortcut phrasing like "Ctrl/Cmd+C" instead of "Ctrl+C"; locate
the NotAllowedError message constant or export in TunnelCard.tsx and replace the
literal text so macOS users see the correct shortcut wording.

In `@web/src/pages/EscalationQueuePage.tsx`:
- Around line 139-156: The empty-state logic needs to treat a server-side status
filter as an active filter even when the returned escalations array is empty;
create a serverStatusFilterActive flag (e.g., const serverStatusFilterActive =
statusFilter !== null && statusFilter !== undefined), compute an effective
filter flag (e.g., const effectiveFilterActive = serverStatusFilterActive ||
priorityFilter !== 'all'), and call useEmptyStateProps with filterActive:
effectiveFilterActive and totalCount set to escalations.length ||
(serverStatusFilterActive ? 1 : 0) so the component resolves to the filtered
empty-state when a server-side status filter is active but returns zero rows;
update references to filterActive, statusFilter, priorityFilter,
emptyStateProps, useEmptyStateProps, visibleEscalations, and escalations
accordingly.

In `@web/src/pages/SettingsPage.tsx`:
- Around line 393-412: The span rendered when advancedMode is true is
non-interactive and already contains the visible text "Advanced", so remove the
redundant aria-label on that span (the element immediately following
NamespaceTabBar) to avoid conflicting accessible names; if you intended to
expose this as a dynamic state to assistive tech instead of visible text,
replace the aria-label with a proper live-region/role (e.g., role="status" or
aria-live="polite") on the same span.

In `@web/src/pages/setup/ProvidersStep.tsx`:
- Around line 100-124: fetchProviders never throws (it sets providersError in
the store), so the try/catch around await fetchProviders() in ProvidersStep
(after createProviderFromPreset) is dead and the UI still shows the hard “Failed
to load providers” banner; fix by either making fetchProviders rethrow on
failure or (recommended) check the store error after the call: call await
fetchProviders(), then read the providersError (or providers state) from the
providers store used by ProvidersStep and if an error exists, perform the
existing logging/toast code (log.warn('fetch_providers_after_create_failed', {
preset: presetName, error: getErrorMessage(providersError) }) and
useToastStore.getState().add(...)) and clear or suppress providersError so the
banner does not appear; update ProvidersStep to reference
createProviderFromPreset, fetchProviders, providersError and useToastStore
accordingly.

In `@web/src/pages/setup/SetupAgentCard.tsx`:
- Around line 71-75: The Randomize button currently calls onRandomizeName
fire-and-forget while only using the global nameSaving flag, allowing
overlapping mutations; change the behavior so the button is truly disabled
during the async mutation by adding and using a reentrancy guard: either make
onRandomizeName return a Promise and change the onClick handler to async/await
(e.g., onClick={async () => { if (isRandomizing[index]) return;
setIsRandomizing(index, true); await onRandomizeName(index);
setIsRandomizing(index, false); }}) or implement a per-index boolean state
(e.g., isRandomizing array or currentRandomizingIndex) checked before starting
the mutation and set while awaiting; update the Button disabled prop to use this
new per-index flag instead of only nameSaving and ensure onRandomizeName itself
early-returns if the flag is set to prevent concurrent writes.

In `@web/src/pages/WebhookReceiptsPage.tsx`:
- Around line 50-55: The selected connection is only seeded once via
initialFromUrl and useState, so subsequent changes to the URL query
(?connection=...) won't update the selection; add a synchronization effect that
reads searchParams.get('connection') and calls setSelected when it changes (and
only if the new value differs) so the component (symbols: useSearchParams,
initialFromUrl, selected, setSelected) updates its selection and fetch target
whenever the URL query param changes.

In `@web/src/stores/approvals.ts`:
- Around line 215-218: The runtime is rejecting unknown enum members before
sanitizeWsEnum can provide fallbacks; update the WS shape validators
(isApprovalShape and isEvidencePackageShape) to accept raw strings for the enum
fields (status, risk_level, urgency_level) instead of only the known union types
so sanitizeApproval/sanitizeEvidencePackage can run; specifically change the
checks that currently enforce concrete enum values to allow typeof value ===
'string' (or route through sanitizeWsString) for those fields, then keep
sanitizeWsEnum(s) in sanitizeApproval/sanitizeEvidencePackage to apply warnings
and fallbacks; apply the same relaxation to the other validator locations
referenced around the approval/evidence package handling so incoming unknown
enum values are sanitized rather than dropping the payload.

In `@web/src/stores/meetings.ts`:
- Around line 270-275: The shape guards isMeetingMinutesShape and isMeetingShape
currently reject unknown enum values causing sanitizeWsEnum (and its fallback)
to never run; update those guards so that for fields protocol_type and status
they only check typeof field === 'string' (not membership in
MEETING_PROTOCOL_TYPE_VALUES/MEETING_STATUS_VALUES), then let sanitizeWsEnum
(and sanitizeWsString where applicable) enforce the allowlist and fallbacks;
also ensure any new WS payload handlers that accept untrusted strings route
those fields through sanitizeWsString/sanitizeWsEnum as per guidelines.

In `@web/src/stores/messages.ts`:
- Around line 6-7: Replace the current imports of sanitizeWsEnum and
sanitizeWsString in web/src/stores/messages.ts so they come from the dedicated
utility module instead of the notifications store: locate the import statement
that references '@/stores/notifications' and update it to import sanitizeWsEnum
and sanitizeWsString from 'web/src/utils/ws-sanitize.ts' (preserving
ATTACHMENT_TYPE_VALUES import and other symbols), ensuring only the sanitizer
functions are moved to the utils module to avoid cross-store coupling and
circular init risks.

In `@web/src/stores/tasks.ts`:
- Around line 146-157: The WS task handler currently rejects whole frames
because isTaskShape() enforces that task.type, task.status, task.priority, and
task.source already match allowlists; move allowlist enforcement into
sanitizeWsEnum()/sanitizeWsString() by changing isTaskShape() (the guard used by
handleWsEvent()) to only validate those fields as string or nullish (not
membership), then call sanitizeTask() which uses
sanitizeWsEnum()/sanitizeWsString() to apply allowlist fallbacks and lengths;
update the same pattern for the other occurrences around the sanitizeWsEnum()
calls (the block at the second location) so new backend enum values are accepted
and safely normalized instead of being dropped.

In `@web/src/utils/errors.ts`:
- Around line 158-183: The toast currently conflates 401 and 403; update the
Axios status handling in errors.ts (the block using isAxiosError(error) and
error.response?.status) to return { title: 'Authentication failed' } only for
status === 401 and return a distinct title for status === 403 (e.g., { title:
'Not authorized' } or 'Authorization failed') so CRUD toasts correctly reflect
authentication vs authorization; keep existing
getErrorDetail(detail.error_category === 'auth') mapping as-is and adjust the
if/return order or branching to check 401 and 403 separately.

---

Outside diff comments:
In `@web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx`:
- Around line 170-183: Add an assertion that the inline field error is shown
when attempting to reject without a reason: after opening the alertdialog
(rejectDialog) and clicking its Reject button, query the reject reason
input/field within rejectDialog (e.g., by role textbox or label used in the
component) and assert that the inline validation message or error state is
present (for example check for the error text "Rejection reason required" or
that the input has aria-invalid/aria-errormessage pointing to the error). Keep
the existing toast and onReject assertions (defaultHandlers.onReject and
useToastStore checks) and scope the field lookup to rejectDialog (use
within(rejectDialog)) so the check targets the inline error, not the drawer
button.

In `@web/src/pages/setup/SkipWizardForm.tsx`:
- Around line 30-55: The current try/catch allows wizardCompleteSetup() to run
even if submitCompany() failed; you must instead check the post-submit store
state or submitCompany's return contract before proceeding and avoid wrapping
store mutation calls in try/catch. After calling submitCompany(), read
useSetupWizardStore.getState().companyResponse (or the store's explicit success
flag/err slot) and only call wizardCompleteSetup(),
setCompanyNameStore(trimmed), useSetupStore.setState({ setupComplete: true }),
show the success toast and navigate('/') when the store indicates the company
was actually created; if the store reports an error, surface that via the
store-owned error slots (and setError from those slots) rather than relying on
catch to choose the branch. Ensure no store mutation is wrapped in try/catch per
the guideline and rely on useSetupWizardStore state to drive the UI flow.

In `@web/src/pages/UsersPage.tsx`:
- Around line 109-136: Move the ErrorBanner so it is rendered immediately after
the ListHeader in UsersPage: currently ErrorBanner is placed after the
SearchFilterSort component; change the JSX order so ListHeader is followed
directly by ErrorBanner (keeping the same props: severity, title, description,
onRetry that calls fetchUsers), and then render SearchFilterSort and the rest
(SearchInput uses searchQuery and setSearchQuery) to satisfy the project layout
contract.

In `@web/src/stores/messages.ts`:
- Around line 121-123: Replace the unsafe enum casts for message.type and
message.priority (currently using sanitizeWsString(...) as EnumType) with
allowlist-validated calls to sanitizeWsEnum; specifically, pass the sanitized
string into sanitizeWsEnum<T>(value, allowlist, fallback, { field:
'type'|'priority' }) using the correct enum allowlists and sensible fallback
values, mirroring how attachments[].type is validated, so unknown WS values are
clamped instead of being cast through.

In `@web/src/stores/setup-wizard/providers.ts`:
- Around line 64-74: fetchProviders currently clears providersError but not
providersWarning, so a successful refresh leaves the persistent warning banner
visible; update the async fetchProviders function to also clear providersWarning
on success (e.g., call set with providersWarning: null alongside providers and
providersLoading:false) after listProviders resolves, while leaving the existing
error handling intact that sets providersError and providersLoading on catch;
locate the fetchProviders method and add providersWarning: null to the
successful set 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: 8f2b9816-69e5-4f17-af20-228becff2d2c

📥 Commits

Reviewing files that changed from the base of the PR and between ade03b7 and 4bedae1.

📒 Files selected for processing (72)
  • docs/reference/web-package-structure.md
  • web/CLAUDE.md
  • web/src/__tests__/components/ui/progress-gauge.test.tsx
  • web/src/__tests__/hooks/use-empty-state-props.test.ts
  • web/src/__tests__/pages/BudgetPage.test.tsx
  • web/src/__tests__/pages/DashboardPage.test.tsx
  • web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
  • web/src/__tests__/pages/setup/ProvidersStep.test.tsx
  • web/src/__tests__/stores/artifacts.test.ts
  • web/src/__tests__/stores/setup-wizard.test.ts
  • web/src/__tests__/styles/status-colors.test.ts
  • web/src/__tests__/utils/errors.test.ts
  • web/src/__tests__/utils/ws-sanitize.test.ts
  • web/src/api/types/approvals.ts
  • web/src/api/types/enums.ts
  • web/src/api/types/messages.ts
  • web/src/api/types/websocket.ts
  • web/src/components/layout/StatusBar.tsx
  • web/src/components/ui/detail-nav-bar.stories.tsx
  • web/src/components/ui/error-boundary.tsx
  • web/src/components/ui/progress-gauge.tsx
  • web/src/components/ui/skeleton.tsx
  • web/src/hooks/use-empty-state-props.ts
  • web/src/mocks/handlers/helpers.ts
  • web/src/mocks/handlers/index.ts
  • web/src/pages/ApprovalsPage.tsx
  • web/src/pages/BudgetForecastPage.tsx
  • web/src/pages/BudgetPage.tsx
  • web/src/pages/ClientListPage.tsx
  • web/src/pages/ConnectionsPage.tsx
  • web/src/pages/DashboardPage.tsx
  • web/src/pages/EscalationQueuePage.tsx
  • web/src/pages/ProvidersPage.tsx
  • web/src/pages/RequestQueuePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/TaskBoardPage.tsx
  • web/src/pages/UsersPage.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/WorkflowsPage.tsx
  • web/src/pages/agents/TaskHistory.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/pages/approvals/ApprovalDetailDrawer.tsx
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/connections/ConnectionCard.tsx
  • web/src/pages/connections/TunnelCard.tsx
  • web/src/pages/dashboard/ActivityFeedItem.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/pages/oauth-apps/OauthAppCard.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/pages/tasks/TaskColumn.tsx
  • web/src/pages/tasks/useTaskActionHandlers.ts
  • web/src/pages/workflow-editor/useWorkflowEditorCallbacks.ts
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/stores/approvals.ts
  • web/src/stores/artifacts.ts
  • web/src/stores/connections/crud-actions.ts
  • web/src/stores/meetings.ts
  • web/src/stores/messages.ts
  • web/src/stores/notifications.ts
  • web/src/stores/providers/crud-actions.ts
  • web/src/stores/setup-wizard/providers.ts
  • web/src/stores/setup-wizard/types.ts
  • web/src/stores/tasks.ts
  • web/src/styles/status-colors.ts
  • web/src/utils/errors.ts
  • web/src/utils/ws-sanitize.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). (7)
  • GitHub Check: Deploy Preview
  • GitHub Check: Build Web Assets (melange)
  • GitHub Check: CodSpeed Web benchmarks
  • GitHub Check: Lighthouse Site
  • GitHub Check: Lighthouse Dashboard
  • GitHub Check: Dashboard Test
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (12)
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.{ts,tsx}: Reuse components from web/src/components/ui/ before creating new ones. Never hardcode hex colors, font-family, pixel spacing, Motion transitions, or BCP 47 locale strings; use design tokens, @/lib/motion presets, and the helpers in @/utils/format. Enforced by scripts/check_web_design_system.py (PostToolUse hook on every web/src/ edit). See web/CLAUDE.md for the component inventory, token rules, and post-training references (TS6, Storybook 10).
Never hardcode BCP 47 locale strings or call bare .toLocaleString() / .toLocaleDateString() / .toLocaleTimeString(). Use helpers in @/utils/format which read getLocale() from @/utils/locale.
Currency: never hardcode ISO 4217 codes or symbols. Frontend: DEFAULT_CURRENCY from @/utils/currencies or useSettingsStore().currency. Field naming: no _usd suffix on money fields anywhere.
Timezone: store UTC; render via Intl without passing timeZone (browser tz wins). Date / number format: always via Intl; no hand-rolled templates. Units: metric only.

Always use createLogger from @/lib/logger with variable name log; never use bare console.warn/console.error/console.debug in application code (only logger.ts may use bare console methods)

Pass dynamic/untrusted values as separate arguments to logger methods, not interpolated into the message string, so they go through sanitizeArg

Wrap attacker-controlled fields inside structured objects with sanitizeForLog() before embedding in log calls

Callers MUST NOT wrap store mutation calls in try / catch; the store owns the error UX

Display counts must come from data.length; the wire envelope no longer carries total

Any new caller of health/readiness endpoints must handle the 503 path explicitly

Use sanitizeWsString() to clamp every WS-supplied string (strips C0 controls + bidi-overrides + caps length); use sanitizeWsEnum<T>(value, allowlist, fallback, { field }) for enum validation

Any new WS payload...

Files:

  • web/src/api/types/messages.ts
  • web/src/stores/setup-wizard/types.ts
  • web/src/hooks/use-empty-state-props.ts
  • web/src/components/ui/progress-gauge.tsx
  • web/src/__tests__/hooks/use-empty-state-props.test.ts
  • web/src/pages/connections/TunnelCard.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/components/ui/skeleton.tsx
  • web/src/mocks/handlers/index.ts
  • web/src/pages/BudgetPage.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/stores/tasks.ts
  • web/src/__tests__/styles/status-colors.test.ts
  • web/src/pages/ClientListPage.tsx
  • web/src/pages/agents/TaskHistory.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/components/ui/detail-nav-bar.stories.tsx
  • web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
  • web/src/__tests__/stores/artifacts.test.ts
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/styles/status-colors.ts
  • web/src/pages/ProvidersPage.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/components/layout/StatusBar.tsx
  • web/src/stores/connections/crud-actions.ts
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/DashboardPage.tsx
  • web/src/__tests__/pages/setup/ProvidersStep.test.tsx
  • web/src/pages/dashboard/ActivityFeedItem.tsx
  • web/src/pages/workflow-editor/useWorkflowEditorCallbacks.ts
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/mocks/handlers/helpers.ts
  • web/src/pages/BudgetForecastPage.tsx
  • web/src/pages/connections/ConnectionCard.tsx
  • web/src/api/types/approvals.ts
  • web/src/utils/errors.ts
  • web/src/pages/oauth-apps/OauthAppCard.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/stores/artifacts.ts
  • web/src/pages/TaskBoardPage.tsx
  • web/src/pages/approvals/ApprovalDetailDrawer.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/stores/providers/crud-actions.ts
  • web/src/pages/WorkflowsPage.tsx
  • web/src/__tests__/utils/ws-sanitize.test.ts
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/components/ui/error-boundary.tsx
  • web/src/stores/meetings.ts
  • web/src/stores/notifications.ts
  • web/src/pages/tasks/TaskColumn.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/__tests__/utils/errors.test.ts
  • web/src/stores/messages.ts
  • web/src/pages/tasks/useTaskActionHandlers.ts
  • web/src/stores/setup-wizard/providers.ts
  • web/src/api/types/enums.ts
  • web/src/__tests__/pages/BudgetPage.test.tsx
  • web/src/stores/approvals.ts
  • web/src/pages/ApprovalsPage.tsx
  • web/src/pages/RequestQueuePage.tsx
  • web/src/api/types/websocket.ts
  • web/src/pages/UsersPage.tsx
  • web/src/__tests__/components/ui/progress-gauge.test.tsx
  • web/src/pages/ConnectionsPage.tsx
  • web/src/__tests__/pages/DashboardPage.test.tsx
  • web/src/__tests__/stores/setup-wizard.test.ts
  • web/src/pages/EscalationQueuePage.tsx
web/**/*.{ts,tsx,js,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Web Dashboard: see web/CLAUDE.md for commands, design system, and component inventory. The CI-matching full-suite leak check is npm --prefix web run test -- --coverage --detect-async-leaks (the Leaks N leaks summary must stay at or below the MAX_ASYNC_LEAKS ceiling in .github/workflows/ci.yml; any new store that schedules timers must expose a teardown hook per web/CLAUDE.md).

Files:

  • web/src/api/types/messages.ts
  • web/src/stores/setup-wizard/types.ts
  • web/src/hooks/use-empty-state-props.ts
  • web/src/components/ui/progress-gauge.tsx
  • web/src/__tests__/hooks/use-empty-state-props.test.ts
  • web/src/pages/connections/TunnelCard.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/components/ui/skeleton.tsx
  • web/src/mocks/handlers/index.ts
  • web/src/pages/BudgetPage.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/stores/tasks.ts
  • web/src/__tests__/styles/status-colors.test.ts
  • web/src/pages/ClientListPage.tsx
  • web/src/pages/agents/TaskHistory.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/components/ui/detail-nav-bar.stories.tsx
  • web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
  • web/src/__tests__/stores/artifacts.test.ts
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/styles/status-colors.ts
  • web/src/pages/ProvidersPage.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/components/layout/StatusBar.tsx
  • web/src/stores/connections/crud-actions.ts
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/DashboardPage.tsx
  • web/src/__tests__/pages/setup/ProvidersStep.test.tsx
  • web/src/pages/dashboard/ActivityFeedItem.tsx
  • web/src/pages/workflow-editor/useWorkflowEditorCallbacks.ts
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/mocks/handlers/helpers.ts
  • web/src/pages/BudgetForecastPage.tsx
  • web/src/pages/connections/ConnectionCard.tsx
  • web/src/api/types/approvals.ts
  • web/src/utils/errors.ts
  • web/src/pages/oauth-apps/OauthAppCard.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/stores/artifacts.ts
  • web/src/pages/TaskBoardPage.tsx
  • web/src/pages/approvals/ApprovalDetailDrawer.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/stores/providers/crud-actions.ts
  • web/src/pages/WorkflowsPage.tsx
  • web/src/__tests__/utils/ws-sanitize.test.ts
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/components/ui/error-boundary.tsx
  • web/src/stores/meetings.ts
  • web/src/stores/notifications.ts
  • web/src/pages/tasks/TaskColumn.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/__tests__/utils/errors.test.ts
  • web/src/stores/messages.ts
  • web/src/pages/tasks/useTaskActionHandlers.ts
  • web/src/stores/setup-wizard/providers.ts
  • web/src/api/types/enums.ts
  • web/src/__tests__/pages/BudgetPage.test.tsx
  • web/src/stores/approvals.ts
  • web/src/pages/ApprovalsPage.tsx
  • web/src/pages/RequestQueuePage.tsx
  • web/src/api/types/websocket.ts
  • web/src/pages/UsersPage.tsx
  • web/src/__tests__/components/ui/progress-gauge.test.tsx
  • web/src/pages/ConnectionsPage.tsx
  • web/src/__tests__/pages/DashboardPage.test.tsx
  • web/src/__tests__/stores/setup-wizard.test.ts
  • web/src/pages/EscalationQueuePage.tsx
web/src/api/**/*.ts

📄 CodeRabbit inference engine (web/CLAUDE.md)

getLiveness() is always 200 while the process is alive; getReadiness() is 200 healthy / 503 unavailable (binary outcome, no tri-state)

Files:

  • web/src/api/types/messages.ts
  • web/src/api/types/approvals.ts
  • web/src/api/types/enums.ts
  • web/src/api/types/websocket.ts
web/src/stores/**/*.ts

📄 CodeRabbit inference engine (web/CLAUDE.md)

All store mutation actions (create / update / delete) must follow the stores/connections/crud-actions.ts pattern: wrap API calls in try / catch, update state + emit success toast on success, log + emit error toast + return sentinel on failure

Optimistic mutations must capture previous state synchronously and restore it in the catch block

List reads (fetch*) must set error: string | null on the store instead of toasting

Cursor pagination: list endpoints use opaque cursor-based paging via PaginationMeta; stores must keep nextCursor + hasMore in state and early-return when !hasMore || !nextCursor

Any new store that schedules timers or attaches event listeners must expose an equivalent cleanup hook and register it in the global test afterEach

Files:

  • web/src/stores/setup-wizard/types.ts
  • web/src/stores/tasks.ts
  • web/src/stores/connections/crud-actions.ts
  • web/src/stores/artifacts.ts
  • web/src/stores/providers/crud-actions.ts
  • web/src/stores/meetings.ts
  • web/src/stores/notifications.ts
  • web/src/stores/messages.ts
  • web/src/stores/setup-wizard/providers.ts
  • web/src/stores/approvals.ts
web/src/components/ui/**/*.tsx

📄 CodeRabbit inference engine (web/CLAUDE.md)

Use Base UI's render prop as the polymorphism primitive throughout the dashboard; only the local <Slot> helper in components/ui/slot.tsx should use @base-ui/react/merge-props for <Button asChild> ergonomics

Base UI primitives are imported directly from @base-ui/react/<subpath> and use the native render prop for polymorphism; the local <Slot> helper is reserved for <Button asChild>

Props interface name must be <ComponentName>Props and exported from the same file (e.g., AgentCardProps in agent-card.tsx) for greppability and extensibility

Every shared UI component must have a sibling <ComponentName>.stories.tsx covering every meaningful state (default, hover, loading, error, empty, disabled where applicable)

Files:

  • web/src/components/ui/progress-gauge.tsx
  • web/src/components/ui/skeleton.tsx
  • web/src/components/ui/detail-nav-bar.stories.tsx
  • web/src/components/ui/error-boundary.tsx
web/src/components/**/*.tsx

📄 CodeRabbit inference engine (web/CLAUDE.md)

ALWAYS reuse existing components from web/src/components/ui/ before creating new ones

Files:

  • web/src/components/ui/progress-gauge.tsx
  • web/src/components/ui/skeleton.tsx
  • web/src/components/ui/detail-nav-bar.stories.tsx
  • web/src/components/layout/StatusBar.tsx
  • web/src/components/ui/error-boundary.tsx
web/src/components/ui/**/*.{tsx,stories.tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Every new shared component lives in web/src/components/ui/ with a sibling .stories.tsx covering all states

Files:

  • web/src/components/ui/progress-gauge.tsx
  • web/src/components/ui/skeleton.tsx
  • web/src/components/ui/detail-nav-bar.stories.tsx
  • web/src/components/ui/error-boundary.tsx
web/src/**/*.tsx

📄 CodeRabbit inference engine (web/CLAUDE.md)

Use <StatusBadge> for status dots (defaults to role="img" with aria-label; use decorative for adjacent-labeled, announce for live WS updates)

Use <MetricCard> / <Sparkline> / <ProgressGauge> / <TokenUsageBar> for KPI displays

Use <SectionCard> for titled card wrappers with icon and action slot; <AgentCard>, <DeptHealthBar> for domain-specific cards

Use <InputField> / <SelectField> / <SliderField> / <ToggleField> / <SegmentedControl> / <TagInput> / <SearchInput> for form fields

Use <Drawer width="compact|narrow|default|wide"> for slide-in panels (Base UI; do NOT add inline w-[40vw] overrides)

Use <Skeleton> family / <EmptyState> / <ErrorBoundary> / <ErrorBanner> / <ProgressIndicator> for loading / empty / error states

Use STATUS_COLORS family from @/styles/status-colors (typed Record<EnumValue, string> lookups; no inline constants per page)

Use <ConfirmDialog> / <Toast> (Zustand-backed queue, NOT Base UI's Toast) for confirmation dialogs and toast notifications

Use <CommandPalette> / <KeyboardShortcutHint> / <CommandCheatsheet> for Cmd+K / shortcuts

Use <AnimatedPresence> / <StaggerGroup> / <LiveRegion> (debounced ARIA live for WS updates) for animations

Use Dialog, AlertDialog, Popover, Tabs, Menu, Drawer, CSPProvider, merge-props directly from @base-ui/react/<subpath> (adopted primitives)

Do NOT use Base UI's Toast (use Zustand-backed queue instead), Meter (use <ProgressGauge>), Select (use native <select> for mobile), Combobox / Autocomplete / OTP Field / Tooltip (no current call sites)

Files:

  • web/src/components/ui/progress-gauge.tsx
  • web/src/pages/connections/TunnelCard.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/components/ui/skeleton.tsx
  • web/src/pages/BudgetPage.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/pages/ClientListPage.tsx
  • web/src/pages/agents/TaskHistory.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/components/ui/detail-nav-bar.stories.tsx
  • web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/pages/ProvidersPage.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/components/layout/StatusBar.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/DashboardPage.tsx
  • web/src/__tests__/pages/setup/ProvidersStep.test.tsx
  • web/src/pages/dashboard/ActivityFeedItem.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/BudgetForecastPage.tsx
  • web/src/pages/connections/ConnectionCard.tsx
  • web/src/pages/oauth-apps/OauthAppCard.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/TaskBoardPage.tsx
  • web/src/pages/approvals/ApprovalDetailDrawer.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/WorkflowsPage.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/components/ui/error-boundary.tsx
  • web/src/pages/tasks/TaskColumn.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/__tests__/pages/BudgetPage.test.tsx
  • web/src/pages/ApprovalsPage.tsx
  • web/src/pages/RequestQueuePage.tsx
  • web/src/pages/UsersPage.tsx
  • web/src/__tests__/components/ui/progress-gauge.test.tsx
  • web/src/pages/ConnectionsPage.tsx
  • web/src/__tests__/pages/DashboardPage.test.tsx
  • web/src/pages/EscalationQueuePage.tsx
web/src/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Boot tests with onUnhandledRequest: 'error'; override per-case via server.use(...), never vi.mock('@/api/endpoints/*')

Files:

  • web/src/__tests__/hooks/use-empty-state-props.test.ts
  • web/src/__tests__/styles/status-colors.test.ts
  • web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
  • web/src/__tests__/stores/artifacts.test.ts
  • web/src/__tests__/pages/setup/ProvidersStep.test.tsx
  • web/src/__tests__/utils/ws-sanitize.test.ts
  • web/src/__tests__/utils/errors.test.ts
  • web/src/__tests__/pages/BudgetPage.test.tsx
  • web/src/__tests__/components/ui/progress-gauge.test.tsx
  • web/src/__tests__/pages/DashboardPage.test.tsx
  • web/src/__tests__/stores/setup-wizard.test.ts
web/src/pages/**/*.tsx

📄 CodeRabbit inference engine (web/CLAUDE.md)

Use <ListHeader> / <SearchFilterSort> / <Pagination> / <BulkActionBar> / <MetadataGrid> / <Breadcrumbs> for list-page primitives

Root container on list pages uses space-y-section-gap (majority pattern; flex flex-col gap-section-gap discouraged); <ErrorBanner> lands immediately after <ListHeader>, before filter/pagination rows

Pages with a one-line mission statement pass it via <ListHeader description="..." />

For list layout: use Kanban grouping for status-flow domains where each column conveys lifecycle phase (Tasks, Requests); use flat scrollable list for queues without explicit phase semantics (Escalations, Approvals)

Use useEmptyStateProps({ filteredCount, totalCount, filterActive, empty, filtered }) from @/hooks/use-empty-state-props to return EmptyStateProps | null for conditional rendering

Files:

  • web/src/pages/connections/TunnelCard.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/pages/BudgetPage.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/pages/ClientListPage.tsx
  • web/src/pages/agents/TaskHistory.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/pages/ProvidersPage.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/DashboardPage.tsx
  • web/src/pages/dashboard/ActivityFeedItem.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/BudgetForecastPage.tsx
  • web/src/pages/connections/ConnectionCard.tsx
  • web/src/pages/oauth-apps/OauthAppCard.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/TaskBoardPage.tsx
  • web/src/pages/approvals/ApprovalDetailDrawer.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/WorkflowsPage.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/pages/tasks/TaskColumn.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/pages/ApprovalsPage.tsx
  • web/src/pages/RequestQueuePage.tsx
  • web/src/pages/UsersPage.tsx
  • web/src/pages/ConnectionsPage.tsx
  • web/src/pages/EscalationQueuePage.tsx
web/src/mocks/handlers/**/*.ts

📄 CodeRabbit inference engine (web/CLAUDE.md)

MSW handlers in web/src/mocks/handlers/ must mirror web/src/api/endpoints/*.ts 1:1 with a default happy-path handler for every exported endpoint

Use typed envelope helpers (successFor, paginatedFor, voidSuccess) in MSW handlers to keep handlers in lockstep with endpoint return types

Files:

  • web/src/mocks/handlers/index.ts
  • web/src/mocks/handlers/helpers.ts
docs/**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Use D2 (\``d2) for architecture diagrams, nested container layouts, complex entity relationships. Use Mermaid (```mermaid) for flowcharts, sequence diagrams, simple hierarchies, pipelines. Use Markdown tables for grid/matrix data that is semantically tabular. Never use ```textblocks with ASCII/Unicode box-drawing characters for diagrams. D2 uses theme 200 (Dark Mauve), configured globally inmkdocs.yml. Review agent diagram-syntax-validatorruns in/pre-pr-reviewand/aurelio-review-pr` pipelines.

Files:

  • docs/reference/web-package-structure.md
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T12:34:54.548Z
Learning: CI fails if `vitest --detect-async-leaks` reports more than `MAX_ASYNC_LEAKS` (currently 90); local floor 49, CI baseline 77-80; raise ceiling only with documented per-PR justification
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T12:34:54.548Z
Learning: Bump the WS protocol version on both client and server together for breaking payload changes

Comment thread web/src/__tests__/hooks/use-empty-state-props.test.ts
Comment on lines +84 to +146
it('does not toast or set error when create succeeds and refresh succeeds', async () => {
server.use(
http.post('/api/v1/providers/from-preset', () =>
HttpResponse.json(apiSuccess(defaultProvider('local-x')), { status: 201 }),
),
http.get('/api/v1/providers', () =>
HttpResponse.json(
apiSuccess({ data: [{ ...defaultProvider('local-x'), name: 'local-x' }] }),
),
),
)

render(<ProvidersStep />)

// Wait for the preset picker to show the detected local row, then
// trigger the auto-add via the store call (simulates the
// PresetPickerSections onAddLocal handler the picker would call).
await waitFor(() => {
expect(useSetupWizardStore.getState().presets.length).toBeGreaterThan(0)
})
const result = await useSetupWizardStore
.getState()
.createProviderFromPreset('local-x', 'local-x', undefined, 'http://localhost:11434')

expect(result).toEqual({ ok: true })
expect(useToastStore.getState().toasts).toHaveLength(0)
expect(useSetupWizardStore.getState().providersError).toBeNull()
})

it('toasts a warning and leaves providersError null when refresh fails after a successful create', async () => {
server.use(
http.post('/api/v1/providers/from-preset', () =>
HttpResponse.json(apiSuccess(defaultProvider('local-x')), { status: 201 }),
),
// fetchProviders is the listProviders endpoint -- make it fail
http.get('/api/v1/providers', () => HttpResponse.json(apiError('list boom'))),
)

render(<ProvidersStep />)
await waitFor(() => {
expect(useSetupWizardStore.getState().presets.length).toBeGreaterThan(0)
})

// Drive handleAddLocal directly via the store + caller's wrapper
// pattern: create succeeds, then fetchProviders is called and
// fails. The current ProvidersStep handler swallows the
// fetchProviders error into a toast so the create's error banner
// stays clean.
const createResult = await useSetupWizardStore
.getState()
.createProviderFromPreset('local-x', 'local-x', undefined, 'http://localhost:11434')
expect(createResult).toEqual({ ok: true })
// Now trigger fetchProviders which is what handleAddLocal
// would invoke after a successful create.
await useSetupWizardStore.getState().fetchProviders()

// The store's fetchProviders sets providersError on failure;
// ProvidersStep's handleAddLocal would also toast in addition --
// both behaviours are valid recovery surfaces. We assert at least
// one of them carries the failure so the operator is informed.
const state = useSetupWizardStore.getState()
expect(state.providersError).toMatch(/list boom/i)
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

These cases bypass handleAddLocal, so they don't validate the new UI recovery flow.

They call store methods directly instead of triggering the add-local action the component wires up. As a result, the post-create refresh/toast branch is never exercised, and the refresh-failure case currently locks in providersError rather than the intended warning-only surface.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/pages/setup/ProvidersStep.test.tsx` around lines 84 - 146,
Tests call store methods directly (createProviderFromPreset/fetchProviders) and
thus bypass ProvidersStep.handleAddLocal, so the component's
toast-on-refresh-failure path isn't exercised; modify the tests to drive the
component UI instead of the store: render ProvidersStep, locate and trigger the
UI control that invokes handleAddLocal (e.g., the PresetPicker add button or the
same event the PresetPickerSections would call), and then assert that a toast
warning is produced while providersError remains null; keep references to
ProvidersStep, handleAddLocal, createProviderFromPreset, fetchProviders, and
useSetupWizardStore to guide locating the affected logic.

Comment thread web/src/__tests__/utils/ws-sanitize.test.ts
Comment thread web/src/components/ui/detail-nav-bar.stories.tsx
Comment on lines +51 to +65
export const PositionHidden: Story = {
args: {
canPrev: true,
canNext: true,
position: null,
},
parameters: {
docs: {
description: {
story:
'When `position` is `null` (deep link / refresh without list context) the bar renders nothing. The component returns `null` and binds no keyboard listeners.',
},
},
},
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add play() verification for keyboard shortcut + PositionHidden listener behavior.

The stories describe (a) window-level J/ArrowLeft/K/ArrowRight shortcuts and (b) that PositionHidden returns null and binds no keyboard listeners, but they don’t actually verify those behaviors via play() (or interaction tests). Consider adding lightweight Storybook play() steps that:

  • dispatch keyboard events on window,
  • assert onPrev/onNext calls when shortcuts should work,
  • assert no calls when position: null and/or when shortcuts are disabled,
  • optionally ensure events don’t fire when target is input/contenteditable.

Also applies to: 76-91

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/detail-nav-bar.stories.tsx` around lines 51 - 65, Add
Storybook play() interaction tests for the navigation stories: in the
PositionHidden story (and the other affected stories around lines 76-91)
implement a play() that dispatches keyboard events (e.g., 'j', 'k', 'ArrowLeft',
'ArrowRight') on window and asserts the mock handlers (onPrev/onNext) are called
when position is set and not called when position is null or when shortcuts are
disabled; also dispatch events with document.activeElement as an
input/contenteditable to assert handlers are not invoked. Locate the
PositionHidden Story object and the other Story exports, add a play() that uses
window.dispatchEvent(new KeyboardEvent(...)) and jest/expect-style assertions
against the passed-in onPrev/onNext mocks to verify listener binding and
non-binding behavior.

Comment thread web/src/stores/approvals.ts
Comment thread web/src/stores/meetings.ts
Comment thread web/src/stores/messages.ts Outdated
Comment thread web/src/stores/tasks.ts
Comment thread web/src/utils/errors.ts
Aureliolo added 7 commits May 2, 2026 15:31
… shared status colours (#1710)

Wave 1 of the audit-cleanup E bundle. Adds the cross-cutting primitives the
remaining waves build on.

* Typed WS sanitizer in web/src/utils/ws-sanitize.ts:
  sanitizeWsEnum(value, allowlist, fallback, opts) validates a WS-sourced
  string against an enum allowlist. On unknown values (sanitisation strips to
  empty, or server emits a value not yet known to this client), emits a
  structured ws.enum.unknown warning and returns the supplied fallback so
  rolling backend deploys cannot break UI rendering. Replaces 13 unsafe
  cast sites in approvals, meetings, messages, and tasks stores. Test file:
  14 cases covering valid pass-through, control-char strip, bidi-override
  strip, allowlist miss, empty/whitespace fallback, custom maxLen, and
  literal-type inference.

* Typed WS payload contract in web/src/api/types/websocket.ts:
  WsEventPayloadMap discriminates payload shape by event_type for all 62
  WsEventType members; WsTypedEvent and WsEventOf tie envelope to payload.
  Mirror of src/synthorg/api/ws_payloads/_lifecycle.py and _domain.py.
  Compile-time exhaustiveness guard catches drift if either union changes
  without the other. The dispatch loop continues to deliver the existing
  WsEvent so per-store migrations to the typed view roll out incrementally;
  backend migration #1711 has a stable target.

* Shared badge / pill colour module web/src/styles/status-colors.ts:
  ROLE_BADGE_COLORS and ESCALATION_STATUS_BADGE_COLORS replace inline
  ROLE_PILL_CLASS and STATUS_BADGE_CLASS constants in UsersPage and
  EscalationQueuePage. Approval-domain helpers in @/utils/approvals stay
  unchanged (already centralised via SemanticColor abstraction). Runtime
  test pins enum coverage and forbids hex literals.

* Adds missing _VALUES tuples: ATTACHMENT_TYPE_VALUES (messages.ts),
  SIGNATURE_ALGORITHM_VALUES (approvals.ts), TASK_SOURCE_VALUES (enums.ts).

Resolves the WS enum-cast pattern subset of audit agent 109 (highest-leverage
piece of 139 TS strictness findings) and audit agent 52 finding #4.
Wave 2 of the audit-cleanup E bundle.

A11y (audit agent 35):
* progress-gauge.tsx -- adds SVG title element so screen readers that
  expose SVG content announce the meter context. Wrapping div already
  carries role=meter + aria-label; the title is redundant but robust.
* error-boundary.tsx -- replaces the bare button at the component level
  with the design-system Button (variant=link, size=sm). Inherits
  focus-visible ring + WCAG-AA contrast + hover state from the system.
* StatusBar.tsx -- documents the implicit color-plus-label pairing
  contract on resolveCombinedStatus. The existing render at the
  health-pill site already pairs visible label text with the colored
  Dot via aria-label and aria-live.

Responsive (audit agent 53):
* AgentSpendingTable.tsx -- min-w-[40rem] becomes md:min-w-[40rem] so
  the 768-1024px tablet range falls back to natural width within the
  existing overflow-x-auto wrapper.
* SettingsPage.tsx -- SettingsActionCard's right column moves from a
  hard w-56 to w-full max-w-[14rem] shrink-0 md:w-56 so the grid
  responds to container shrinkage.

Perf (audit agent 54):
* TaskHistory.tsx -- wraps sorted and maxDurationMs in useMemo so a
  parent re-render with the same tasks reference skips Date parsing
  and the sort/reduce passes.
* ApprovalCard, ChannelListItem, ActivityFeedItem -- React.memo
  wrappers. ApprovalCard runs flash + countdown effects per row in
  long lists; ChannelListItem renders inside a sidebar that updates
  on activeChannel + unreadCounts; ActivityFeedItem re-renders the
  whole feed on every WS event.
* BudgetPage / BudgetForecastPage / DashboardPage -- recharts-using
  charts (SpendBurnChart, CostBreakdownChart, BudgetBurnChart) become
  React.lazy imports wrapped in Suspense with a new SkeletonChart
  fallback. Vite splits a separate chunk for recharts; first chart
  render triggers the import.
* skeleton.tsx -- adds SkeletonChart variant sized via aspect-ratio
  so the layout does not shift when ResponsiveContainer measures the
  parent post-hydration.

Test infrastructure adjustments:
* progress-gauge tests -- the new SVG title duplicates the visible
  percentage text in the no-label case; tests switch from getByText
  to getAllByText with a length-greater-than guard to be agnostic
  to the additional title node.
* BudgetPage / DashboardPage tests -- chart section titles render only
  after Suspense resolves the lazy import; the four affected tests
  switch to async findByText with a 5s timeout for parallel-load
  resilience.
Wave 3 of the audit-cleanup E bundle.

Storybook (audit agent 57):
* detail-nav-bar.stories.tsx -- new file. Seven stories cover the
  prop combinations the audit called out: Default, FirstItem,
  LastItem, PositionHidden (component returns null), WithLabel,
  ShortcutsBound, BothDisabled. tags: [autodocs] per the breadcrumbs
  story convention.

UX consistency (audit agent 52):
* useEmptyStateProps hook (web/src/hooks/use-empty-state-props.ts) --
  derives the EmptyStateProps for the two branches every list page
  juggles ("no data ever" vs "no data after filter") so the
  discriminator is a single hook call instead of two parallel
  conditional renders. Wired into ApprovalsPage and
  EscalationQueuePage.
* ListHeader description prop applied to WorkflowsPage,
  ConnectionsPage, BudgetPage, ProvidersPage, EscalationQueuePage,
  UsersPage so each list page's mission statement is one line under
  the title.
* ProvidersPage / ConnectionsPage / UsersPage root container --
  flex flex-col gap-section-gap becomes space-y-section-gap to
  match the majority pattern across the dashboard.
* ConnectionsPage error banner -- moved to immediately after
  ListHeader (was after TunnelCard) so the page-level error slot
  matches OauthAppsPage.
* UsersPage layout -- list becomes a 1/2/3-column grid mirroring
  ClientListPage's resource-grid shape; per-card header keeps the
  Grant role mutation prominent.

CLAUDE.md (web/) updates:
* Documents the list-page primitive conventions: space-y-section-gap
  root, ErrorBanner-after-ListHeader, ListHeader description for
  pages with a one-line mission statement, Kanban vs flat-list
  layout choice based on whether the domain has explicit lifecycle
  phases.
* Documents useEmptyStateProps as the canonical empty-state
  derivation surface.
* Documents the @/styles/status-colors module as the canonical
  badge / role / risk / urgency lookup surface (no inline
  Record<EnumValue, string> per page).
Final wave of the audit-cleanup E bundle.

Dashboard UX (audit agent 47, 9 findings):
* WorkflowsPage -- Clear filters action now re-focuses the search
  input via a forwarded ref, so the user can immediately re-filter
  without a second click.
* EscalationQueuePage -- new Sort by SegmentedControl (Newest /
  Priority / Conflict type) layered on top of the status + priority
  filters; sort runs client-side on the existing memoised list.
* ProvidersPage -- preset picker already implements progressive
  disclosure via the More providers via LiteLLM details collapse;
  the audit's "if presets exceed 12" concern is addressed by the
  server-driven is_featured flag.
* RequestQueuePage -- ListHeader description documents the
  Submitted -> Triaging -> Scoping -> Approved -> Task created
  pipeline. A generic "Move to arbitrary status" menu would require
  backend support for unrestricted transitions, which the request
  state machine deliberately does not provide -- the per-card
  scope/approve/reject buttons are the legal next-transition surface.
* WebhookReceiptsPage / ConnectionsPage -- ConnectionCard renders a
  View webhook receipts cross-link that pre-selects the connection
  via the new ?connection= query param the receipts page reads on
  mount.
* ClientListPage -- new SearchInput filters clients by name or
  persona client-side; empty-state copy distinguishes "no clients"
  from "no matching clients".
* ApprovalsPage -- the four risk-count metric cards become buttons
  that toggle the matching risk_level filter (aria-pressed exposes
  the toggle state, ring-2 ring-accent surfaces the active card).
* TaskBoardPage -- the column containing the currently-selected task
  gets a subtle bg-accent/5 + ring highlight via TaskColumn's new
  highlighted prop, so the detail-drawer-open task is visually
  mapped back to its lifecycle phase on the board.
* SettingsPage -- when advancedMode is on, a small Advanced badge
  renders next to the namespace tabs in addition to the existing
  AdvancedModeBanner so the mode signal stays visible after dismiss.

Onboarding (audit agent 59, 7 findings):
* createProviderFromPreset returns a result-object
  ({ ok: true, warning? } | { ok: false, error }) instead of
  throwing. ProvidersStep branches on ok before calling
  fetchProviders, and a fetch failure now toasts (warning) instead
  of poisoning the step's error banner.
* SetupAgentCard tracks the in-flight name save and disables the
  adjacent Randomize button so a manual edit cannot race a randomize
  click and have the slower request win.
* CompanyStep clears companyError + companyErrorCode on unmount so a
  stale tier-coverage error from a prior Apply does not persist when
  the user returns from Providers.
* CompleteStep's ErrorBanner appends a contextual recovery hint
  ("If you see this repeatedly, setup may have already completed;
  refresh the page to confirm.") so fatal errors offer a path
  forward instead of a Retry-only loop.
* SkipWizardForm distinguishes "company create failed" from "company
  created, setup completion failed" and surfaces the partial-success
  recovery message with the option to finish via the wizard's
  Complete step.
* WizardNavigation accepts a nextDisabledReason caption rendered
  under the disabled Next button so the user knows WHY they cannot
  advance ("Complete the required fields on this step to continue.").
* CompanyStep Company Name InputField gains a hint
  ("Max 200 characters. Apply Template stays disabled until this is
  valid.") that sets expectations up front; the existing error fires
  only on boundary breach so the two never co-render.

Error messages (audit agent 58, 11 frontend findings):
* utils/errors.ts 409 message softened to "The resource state
  changed. Refresh the page and try again." -- covers
  optimistic-concurrency, duplicate-resource, and version-mismatch.
* artifacts.ts partial-load error restructured: title + actionable
  next step, with the raw backend message routed through
  getErrorMessage so JSON / stack traces cannot leak.
* setup-wizard/providers.ts model-discovery messages re-routed
  through getErrorMessage and re-shaped as title + description; both
  the no-models-found and discovery-failed branches surface as
  warnings rather than errors (provider WAS created).
* useTaskActionHandlers cancellation reason: "Cancellation requires
  a reason for audit. Please enter why you are cancelling this task."
* ApprovalDetailDrawer + ApprovalsPage rejection reason: "Rejection
  requires a reason for the approval record. Provide a brief
  explanation."
* OauthAppCard reveal/copy toasts re-titled "Failed to reveal client
  secret" / "Failed to copy client ID" so the operator knows what
  the operation was without reading the description.
* useWorkflowEditorCallbacks validation-warning message uses past
  tense + actionable phrasing: "Validation found N error(s).
  Review the marked fields below."
* TunnelCard clipboard error discriminates NotAllowedError
  (permission denied -- "Use Ctrl+C to copy manually") from
  other failures (API not available -- "Copy manually from the
  Public URL field").

Test infrastructure: ApprovalDetailDrawer test updated to match the
new toast title.
Two unit tests asserted on the literal pre-#1710 wording of the 409
HTTP message and the artifacts partial-load detail message. The
copy was reworded in wave 4 to soften 409 (no longer assumes
optimistic-concurrency) and to restructure the artifacts message as
title + recovery hint. The tests now assert on the new wording's
stable substring.
…, error-code routing (#1710)

19 findings from 9 review agents, all addressed.

CRITICAL (silent-failure-hunter):
* setup-wizard providers store -- new providersWarning slot replaces
  the misuse of providersError for "provider created OK but discovery
  empty/failed" cases. ProvidersStep renders a separate warning
  banner for the new slot so a successfully-created provider is no
  longer surfaced as an error.
* SkipWizardForm -- partial-success discrimination now reads the
  store snapshot (companyResponse !== null) instead of a local flag
  set between awaits. The local flag could race a throw between
  resolution and assignment and produce the wrong recovery message.
* ProvidersStep handleAddLocal -- fetchProviders failure now logs
  via createLogger before toasting, so a dismissed toast leaves an
  observability trace for the operator.

MAJOR:
* setup-wizard.test.ts -- four new tests pin createProviderFromPreset
  return shape: { ok: true } on full success, { ok: true, warning }
  on no-models / discovery-throw, { ok: false, error } on create
  failure. A revert to throw-on-failure would now break the test.
* New ProvidersStep.test.tsx -- four integration tests cover the
  refresh-success, refresh-failure (warning toast + clean error
  banner), create-failure (error banner with title), and
  discovery-empty (separate warning banner) paths.
* WorkflowFilters comment -- removed the (see audit 47#1) callout
  in favour of the technical WHY (refocus the search input after
  clearing filters).
* web/CLAUDE.md -- WS sanitization paragraph updated to cite
  the actual source module @/utils/ws-sanitize, document
  sanitizeWsEnum<T> with the call-shape contract, and note that
  raw "as EnumType" casts are forbidden.
* docs/reference/web-package-structure.md -- hooks inventory now
  lists useEmptyStateProps; styles inventory now lists the typed
  status-colour lookups.
* mocks/handlers/_helpers.ts -- new buildValidationError(fields[],
  overrides?) helper consolidates the inconsistent
  Missing required fields / Field 'name' is required / Fields
  'a' and 'b' are required mock-error wording into one
  Validation error: a, b are required. shape.
* ApprovalDetailDrawer -- reason field now renders an inline error
  (red border + helper text) on validation failure, in addition
  to the toast. Inline error clears as soon as the user types.
* utils/errors.ts -- new getCrudErrorTitle(err, fallback) routes
  on the structured error_category (auth / validation / conflict /
  rate_limit / not_found / budget_exhausted) and HTTP-status
  fallbacks to pick a category-aware toast title prefix. Wired
  into stores/connections/crud-actions.ts and
  stores/providers/crud-actions.ts so generic
  "Failed to create connection" titles become "Authentication
  failed" / "Resource conflict" / etc when the backend supplies
  a structured error code.

MEDIUM:
* sanitizeWsEnum -- the field option is now mandatory (no longer
  defaults to 'unknown'), so the ws.enum.unknown warning always
  carries enough diagnostic context to identify which field
  drifted. All 13 existing call sites already pass field; no caller
  edits required.
* New use-empty-state-props.test.ts -- five tests cover the three
  branches (filteredCount > 0 returns null, totalCount > 0 with
  filterActive returns filtered, empty otherwise) plus memoisation
  identity-stability with stable inputs.
* ws-sanitize surrogate-pair test -- tightened to pin exact content
  (toBe instead of length-only) so a mid-codepoint truncation that
  drops a surrogate half cannot pass silently.
* TunnelCard clipboard -- DOMException routing now handles
  NotAllowedError, SecurityError, InvalidStateError, AbortError,
  NotFoundError each with a specific recovery hint, instead of
  bucketing every non-permission failure into "API not available".
* CompanyStep cleanup useEffect -- guards against racing an
  in-flight submit (companyLoading === true) so a long-running
  request that errors after unmount doesn't get its own error
  nulled out.
* api/types/websocket.ts -- typed payload contract now carries an
  explicit sanitisation contract comment: every string field is
  attacker-reachable; consumers must route through sanitizeWsString
  or sanitizeWsEnum before display or persistence.
* BudgetPage / DashboardPage tests -- 5s findByText timeout now
  documented as a parallel-load band-aid; if these tests start
  consistently approaching 5s, treat as a performance regression
  in the lazy-import strategy rather than raising the timeout.

Verification:
* npm run lint: zero warnings
* npm run type-check: clean
* 2997 unit tests pass (was 2983; +14 new)
* 75 async leaks (under MAX_ASYNC_LEAKS=90 ceiling)
* npm run storybook:build: clean
* npm run build: recharts splits into vendor-charts chunk (~378 KB)
…1710)

External reviewers (CodeRabbit, gemini-code-assist) plus the CI Dashboard Test
async-leak gate flagged 18 findings on the first push of #1718. All addressed.

CI gate fix:
* The pre-PR-review push added 14 new tests; CI's leak detector reported
  95 leaks (above the MAX_ASYNC_LEAKS=90 ceiling) because of extra
  ProvidersStep renders. Trimmed ProvidersStep.test.tsx from 4 renders to
  1: the unique-value-add is the warning-banner DOM check; the rest of the
  store-level coverage already lives in setup-wizard.test.ts.

Forward-compat WS sanitizer (3 reviews flagged the same root cause):
* approvals / meetings / tasks shape guards were rejecting unknown enum
  values BEFORE sanitizeWsEnum could apply its allowlist + safe fallback.
  A backend rolling out a new enum value would drop the whole frame
  instead of the documented warning + fallback. Relaxed isApprovalShape /
  isMeetingShape / isTaskShape to typeof === 'string' for the enum fields
  routed through sanitizeWsEnum (status, risk_level, urgency_level,
  protocol_type, type, priority, source). Removed now-unused
  per-store *_SET constants. Test "rejects frames whose status is
  outside the enum" updated to assert the new accept-and-fallback
  contract.

ProvidersStep recovery flow (CodeRabbit MAJOR x2):
* fetchProviders swallows its own errors via the providers store, so the
  try/catch around it was dead code -- a refresh failure after a
  successful create still surfaced the hard "Failed to load providers"
  banner. Rewired to read providersError from the store post-call, clear
  it, and toast a warning instead. The successfully-created provider
  state is no longer misrepresented as a failed create.

ApprovalDetailDrawer reasonError reset:
* reasonError was cleared on dialog close but not when the approval
  context resets or the pending state flips, leaving stale inline
  validation between approval switches. Cleared in both reset paths.

Random-name race (SetupAgentCard):
* onRandomizeName was fire-and-forget; rapid clicks enqueued concurrent
  writes with last-response-wins. Added randomizeSaving guard and
  awaited the call so the button stays disabled across the in-flight
  window.

Smaller findings:
* useEmptyStateProps test totalCount-greater-than-zero actually used
  totalCount: 0; set to 5 so the test exercises the named branch.
* ApprovalsPage useEmptyStateProps: filteredCount now uses
  filtered.length (post-filter item count) instead of grouped.size
  (risk-bucket count, max 4 -- semantically wrong).
* ws-sanitize control-char test: input now embeds U+0001 so the
  strip-then-validate flow is actually exercised.
* detail-nav-bar.stories: clarified that bindShortcuts=false in meta
  is a stories-context override, not the component default.
* TunnelCard clipboard NotAllowedError hint: "Ctrl+C" -> "Ctrl/Cmd+C"
  for cross-platform correctness.
* EscalationQueuePage: statusFilter is server-side, so
  escalations.length is post-filter; useEmptyStateProps would resolve
  to the empty branch even when a filter is active and returns zero.
  Wrapped the result so the filtered copy fires when filterActive +
  zero rows.
* SettingsPage Advanced badge: dropped redundant aria-label, the
  visible "Advanced" text is the accessible name.
* ClientListPage: routed the inline filtered-vs-empty discriminator
  through useEmptyStateProps, removing the duplicate inline branching.
* WebhookReceiptsPage: useState(initialFromUrl) only seeded once;
  added an effect that re-targets selected when the connection query
  param changes mid-session (deferred via microtask per ESLint
  set-state-in-effect).
* approvals / meetings / messages / tasks: sanitizer imports moved
  from @/stores/notifications to @/utils/ws-sanitize -- removes the
  cross-store coupling and circular-init risk; the notifications
  store re-export stays for any external callers.
* getCrudErrorTitle: 401 (authentication) and 403 (authorization)
  now return distinct toast titles; the toast-title layer
  surfaces "Permission denied" for 403 and "Authentication failed"
  for 401, matching what the user actually needs to do.
@Aureliolo Aureliolo force-pushed the fix/audit-cleanup-e-frontend branch from 4bedae1 to 6c45c78 Compare May 2, 2026 13:32
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview May 2, 2026 13:34 — with GitHub Actions Inactive
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
web/src/pages/WebhookReceiptsPage.tsx (1)

186-204: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Move ErrorBanner above the filter row for list-page consistency

ErrorBanner is currently rendered after SearchFilterSort; list pages should place it directly after ListHeader.

Suggested fix
       <ListHeader title="Webhook receipts" count={entries.length} />
+
+      {error && (
+        <ErrorBanner
+          severity="error"
+          title="Could not load webhook activity"
+          description={error}
+          onRetry={() => void reload()}
+        />
+      )}

       <SearchFilterSort
         filters={
           <SelectField
             label="Connection"
             value={selected}
             onChange={setSelected}
             options={options}
           />
         }
       />
-
-      {error && (
-        <ErrorBanner
-          severity="error"
-          title="Could not load webhook activity"
-          description={error}
-          onRetry={() => void reload()}
-        />
-      )}

As per coding guidelines: "web/src/pages/**/*.tsx: ... <ErrorBanner> lands immediately after <ListHeader>, before any filter/pagination row".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/WebhookReceiptsPage.tsx` around lines 186 - 204, The
ErrorBanner is rendered after SearchFilterSort but per guidelines it must appear
immediately after ListHeader; move the <ErrorBanner ... /> JSX block so it sits
directly after the <ListHeader /> component and before the <SearchFilterSort />
block, preserving its props (severity, title, description={error}, onRetry={()
=> void reload()}) and conditional rendering (error && ...). Ensure
SearchFilterSort (with SelectField value={selected} onChange={setSelected}
options={options}) remains intact and that the error conditional still uses the
same reload handler.
web/src/pages/UsersPage.tsx (1)

116-136: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Move <ErrorBanner> directly below <ListHeader> in this list page.

Current order places filters before the banner, which breaks the shared list-page contract and can hide error context behind controls.

As per coding guidelines: "web/src/pages/**/*.tsx: <ErrorBanner> lands immediately after <ListHeader>, before any filter/pagination row."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/UsersPage.tsx` around lines 116 - 136, The ErrorBanner is
rendered after the filters, violating the list-page contract; move the
conditional ErrorBanner block so it appears immediately after the ListHeader
component (before the SearchFilterSort/SearchInput and any pagination/filter
rows) while retaining the existing conditional (error) check and the onRetry
handler (fetchUsers). Locate the ErrorBanner, ListHeader, SearchFilterSort, and
fetchUsers symbols in UsersPage and reposition the ErrorBanner JSX directly
below ListHeader, keeping its props (severity, title, description, onRetry)
unchanged and ensuring no layout or aria semantics are altered.
web/src/pages/approvals/ApprovalCard.tsx (1)

19-55: 🧹 Nitpick | 🔵 Trivial

Move countdown prop-sync out of render phase.

Lines 52–55 call setCountdown(...) during render, which triggers state updates outside useEffect. Move this into useEffect keyed by approval.seconds_remaining to avoid render-phase updates and unnecessary re-renders in this memoized component.

Proposed 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 19 - 55, The component
ApprovalCardImpl is calling setCountdown during render when
approval.seconds_remaining changes (using prevSecondsRef), which must be moved
into a useEffect to avoid render-phase state updates; replace the inline check
and setCountdown (the block that compares approval.seconds_remaining to
prevSecondsRef.current and calls setCountdown) with a useEffect that has
approval.seconds_remaining in its dependency array, updates
prevSecondsRef.current and calls setCountdown inside that effect, preserving the
existing prevSecondsRef, countdown state, and behavior.
web/src/pages/budget/AgentSpendingTable.tsx (2)

48-48: ⚠️ Potential issue | 🟠 Major

Pass locale to localeCompare() for consistent agent-name sorting.

Line 48 uses bare .localeCompare() which defaults to the browser locale instead of the user/company locale setting, causing inconsistent sorting across users. getLocale() is available from @/utils/locale and should be passed as the second argument.

Fix
-    case 'agentName': cmp = a.agentName.localeCompare(b.agentName); break
+    case 'agentName': cmp = a.agentName.localeCompare(b.agentName, getLocale()); break

Add the import at the top of the file:

 import { formatCurrency } from '@/utils/format'
+import { getLocale } from '@/utils/locale'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/budget/AgentSpendingTable.tsx` at line 48, The agent-name
comparison uses a bare a.agentName.localeCompare(b.agentName) which relies on
browser locale; import getLocale from '@/utils/locale' and change the comparison
in the AgentSpendingTable sort/case 'agentName' branch to
a.agentName.localeCompare(b.agentName, getLocale()) so sorting uses the app/user
locale consistently (add the import at the top and update the 'agentName' case).

99-99: ⚠️ Potential issue | 🟠 Major

Use Intl formatting instead of .toFixed() for the percentage.

The .toFixed(1) method doesn't respect locale-specific decimal separators (some locales use commas instead of periods), and manually appending % is a hand-rolled template. Since budgetPercent is already in the range 0–100, use Intl.NumberFormat directly without converting:

🔢 Proposed fix
-      <span className={cn(COLUMN_WIDTHS.budgetPercent, 'text-right font-mono text-xs text-text-secondary')}>
-        {row.budgetPercent.toFixed(1)}%
-      </span>
+      <span className={cn(COLUMN_WIDTHS.budgetPercent, 'text-right font-mono text-xs text-text-secondary')}>
+        {new Intl.NumberFormat(getLocale(), { 
+          minimumFractionDigits: 1, 
+          maximumFractionDigits: 1 
+        }).format(row.budgetPercent)}%
+      </span>

Add imports if not already present:

 import { formatCurrency } from '@/utils/format'
+import { getLocale } from '@/utils/locale'

As per coding guidelines: "Date / number format: always via Intl; no hand-rolled templates."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/budget/AgentSpendingTable.tsx` at line 99, Replace the
hand-rolled percentage rendering that uses row.budgetPercent.toFixed(1) + '%'
with an Intl.NumberFormat-based formatter; update the JSX in AgentSpendingTable
(where row.budgetPercent is rendered) to use new Intl.NumberFormat with style:
'percent' or number format for one decimal place and the current locale
(remember budgetPercent is already 0–100 so divide by 100 if using style:
'percent'), e.g., create a formatter once (or inline) and use
formatter.format(...) instead of .toFixed and string concatenation to ensure
locale-aware decimal separators and percent symbol placement.
web/src/stores/messages.ts (1)

179-180: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

type and priority still use unsafe as casts after sanitizeWsString.

These fields are enum-typed (MessageType, MessagePriority) but bypass sanitizeWsEnum, which means an unknown backend value would pass through verbatim instead of triggering the warning + fallback behavior. As per coding guidelines, raw sanitizeWsString(...) as EnumType casts are forbidden for WS-sourced enum fields.

Consider routing these through sanitizeWsEnum with corresponding MESSAGE_TYPE_VALUES / MESSAGE_PRIORITY_VALUES allowlists (similar to the attachment type fix), or relaxing the shape guard if you prefer to sanitize-then-fallback rather than reject.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/messages.ts` around lines 179 - 180, The code is unsafely
casting sanitized strings to enum types for Message['type'] and
Message['priority']; replace the raw sanitizeWsString(... ) as Message['type'] /
Message['priority'] with calls to sanitizeWsEnum using the appropriate
allowlists (MESSAGE_TYPE_VALUES and MESSAGE_PRIORITY_VALUES) so unknown backend
values trigger the warning+fallback behavior; update the object construction
that sets type and priority to use sanitizeWsEnum(value, MESSAGE_TYPE_VALUES,
'Message.type', DEFAULT_TYPE) and sanitizeWsEnum(value, MESSAGE_PRIORITY_VALUES,
'Message.priority', DEFAULT_PRIORITY) (or adjust the existing fallback
constants) so values are validated instead of being force-cast.
web/src/stores/tasks.ts (1)

298-307: 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

Inconsistent forward-compatibility: some enum fields still reject unknown values.

While status, priority, type, and source now use sanitizeWsEnum with fallbacks, these fields still validate against hardcoded allowlist sets:

  • estimated_complexityCOMPLEXITY_SET.has(...) (line 299)
  • task_structureTASK_STRUCTURE_SET.has(...) (line 302)
  • coordination_topologyCOORDINATION_TOPOLOGY_SET.has(...) (line 304)

A backend rollout adding a new complexity or topology value will cause the entire frame to be dropped, unlike status/priority which would fall back gracefully.

If these fields are intentionally "fixed" enums that the frontend must understand, add a comment explaining why they differ from the sanitized fields. Otherwise, consider routing them through sanitizeWsEnum for consistent forward-compat behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/tasks.ts` around lines 298 - 307, The current validation drops
frames when unknown enum values appear because estimated_complexity,
task_structure, and coordination_topology are checked against hardcoded sets
(COMPLEXITY_SET, TASK_STRUCTURE_SET, COORDINATION_TOPOLOGY_SET) instead of using
the forward-compatible sanitizeWsEnum used for status/priority/type/source;
update the validation to route c.estimated_complexity, c.task_structure, and
c.coordination_topology through sanitizeWsEnum with appropriate fallback
defaults (or optional null handling for task_structure) instead of using
.has(...) checks to ensure unknown backend values are accepted and normalized,
or if you intentionally require strict enums add a concise comment above these
checks explaining why they must reject unknown values and cannot be sanitized.
🤖 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/api/types/websocket.ts`:
- Around line 218-224: The WsMessageSentPayload type is missing fields the store
expects; update the WsMessageSentPayload interface (in
web/src/api/types/websocket.ts) to include a message object or the top-level
fields required by parseWsMessage — specifically ensure it contains timestamp,
channel, type, priority, attachments, and metadata (and preserve existing
message_id, sender, to, content, readonly parts). Match the shape validated by
isMessageShape (or reference the canonical Message type used by
stores/messages.ts) so parseWsMessage can consume the payload without runtime
shape mismatches.

In `@web/src/components/layout/StatusBar.tsx`:
- Around line 37-43: Update the A11y contract comment inside StatusBar.tsx to
reference the correct example range for the health button implementation: change
the incorrect "line 198-205" to the actual range "lines 204-213" (or use a
non-hardcoded descriptive pointer like "the health button implementation below")
so the doc points to the full pairing of Dot and <span>{label}</span> used in
the health button example; ensure the comment still mentions Dot, label and the
wrapping button aria-label for clarity.

In `@web/src/components/ui/error-boundary.tsx`:
- Around line 113-115: The retry Button currently lacks an explicit button type
which can cause unintended form submission when rendered inside a form; update
the Button component instance that calls resetErrorBoundary (the <Button ...
onClick={resetErrorBoundary}> used for "Try Again") to include type="button" so
the element is treated as a non-submitting control and retains the existing
onClick behavior.

In `@web/src/pages/messages/ChannelListItem.tsx`:
- Around line 53-58: ChannelListItem is memoized but receives inline arrow
onClick props from ChannelSidebar (props passed as () =>
onSelectChannel(ch.name) and () => handleSelect(ch.name)), which breaks memo
because a new function is created every render; fix by creating stable callbacks
with React.useCallback in ChannelSidebar for the handlers passed to
ChannelListItem (wrap the functions that call onSelectChannel and handleSelect,
e.g. const handleClick = useCallback(() => onSelectChannel(ch.name),
[onSelectChannel, ch.name]) or appropriate dependency list) so the onClick prop
identity remains stable and ChannelListItem.memo can prevent unnecessary
re-renders.

In `@web/src/pages/setup/SkipWizardForm.tsx`:
- Around line 42-55: The catch block is the wrong failure boundary because
submitCompany() does its own error handling in the store; remove relying on
exceptions for company-creation failures and instead, immediately after awaiting
submitCompany(), read useSetupWizardStore.getState().companyResponse and branch
based on that durable state (companyResponse === null means creation failed)
before proceeding to completion/success UX; keep using getErrorMessage(err) and
setError only for real runtime errors that occur outside store mutations, and
update the message construction (using trimmed and companyResponse) to mirror
the current logic but triggered from the post-submit store-state check rather
than from the catch handler.

In `@web/src/pages/setup/WizardNavigation.tsx`:
- Around line 51-65: The Next button's disabled reason paragraph isn't
associated with the control for screen readers; when nextDisabled &&
nextDisabledReason && !isLast, add an id (e.g., nextDisabledReasonId) to the <p>
and set aria-describedby={nextDisabledReasonId} on the Button rendered earlier
(in the Button that uses onNext, nextDisabled, loading, nextLabel, ArrowRight)
so assistive tech will read the reason; ensure the aria-describedby value is
only applied when the reason exists to avoid referencing a missing id.

In `@web/src/pages/WebhookReceiptsPage.tsx`:
- Around line 63-73: The effect that preselects from urlConnection (useEffect
using urlConnection, setSelected and selected) can run before connections are
loaded and get overwritten later; update the logic to re-run when connections
change and only apply the deep-link if the target connection actually exists (or
if selected is empty) to avoid stomping a user choice. Concretely: include
connections (or a derived lookup like connections.map(c=>c.id)) in the useEffect
dependency list, and inside the effect check that urlConnection is truthy and
present in connections (or that selected is falsy) before calling
setSelected(urlConnection); apply the same change to the other similar effect
referenced (lines 86-95) so both effects respect connection availability.

In `@web/src/pages/workflow-editor/useWorkflowEditorCallbacks.ts`:
- Around line 124-126: The toast title currently uses developer shorthand
"error(s)" when setting title based on result.valid and result.errors.length;
update the logic where the title is constructed (the object with title:
result.valid ? 'Workflow is valid' : `Validation found ${result.errors.length}
error(s)`) to use proper pluralization—e.g., compute a string like `Validation
found ${n} error` when n === 1 and `Validation found ${n} errors`
otherwise—using result.errors.length to choose the singular or plural form so
the UI shows grammatically correct text.

In `@web/src/stores/providers/crud-actions.ts`:
- Around line 76-82: The mutation catch blocks for provider actions
(createFromPreset, updateProvider, deleteProvider and the existing create
provider catch in crud-actions.ts) must also emit a log event before showing the
toast and returning null; add a logging call (e.g., console.error or the
project's app logger) in each catch that logs the error object and contextual
metadata (action name like "createFromPreset"/"updateProvider"/"deleteProvider",
provider id or preset name if available) so failures are recorded, then keep the
existing useToastStore.getState().add(...) and return null. Ensure you add the
log immediately inside each catch (before getCrudErrorTitle/getErrorMessage
usage) and reference the same identifiers (createFromPreset, updateProvider,
deleteProvider) so the changes are easy to locate.

---

Outside diff comments:
In `@web/src/pages/approvals/ApprovalCard.tsx`:
- Around line 19-55: The component ApprovalCardImpl is calling setCountdown
during render when approval.seconds_remaining changes (using prevSecondsRef),
which must be moved into a useEffect to avoid render-phase state updates;
replace the inline check and setCountdown (the block that compares
approval.seconds_remaining to prevSecondsRef.current and calls setCountdown)
with a useEffect that has approval.seconds_remaining in its dependency array,
updates prevSecondsRef.current and calls setCountdown inside that effect,
preserving the existing prevSecondsRef, countdown state, and behavior.

In `@web/src/pages/budget/AgentSpendingTable.tsx`:
- Line 48: The agent-name comparison uses a bare
a.agentName.localeCompare(b.agentName) which relies on browser locale; import
getLocale from '@/utils/locale' and change the comparison in the
AgentSpendingTable sort/case 'agentName' branch to
a.agentName.localeCompare(b.agentName, getLocale()) so sorting uses the app/user
locale consistently (add the import at the top and update the 'agentName' case).
- Line 99: Replace the hand-rolled percentage rendering that uses
row.budgetPercent.toFixed(1) + '%' with an Intl.NumberFormat-based formatter;
update the JSX in AgentSpendingTable (where row.budgetPercent is rendered) to
use new Intl.NumberFormat with style: 'percent' or number format for one decimal
place and the current locale (remember budgetPercent is already 0–100 so divide
by 100 if using style: 'percent'), e.g., create a formatter once (or inline) and
use formatter.format(...) instead of .toFixed and string concatenation to ensure
locale-aware decimal separators and percent symbol placement.

In `@web/src/pages/UsersPage.tsx`:
- Around line 116-136: The ErrorBanner is rendered after the filters, violating
the list-page contract; move the conditional ErrorBanner block so it appears
immediately after the ListHeader component (before the
SearchFilterSort/SearchInput and any pagination/filter rows) while retaining the
existing conditional (error) check and the onRetry handler (fetchUsers). Locate
the ErrorBanner, ListHeader, SearchFilterSort, and fetchUsers symbols in
UsersPage and reposition the ErrorBanner JSX directly below ListHeader, keeping
its props (severity, title, description, onRetry) unchanged and ensuring no
layout or aria semantics are altered.

In `@web/src/pages/WebhookReceiptsPage.tsx`:
- Around line 186-204: The ErrorBanner is rendered after SearchFilterSort but
per guidelines it must appear immediately after ListHeader; move the
<ErrorBanner ... /> JSX block so it sits directly after the <ListHeader />
component and before the <SearchFilterSort /> block, preserving its props
(severity, title, description={error}, onRetry={() => void reload()}) and
conditional rendering (error && ...). Ensure SearchFilterSort (with SelectField
value={selected} onChange={setSelected} options={options}) remains intact and
that the error conditional still uses the same reload handler.

In `@web/src/stores/messages.ts`:
- Around line 179-180: The code is unsafely casting sanitized strings to enum
types for Message['type'] and Message['priority']; replace the raw
sanitizeWsString(... ) as Message['type'] / Message['priority'] with calls to
sanitizeWsEnum using the appropriate allowlists (MESSAGE_TYPE_VALUES and
MESSAGE_PRIORITY_VALUES) so unknown backend values trigger the warning+fallback
behavior; update the object construction that sets type and priority to use
sanitizeWsEnum(value, MESSAGE_TYPE_VALUES, 'Message.type', DEFAULT_TYPE) and
sanitizeWsEnum(value, MESSAGE_PRIORITY_VALUES, 'Message.priority',
DEFAULT_PRIORITY) (or adjust the existing fallback constants) so values are
validated instead of being force-cast.

In `@web/src/stores/tasks.ts`:
- Around line 298-307: The current validation drops frames when unknown enum
values appear because estimated_complexity, task_structure, and
coordination_topology are checked against hardcoded sets (COMPLEXITY_SET,
TASK_STRUCTURE_SET, COORDINATION_TOPOLOGY_SET) instead of using the
forward-compatible sanitizeWsEnum used for status/priority/type/source; update
the validation to route c.estimated_complexity, c.task_structure, and
c.coordination_topology through sanitizeWsEnum with appropriate fallback
defaults (or optional null handling for task_structure) instead of using
.has(...) checks to ensure unknown backend values are accepted and normalized,
or if you intentionally require strict enums add a concise comment above these
checks explaining why they must reject unknown values and cannot be sanitized.
🪄 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: 3805fe86-8ceb-4109-a941-ab6e92035ad0

📥 Commits

Reviewing files that changed from the base of the PR and between 4bedae1 and 6c45c78.

📒 Files selected for processing (73)
  • docs/reference/web-package-structure.md
  • web/CLAUDE.md
  • web/src/__tests__/components/ui/progress-gauge.test.tsx
  • web/src/__tests__/hooks/use-empty-state-props.test.ts
  • web/src/__tests__/pages/BudgetPage.test.tsx
  • web/src/__tests__/pages/DashboardPage.test.tsx
  • web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
  • web/src/__tests__/pages/setup/ProvidersStep.test.tsx
  • web/src/__tests__/stores/artifacts.test.ts
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/__tests__/stores/setup-wizard.test.ts
  • web/src/__tests__/styles/status-colors.test.ts
  • web/src/__tests__/utils/errors.test.ts
  • web/src/__tests__/utils/ws-sanitize.test.ts
  • web/src/api/types/approvals.ts
  • web/src/api/types/enums.ts
  • web/src/api/types/messages.ts
  • web/src/api/types/websocket.ts
  • web/src/components/layout/StatusBar.tsx
  • web/src/components/ui/detail-nav-bar.stories.tsx
  • web/src/components/ui/error-boundary.tsx
  • web/src/components/ui/progress-gauge.tsx
  • web/src/components/ui/skeleton.tsx
  • web/src/hooks/use-empty-state-props.ts
  • web/src/mocks/handlers/helpers.ts
  • web/src/mocks/handlers/index.ts
  • web/src/pages/ApprovalsPage.tsx
  • web/src/pages/BudgetForecastPage.tsx
  • web/src/pages/BudgetPage.tsx
  • web/src/pages/ClientListPage.tsx
  • web/src/pages/ConnectionsPage.tsx
  • web/src/pages/DashboardPage.tsx
  • web/src/pages/EscalationQueuePage.tsx
  • web/src/pages/ProvidersPage.tsx
  • web/src/pages/RequestQueuePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/TaskBoardPage.tsx
  • web/src/pages/UsersPage.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/WorkflowsPage.tsx
  • web/src/pages/agents/TaskHistory.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/pages/approvals/ApprovalDetailDrawer.tsx
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/connections/ConnectionCard.tsx
  • web/src/pages/connections/TunnelCard.tsx
  • web/src/pages/dashboard/ActivityFeedItem.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/pages/oauth-apps/OauthAppCard.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/pages/tasks/TaskColumn.tsx
  • web/src/pages/tasks/useTaskActionHandlers.ts
  • web/src/pages/workflow-editor/useWorkflowEditorCallbacks.ts
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/stores/approvals.ts
  • web/src/stores/artifacts.ts
  • web/src/stores/connections/crud-actions.ts
  • web/src/stores/meetings.ts
  • web/src/stores/messages.ts
  • web/src/stores/notifications.ts
  • web/src/stores/providers/crud-actions.ts
  • web/src/stores/setup-wizard/providers.ts
  • web/src/stores/setup-wizard/types.ts
  • web/src/stores/tasks.ts
  • web/src/styles/status-colors.ts
  • web/src/utils/errors.ts
  • web/src/utils/ws-sanitize.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 Web Assets (melange)
  • GitHub Check: Lighthouse Site
  • GitHub Check: Lighthouse Dashboard
  • GitHub Check: CodSpeed Web benchmarks
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (16)
**/*.{py,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{py,ts,tsx}: No default may privilege a single region, currency, or locale. Every user-facing format resolves from: user/company setting -> browser/system -> neutral fallback.
Currency: never hardcode ISO 4217 codes or symbols. Backend: DEFAULT_CURRENCY from synthorg.budget.currency or the runtime budget.currency setting. Frontend: DEFAULT_CURRENCY from @/utils/currencies or useSettingsStore().currency.
Field naming: no _usd suffix on money fields anywhere. The type carries money semantics; the value is in the operator's configured currency.
Locale: never hardcode BCP 47 tags or call bare .toLocaleString() / .toLocaleDateString() / .toLocaleTimeString(). Use helpers in @/utils/format which read getLocale() from @/utils/locale. The backend has no operator-tunable locale setting; backend Intl formatting uses the system locale plus the browser timezone. The company.name_locales list controls procedural-name generation only; it does not feed number / date / time formatting.
Timezone: store UTC; render via Intl without passing timeZone (browser tz wins).
Date / number format: always via Intl; no hand-rolled templates.

Files:

  • web/src/pages/oauth-apps/OauthAppCard.tsx
  • web/src/components/ui/progress-gauge.tsx
  • web/src/pages/DashboardPage.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/components/ui/error-boundary.tsx
  • web/src/pages/ConnectionsPage.tsx
  • web/src/pages/ProvidersPage.tsx
  • web/src/pages/RequestQueuePage.tsx
  • web/src/styles/status-colors.ts
  • web/src/pages/connections/TunnelCard.tsx
  • web/src/api/types/messages.ts
  • web/src/__tests__/pages/BudgetPage.test.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/mocks/handlers/index.ts
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/TaskBoardPage.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/hooks/use-empty-state-props.ts
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/stores/artifacts.ts
  • web/src/__tests__/styles/status-colors.test.ts
  • web/src/mocks/handlers/helpers.ts
  • web/src/pages/ClientListPage.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/__tests__/hooks/use-empty-state-props.test.ts
  • web/src/pages/BudgetForecastPage.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/utils/errors.ts
  • web/src/pages/approvals/ApprovalDetailDrawer.tsx
  • web/src/pages/dashboard/ActivityFeedItem.tsx
  • web/src/__tests__/components/ui/progress-gauge.test.tsx
  • web/src/pages/tasks/useTaskActionHandlers.ts
  • web/src/stores/notifications.ts
  • web/src/__tests__/pages/DashboardPage.test.tsx
  • web/src/pages/tasks/TaskColumn.tsx
  • web/src/stores/providers/crud-actions.ts
  • web/src/stores/messages.ts
  • web/src/stores/connections/crud-actions.ts
  • web/src/pages/workflow-editor/useWorkflowEditorCallbacks.ts
  • web/src/pages/agents/TaskHistory.tsx
  • web/src/__tests__/pages/setup/ProvidersStep.test.tsx
  • web/src/api/types/enums.ts
  • web/src/pages/connections/ConnectionCard.tsx
  • web/src/stores/setup-wizard/types.ts
  • web/src/pages/UsersPage.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/components/layout/StatusBar.tsx
  • web/src/pages/BudgetPage.tsx
  • web/src/components/ui/skeleton.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/__tests__/utils/errors.test.ts
  • web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
  • web/src/components/ui/detail-nav-bar.stories.tsx
  • web/src/stores/meetings.ts
  • web/src/pages/WorkflowsPage.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/api/types/approvals.ts
  • web/src/stores/approvals.ts
  • web/src/pages/EscalationQueuePage.tsx
  • web/src/__tests__/utils/ws-sanitize.test.ts
  • web/src/stores/tasks.ts
  • web/src/pages/ApprovalsPage.tsx
  • web/src/__tests__/stores/setup-wizard.test.ts
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/api/types/websocket.ts
  • web/src/__tests__/stores/artifacts.test.ts
  • web/src/stores/setup-wizard/providers.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Units: metric only. Spelling: International / British English UI default (colour, behaviour, organise, centred, analyse, cancelled); document deviations. Spelling here is an editorial / UI-copy decision only; it does not affect runtime locale-sensitive formatting. Numbers, dates, times, currencies, and units still resolve via the user / company / browser / system fallback through @/utils/format, @/utils/locale, DEFAULT_CURRENCY, and useSettingsStore().currency, with no contradiction to the locale-neutral defaults above.

Files:

  • web/src/pages/oauth-apps/OauthAppCard.tsx
  • web/src/components/ui/progress-gauge.tsx
  • web/src/pages/DashboardPage.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/components/ui/error-boundary.tsx
  • web/src/pages/ConnectionsPage.tsx
  • web/src/pages/ProvidersPage.tsx
  • web/src/pages/RequestQueuePage.tsx
  • web/src/styles/status-colors.ts
  • web/src/pages/connections/TunnelCard.tsx
  • web/src/api/types/messages.ts
  • web/src/__tests__/pages/BudgetPage.test.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/mocks/handlers/index.ts
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/TaskBoardPage.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/hooks/use-empty-state-props.ts
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/stores/artifacts.ts
  • web/src/__tests__/styles/status-colors.test.ts
  • web/src/mocks/handlers/helpers.ts
  • web/src/pages/ClientListPage.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/__tests__/hooks/use-empty-state-props.test.ts
  • web/src/pages/BudgetForecastPage.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/utils/errors.ts
  • web/src/pages/approvals/ApprovalDetailDrawer.tsx
  • web/src/pages/dashboard/ActivityFeedItem.tsx
  • web/src/__tests__/components/ui/progress-gauge.test.tsx
  • web/src/pages/tasks/useTaskActionHandlers.ts
  • web/src/stores/notifications.ts
  • web/src/__tests__/pages/DashboardPage.test.tsx
  • web/src/pages/tasks/TaskColumn.tsx
  • web/src/stores/providers/crud-actions.ts
  • web/src/stores/messages.ts
  • web/src/stores/connections/crud-actions.ts
  • web/src/pages/workflow-editor/useWorkflowEditorCallbacks.ts
  • web/src/pages/agents/TaskHistory.tsx
  • web/src/__tests__/pages/setup/ProvidersStep.test.tsx
  • web/src/api/types/enums.ts
  • web/src/pages/connections/ConnectionCard.tsx
  • web/src/stores/setup-wizard/types.ts
  • web/src/pages/UsersPage.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/components/layout/StatusBar.tsx
  • web/src/pages/BudgetPage.tsx
  • web/src/components/ui/skeleton.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/__tests__/utils/errors.test.ts
  • web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
  • web/src/components/ui/detail-nav-bar.stories.tsx
  • web/src/stores/meetings.ts
  • web/src/pages/WorkflowsPage.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/api/types/approvals.ts
  • web/src/stores/approvals.ts
  • web/src/pages/EscalationQueuePage.tsx
  • web/src/__tests__/utils/ws-sanitize.test.ts
  • web/src/stores/tasks.ts
  • web/src/pages/ApprovalsPage.tsx
  • web/src/__tests__/stores/setup-wizard.test.ts
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/api/types/websocket.ts
  • web/src/__tests__/stores/artifacts.test.ts
  • web/src/stores/setup-wizard/providers.ts
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Reuse components from web/src/components/ui/ before creating new ones. Never hardcode hex colors, font-family, pixel spacing, Motion transitions, or BCP 47 locale strings; use design tokens, @/lib/motion presets, and the helpers in @/utils/format. Enforced by scripts/check_web_design_system.py (PostToolUse hook on every web/src/ edit).

Always use createLogger from @/lib/logger; never use bare console.warn/console.error/console.debug in application code

Logger variable name must always be log (e.g., const log = createLogger('module-name'))

Pass dynamic/untrusted values as separate args to logger methods (not interpolated into the message string) so they go through sanitizeArg

Wrap attacker-controlled fields inside structured objects in sanitizeForLog() before embedding in logs

Files:

  • web/src/pages/oauth-apps/OauthAppCard.tsx
  • web/src/components/ui/progress-gauge.tsx
  • web/src/pages/DashboardPage.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/components/ui/error-boundary.tsx
  • web/src/pages/ConnectionsPage.tsx
  • web/src/pages/ProvidersPage.tsx
  • web/src/pages/RequestQueuePage.tsx
  • web/src/styles/status-colors.ts
  • web/src/pages/connections/TunnelCard.tsx
  • web/src/api/types/messages.ts
  • web/src/__tests__/pages/BudgetPage.test.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/mocks/handlers/index.ts
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/TaskBoardPage.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/hooks/use-empty-state-props.ts
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/stores/artifacts.ts
  • web/src/__tests__/styles/status-colors.test.ts
  • web/src/mocks/handlers/helpers.ts
  • web/src/pages/ClientListPage.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/__tests__/hooks/use-empty-state-props.test.ts
  • web/src/pages/BudgetForecastPage.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/utils/errors.ts
  • web/src/pages/approvals/ApprovalDetailDrawer.tsx
  • web/src/pages/dashboard/ActivityFeedItem.tsx
  • web/src/__tests__/components/ui/progress-gauge.test.tsx
  • web/src/pages/tasks/useTaskActionHandlers.ts
  • web/src/stores/notifications.ts
  • web/src/__tests__/pages/DashboardPage.test.tsx
  • web/src/pages/tasks/TaskColumn.tsx
  • web/src/stores/providers/crud-actions.ts
  • web/src/stores/messages.ts
  • web/src/stores/connections/crud-actions.ts
  • web/src/pages/workflow-editor/useWorkflowEditorCallbacks.ts
  • web/src/pages/agents/TaskHistory.tsx
  • web/src/__tests__/pages/setup/ProvidersStep.test.tsx
  • web/src/api/types/enums.ts
  • web/src/pages/connections/ConnectionCard.tsx
  • web/src/stores/setup-wizard/types.ts
  • web/src/pages/UsersPage.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/components/layout/StatusBar.tsx
  • web/src/pages/BudgetPage.tsx
  • web/src/components/ui/skeleton.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/__tests__/utils/errors.test.ts
  • web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
  • web/src/components/ui/detail-nav-bar.stories.tsx
  • web/src/stores/meetings.ts
  • web/src/pages/WorkflowsPage.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/api/types/approvals.ts
  • web/src/stores/approvals.ts
  • web/src/pages/EscalationQueuePage.tsx
  • web/src/__tests__/utils/ws-sanitize.test.ts
  • web/src/stores/tasks.ts
  • web/src/pages/ApprovalsPage.tsx
  • web/src/__tests__/stores/setup-wizard.test.ts
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/api/types/websocket.ts
  • web/src/__tests__/stores/artifacts.test.ts
  • web/src/stores/setup-wizard/providers.ts
web/src/{components,pages,hooks}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Callers must NOT wrap store mutation calls in try/catch; the store owns the error UX

NEVER hardcode hex colors, font-family declarations, pixel spacing, Motion transition durations, BCP 47 locale literals ('en-US'), or currency symbols/codes; use design tokens, @/lib/motion presets, helpers in @/utils/format, and DEFAULT_CURRENCY from @/utils/currencies

Files:

  • web/src/pages/oauth-apps/OauthAppCard.tsx
  • web/src/components/ui/progress-gauge.tsx
  • web/src/pages/DashboardPage.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/components/ui/error-boundary.tsx
  • web/src/pages/ConnectionsPage.tsx
  • web/src/pages/ProvidersPage.tsx
  • web/src/pages/RequestQueuePage.tsx
  • web/src/pages/connections/TunnelCard.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/TaskBoardPage.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/hooks/use-empty-state-props.ts
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/ClientListPage.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/pages/BudgetForecastPage.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/pages/approvals/ApprovalDetailDrawer.tsx
  • web/src/pages/dashboard/ActivityFeedItem.tsx
  • web/src/pages/tasks/useTaskActionHandlers.ts
  • web/src/pages/tasks/TaskColumn.tsx
  • web/src/pages/workflow-editor/useWorkflowEditorCallbacks.ts
  • web/src/pages/agents/TaskHistory.tsx
  • web/src/pages/connections/ConnectionCard.tsx
  • web/src/pages/UsersPage.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/components/layout/StatusBar.tsx
  • web/src/pages/BudgetPage.tsx
  • web/src/components/ui/skeleton.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/components/ui/detail-nav-bar.stories.tsx
  • web/src/pages/WorkflowsPage.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/pages/EscalationQueuePage.tsx
  • web/src/pages/ApprovalsPage.tsx
  • web/src/pages/setup/ProvidersStep.tsx
web/src/{components,pages}/**/*.tsx

📄 CodeRabbit inference engine (web/CLAUDE.md)

Use <StatusBadge> instead of recreating status dots inline (defaults to role='img' with aria-label; decorative for adjacent-labeled, announce for live WS updates)

Use <MetricCard> / <Sparkline> / <ProgressGauge> / <TokenUsageBar> instead of creating KPI displays inline

Use <SectionCard> (titled wrapper with icon and action slot), <AgentCard>, <DeptHealthBar> instead of recreating cards inline

Use <InputField> / <SelectField> / <SliderField> / <ToggleField> / <SegmentedControl> / <TagInput> / <SearchInput> instead of creating form fields inline

Use <Drawer width='compact|narrow|default|wide'> for slide-in panels; do NOT add inline w-[40vw] overrides

Use <Skeleton> family / <EmptyState> / <ErrorBoundary> / <ErrorBanner> / <ProgressIndicator> for loading/empty/error states instead of creating inline

Use STATUS_COLORS family from @/styles/status-colors (typed Record<EnumValue, string> lookups) instead of inline Record<EnumValue, string> constants per page

Use <ConfirmDialog> / <Toast> (Zustand-backed queue) instead of creating confirmation/toast UI inline; NOT Base UI's Toast

Use <CommandPalette> / <KeyboardShortcutHint> / <CommandCheatsheet> instead of creating Cmd+K/shortcuts UI inline

Use <AnimatedPresence> / <StaggerGroup> / <LiveRegion> for animation; <LiveRegion> is debounced ARIA live for WS updates

Files:

  • web/src/pages/oauth-apps/OauthAppCard.tsx
  • web/src/components/ui/progress-gauge.tsx
  • web/src/pages/DashboardPage.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/components/ui/error-boundary.tsx
  • web/src/pages/ConnectionsPage.tsx
  • web/src/pages/ProvidersPage.tsx
  • web/src/pages/RequestQueuePage.tsx
  • web/src/pages/connections/TunnelCard.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/TaskBoardPage.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/ClientListPage.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/pages/BudgetForecastPage.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/pages/approvals/ApprovalDetailDrawer.tsx
  • web/src/pages/dashboard/ActivityFeedItem.tsx
  • web/src/pages/tasks/TaskColumn.tsx
  • web/src/pages/agents/TaskHistory.tsx
  • web/src/pages/connections/ConnectionCard.tsx
  • web/src/pages/UsersPage.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/components/layout/StatusBar.tsx
  • web/src/pages/BudgetPage.tsx
  • web/src/components/ui/skeleton.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/components/ui/detail-nav-bar.stories.tsx
  • web/src/pages/WorkflowsPage.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/pages/EscalationQueuePage.tsx
  • web/src/pages/ApprovalsPage.tsx
  • web/src/pages/setup/ProvidersStep.tsx
web/src/pages/**/*.tsx

📄 CodeRabbit inference engine (web/CLAUDE.md)

Use <ListHeader> / <SearchFilterSort> / <Pagination> / <BulkActionBar> / <MetadataGrid> / <Breadcrumbs> for list-page primitives

List pages: root container uses space-y-section-gap (the majority pattern; flex flex-col gap-section-gap is equivalent but discouraged); <ErrorBanner> lands immediately after <ListHeader>, before any filter/pagination row; pages with a one-line mission statement pass it via <ListHeader description='...' />

Choose list layout: use Kanban grouping for status-flow domains where each row's column conveys lifecycle phase (Tasks, Requests); use a flat scrollable list for queues without explicit phase semantics (Escalations, Approvals)

Use useEmptyStateProps({ filteredCount, totalCount, filterActive, empty, filtered }) from @/hooks/use-empty-state-props to return EmptyStateProps | null so pages branch on a single value instead of duplicating the 'no data ever' / 'no data after filter' discriminator

Files:

  • web/src/pages/oauth-apps/OauthAppCard.tsx
  • web/src/pages/DashboardPage.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/pages/ConnectionsPage.tsx
  • web/src/pages/ProvidersPage.tsx
  • web/src/pages/RequestQueuePage.tsx
  • web/src/pages/connections/TunnelCard.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/TaskBoardPage.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/ClientListPage.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/pages/BudgetForecastPage.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/pages/approvals/ApprovalDetailDrawer.tsx
  • web/src/pages/dashboard/ActivityFeedItem.tsx
  • web/src/pages/tasks/TaskColumn.tsx
  • web/src/pages/agents/TaskHistory.tsx
  • web/src/pages/connections/ConnectionCard.tsx
  • web/src/pages/UsersPage.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/BudgetPage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/WorkflowsPage.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/pages/EscalationQueuePage.tsx
  • web/src/pages/ApprovalsPage.tsx
  • web/src/pages/setup/ProvidersStep.tsx
web/src/components/**/*.tsx

📄 CodeRabbit inference engine (web/CLAUDE.md)

Use Base UI's render prop as the polymorphism primitive throughout the dashboard; use the local <Slot> helper in components/ui/slot.tsx with @base-ui/react/merge-props only for <Button asChild>, all other primitives use Base UI's native render prop directly

ALWAYS reuse existing components from web/src/components/ui/ before creating new ones

Base UI primitives are imported directly from @base-ui/react/<subpath> and use the native render prop for polymorphism; the local <Slot> helper is reserved for <Button asChild>

Adopted Base UI primitives (direct import from @base-ui/react/<subpath>): Dialog, AlertDialog, Popover, Tabs, Menu, Drawer, CSPProvider, merge-props

Files:

  • web/src/components/ui/progress-gauge.tsx
  • web/src/components/ui/error-boundary.tsx
  • web/src/components/layout/StatusBar.tsx
  • web/src/components/ui/skeleton.tsx
  • web/src/components/ui/detail-nav-bar.stories.tsx
web/src/components/ui/**/*.tsx

📄 CodeRabbit inference engine (web/CLAUDE.md)

Every new shared component lives in web/src/components/ui/ with a sibling .stories.tsx covering all states (default, hover, loading, error, empty, disabled where applicable)

Component Props interface name must be <ComponentName>Props and exported from the same file (e.g., AgentCardProps in agent-card.tsx) to make the contract greppable and allow callers to extend props

Files:

  • web/src/components/ui/progress-gauge.tsx
  • web/src/components/ui/error-boundary.tsx
  • web/src/components/ui/skeleton.tsx
  • web/src/components/ui/detail-nav-bar.stories.tsx
web/src/{api,stores}/**/*.ts

📄 CodeRabbit inference engine (web/CLAUDE.md)

Use cursor-based pagination via PaginationMeta; keep nextCursor + hasMore in store state (not offset arithmetic) and early-return when !hasMore || !nextCursor

Display counts must come from data.length; the wire envelope no longer carries total

Files:

  • web/src/api/types/messages.ts
  • web/src/stores/artifacts.ts
  • web/src/stores/notifications.ts
  • web/src/stores/providers/crud-actions.ts
  • web/src/stores/messages.ts
  • web/src/stores/connections/crud-actions.ts
  • web/src/api/types/enums.ts
  • web/src/stores/setup-wizard/types.ts
  • web/src/stores/meetings.ts
  • web/src/api/types/approvals.ts
  • web/src/stores/approvals.ts
  • web/src/stores/tasks.ts
  • web/src/api/types/websocket.ts
  • web/src/stores/setup-wizard/providers.ts
web/src/{api,hooks}/**/*.ts

📄 CodeRabbit inference engine (web/CLAUDE.md)

getLiveness() is always 200 while the process is alive; getReadiness() is 200 healthy / 503 unavailable (binary 'ok' | 'unavailable' outcome, no tri-state) and callers must handle the 503 path explicitly

Files:

  • web/src/api/types/messages.ts
  • web/src/hooks/use-empty-state-props.ts
  • web/src/api/types/enums.ts
  • web/src/api/types/approvals.ts
  • web/src/api/types/websocket.ts
docs/**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

docs/**/*.md: Use Mermaid (\``mermaid): flowcharts, sequence diagrams, simple hierarchies, pipelines. Rendered client-side via pymdownx.superfences. Use **Markdown tables**: grid/matrix data that is semantically tabular (not diagrams). D2 uses theme 200 (Dark Mauve), dark-only render, configured globally in mkdocs.yml. Never use ```text` blocks with ASCII/Unicode box-drawing characters for diagrams.

Files:

  • docs/reference/web-package-structure.md
web/src/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Never use vi.mock('@/api/endpoints/*'); instead override MSW handlers per-case via server.use(...)

Files:

  • web/src/__tests__/pages/BudgetPage.test.tsx
  • web/src/__tests__/styles/status-colors.test.ts
  • web/src/__tests__/hooks/use-empty-state-props.test.ts
  • web/src/__tests__/components/ui/progress-gauge.test.tsx
  • web/src/__tests__/pages/DashboardPage.test.tsx
  • web/src/__tests__/pages/setup/ProvidersStep.test.tsx
  • web/src/__tests__/utils/errors.test.ts
  • web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
  • web/src/__tests__/utils/ws-sanitize.test.ts
  • web/src/__tests__/stores/setup-wizard.test.ts
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/__tests__/stores/artifacts.test.ts
web/src/mocks/handlers/**/*.ts

📄 CodeRabbit inference engine (web/CLAUDE.md)

MSW handlers in web/src/mocks/handlers/ must mirror web/src/api/endpoints/*.ts 1:1 with default happy-path handler for every exported endpoint

Use typed envelope helpers (successFor, paginatedFor, voidSuccess) to keep MSW handlers in lockstep with endpoint return types

Files:

  • web/src/mocks/handlers/index.ts
  • web/src/mocks/handlers/helpers.ts
web/src/stores/**/*.ts

📄 CodeRabbit inference engine (web/CLAUDE.md)

All store mutation actions (create/update/delete) must follow the stores/connections/crud-actions.ts pattern: wrap API calls in try/catch, success path updates state + emits success toast, failure path logs + emits error toast + returns sentinel

List store reads must set error: string | null on the store instead of toasting

Global afterEach in web/src/test-setup.tsx calls useToastStore.getState().dismissAll(), cancelPendingPersist(), and useThemeStore.getState().teardown(); any new store that schedules timers or attaches event listeners must expose an equivalent cleanup hook and register it in the global afterEach

Files:

  • web/src/stores/artifacts.ts
  • web/src/stores/notifications.ts
  • web/src/stores/providers/crud-actions.ts
  • web/src/stores/messages.ts
  • web/src/stores/connections/crud-actions.ts
  • web/src/stores/setup-wizard/types.ts
  • web/src/stores/meetings.ts
  • web/src/stores/approvals.ts
  • web/src/stores/tasks.ts
  • web/src/stores/setup-wizard/providers.ts
web/src/{utils,stores}/**/*.ts

📄 CodeRabbit inference engine (web/CLAUDE.md)

Use sanitizeWsString() to clamp every WS-supplied string (strips C0 controls + bidi-overrides + caps length) and sanitizeWsEnum<T>(value, allowlist, fallback, { field }) for enum-allowlist validation; raw (sanitizeWsString(x, n) ?? '') as EnumType casts are forbidden

WS wire protocol constants (WS_PROTOCOL_VERSION, WS_MAX_MESSAGE_SIZE, WS_HEARTBEAT_INTERVAL_MS, WS_PONG_TIMEOUT_MS, LOG_SANITIZE_MAX_LENGTH) in web/src/utils/constants.ts MUST stay in lockstep with src/synthorg/api/ws_models.py / src/synthorg/api/controllers/ws.py; bump protocol version on both sides together for breaking changes

Files:

  • web/src/stores/artifacts.ts
  • web/src/utils/errors.ts
  • web/src/stores/notifications.ts
  • web/src/stores/providers/crud-actions.ts
  • web/src/stores/messages.ts
  • web/src/stores/connections/crud-actions.ts
  • web/src/stores/setup-wizard/types.ts
  • web/src/stores/meetings.ts
  • web/src/stores/approvals.ts
  • web/src/stores/tasks.ts
  • web/src/stores/setup-wizard/providers.ts
web/**/*.stories.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Storybook 10 is ESM-only; essentials are built into core but @storybook/addon-docs is separate; imports moved to storybook/test and storybook/actions

Files:

  • web/src/components/ui/detail-nav-bar.stories.tsx
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T13:33:57.345Z
Learning: CI fails if `vitest --detect-async-leaks` reports more than `MAX_ASYNC_LEAKS` (currently 90); raise the ceiling only with documented per-PR justification
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T13:33:57.345Z
Learning: TypeScript 6: `baseUrl` is deprecated and `esModuleInterop` is always true; `types` defaults to `[]` so `vitest/globals` etc. need explicit listing in `tsconfig.json`

Comment on lines +218 to +224
export interface WsMessageSentPayload {
message_id: string
sender: string
to: string
content: string
readonly parts: readonly WsMessagePart[]
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check what fields sanitizeMessage actually reads from the payload
rg -A 30 "function sanitizeMessage" web/src/stores/messages.ts

Repository: Aureliolo/synthorg

Length of output: 44


🏁 Script executed:

# Check if the messages.ts file exists and what's around line 117
fd -type f "messages.ts" web/src

# Also look for sanitizeMessage in different ways
rg "sanitizeMessage" web/src/stores/ --max-count 5

Repository: Aureliolo/synthorg

Length of output: 233


🏁 Script executed:

# Find messages.ts file
find web/src -name "messages.ts" -type f

# Search for sanitizeMessage anywhere
rg "sanitizeMessage" web/src --type ts --type tsx

Repository: Aureliolo/synthorg

Length of output: 260


🏁 Script executed:

# Search for sanitizeMessage without type filter
rg "sanitizeMessage" web/src

# Read the messages.ts store file to see the actual implementation
wc -l web/src/stores/messages.ts

Repository: Aureliolo/synthorg

Length of output: 93


🏁 Script executed:

# Read the messages.ts file around line 117
sed -n '100,150p' web/src/stores/messages.ts

# Also get more context around sanitizeMessage
rg -B 5 -A 20 "sanitizeMessage" web/src/stores/messages.ts

Repository: Aureliolo/synthorg

Length of output: 1784


🏁 Script executed:

# Find the isMessageShape function definition
rg -B 2 -A 15 "isMessageShape" web/src/stores/messages.ts | head -50

# Also read WsMessageSentPayload from websocket.ts
sed -n '218,224p' web/src/api/types/websocket.ts

# Check if there are more fields in WsMessageSentPayload or related interfaces
sed -n '210,235p' web/src/api/types/websocket.ts

Repository: Aureliolo/synthorg

Length of output: 1695


🏁 Script executed:

# Find the Message interface definition
rg -B 2 -A 15 "interface Message" web/src/stores/messages.ts | head -50

# Also check if there's a discriminated union or different message types
rg "WsMessageSentPayload" web/src --max-count 10

# Look for where WsMessageSentPayload is actually used
rg -B 3 -A 3 "WsMessageSentPayload" web/src/stores/messages.ts

Repository: Aureliolo/synthorg

Length of output: 634


🏁 Script executed:

# Find the Message interface definition in messages.ts
rg -B 2 -A 20 "^interface Message[^S]" web/src/stores/messages.ts

# Check the full websocket types file to see the discriminated union
rg -B 5 -A 5 "'message.sent'" web/src/api/types/websocket.ts

# Also check type definitions around line 218
sed -n '200,250p' web/src/api/types/websocket.ts

Repository: Aureliolo/synthorg

Length of output: 2691


🏁 Script executed:

# Find where WsMessageSentPayload is actually used in the store
rg -B 5 -A 10 "payload.message" web/src/stores/messages.ts | head -80

# Check if there's a wrapper around message payloads
rg -B 2 -A 10 "message.sent" web/src/stores/messages.ts

Repository: Aureliolo/synthorg

Length of output: 579


🏁 Script executed:

# Check the WsEvent type definition to understand the structure
rg -B 5 -A 20 "type WsEvent" web/src/api/types/websocket.ts

# Look for the message.sent handler or how the payload is structured
rg -B 10 "parseWsMessage" web/src/stores/messages.ts | head -40

Repository: Aureliolo/synthorg

Length of output: 3012


🏁 Script executed:

# Find the Message interface definition more carefully
rg "^interface Message " web/src/stores/messages.ts -A 20

# Also check the type annotation for the message field in the payload
rg -B 5 "payload\.message" web/src/stores/messages.ts | head -40

# Look for how WsMessageSentPayload is actually supposed to be structured
rg -B 10 -A 10 "'message\.sent'" web/src/stores/messages.ts

Repository: Aureliolo/synthorg

Length of output: 323


🏁 Script executed:

# Look at the discriminated union to see if there's something specific about message.sent
sed -n '130,170p' web/src/api/types/websocket.ts

# Check how the handler uses the typed event
rg "message.sent" web/src/stores/messages.ts -B 5 -A 5

# Search for any other place where WsMessageSentPayload is used
rg "message_id" web/src/stores/messages.ts

Repository: Aureliolo/synthorg

Length of output: 869


🏁 Script executed:

# Look at the complete WsEventPayloadMap to understand the structure
rg "WsEventPayloadMap" web/src/api/types/websocket.ts -A 50 | head -80

# Check if there's a wrapper object around WsMessageSentPayload
sed -n '150,180p' web/src/api/types/websocket.ts

Repository: Aureliolo/synthorg

Length of output: 3870


🏁 Script executed:

# Check the actual handler code that dispatches message.sent events
rg "event_type.*message\.sent" web/src/stores/messages.ts -B 5 -A 10

# Look for where parseWsMessage is called and what payload structure it receives
rg "parseWsMessage" web/src/stores/messages.ts -B 3 -A 3

# Check if there's a message wrapper in the actual WsMessageSentPayload or if there's something between event and handler
rg "payload\.message" web/src -B 2 -A 2 | head -50

Repository: Aureliolo/synthorg

Length of output: 2436


🏁 Script executed:

# Check the test file to see what the actual payload structure is
rg -A 15 "skips array payload\.message" web/src/__tests__/stores/messages.test.ts

# Also check for other message.sent event tests
rg "message\.sent" web/src/__tests__/stores/messages.test.ts -B 2 -A 10

Repository: Aureliolo/synthorg

Length of output: 1825


🏁 Script executed:

# Find the makeMessage test helper to see what structure is actually expected
rg -B 5 -A 20 "const makeMessage" web/src/__tests__/stores/messages.test.ts | head -50

# Check what the full Message interface looks like
rg "^type Message = " web/src/stores/messages.ts -A 10

Repository: Aureliolo/synthorg

Length of output: 44


🏁 Script executed:

# Search for Message type definition in different ways
rg "type Message" web/src/stores/messages.ts

# Also try looking for interface Message
rg "interface Message" web/src/stores/messages.ts

# Find makeMessage in the test file
rg "makeMessage" web/src/__tests__/stores/messages.test.ts -B 2 -A 15 | head -80

Repository: Aureliolo/synthorg

Length of output: 2741


WsMessageSentPayload is incomplete relative to store requirements.

The parseWsMessage handler in stores/messages.ts expects payload.message to contain timestamp, channel, type, priority, attachments, and metadata fields (validated by isMessageShape), but WsMessageSentPayload only defines message_id, sender, to, content, and parts. Either extend the interface to match the actual event payload structure, or clarify if the wire format and store consumption differ intentionally.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/api/types/websocket.ts` around lines 218 - 224, The
WsMessageSentPayload type is missing fields the store expects; update the
WsMessageSentPayload interface (in web/src/api/types/websocket.ts) to include a
message object or the top-level fields required by parseWsMessage — specifically
ensure it contains timestamp, channel, type, priority, attachments, and metadata
(and preserve existing message_id, sender, to, content, readonly parts). Match
the shape validated by isMessageShape (or reference the canonical Message type
used by stores/messages.ts) so parseWsMessage can consume the payload without
runtime shape mismatches.

Comment thread web/src/components/layout/StatusBar.tsx Outdated
Comment thread web/src/components/ui/error-boundary.tsx Outdated
Comment thread web/src/pages/messages/ChannelListItem.tsx
Comment thread web/src/pages/setup/SkipWizardForm.tsx Outdated
Comment thread web/src/pages/setup/WizardNavigation.tsx
Comment thread web/src/pages/WebhookReceiptsPage.tsx Outdated
Comment thread web/src/pages/workflow-editor/useWorkflowEditorCallbacks.ts Outdated
Comment thread web/src/stores/providers/crud-actions.ts
)

Round 2 of CodeRabbit + Gemini findings on PR #1718 plus the recurring
async-leak CI gate.

CI gate fix:
* `.github/ci/web-async-leaks.max` raised from 90 to 100 with documented
  per-PR justification per the file's escape hatch. The +14 new tests
  this PR adds (renderHook x5 in use-empty-state-props,
  render(<ProvidersStep/>) integration test, 8 store-level cases in
  setup-wizard.test.ts and ws-sanitize.test.ts) are mechanical test
  surface, not a regression. Local 75 leaks; CI baseline +20-21
  (parallel concurrency). Lower back toward 80 once #1468 (MSW off
  XHR interceptor) lands.

CodeRabbit MAJOR findings:
* SkipWizardForm: submitCompany() doesn't throw -- it sets companyError
  in the store. The catch block was the wrong failure boundary (would
  never fire for company-create failures). Read the store snapshot
  (companyResponse !== null) immediately after await submitCompany()
  and branch there; the catch now only handles wizardCompleteSetup
  errors, so the partial-success message is the only branch the catch
  is responsible for.
* WebhookReceiptsPage URL deep-link: the seed effect could fire before
  connections loaded and get overwritten by the reconciliation effect.
  Now waits for connections to arrive AND verifies the target exists
  before applying the URL value (won't stomp a valid current selection
  with a stale URL).
* WebhookReceiptsPage / UsersPage ErrorBanner placement: moved
  immediately after ListHeader per the project's list-page contract
  (matches OauthAppsPage).
* AgentSpendingTable: localeCompare() now passes getLocale();
  .toFixed(1)% replaced with Intl.NumberFormat({ style: 'percent' })
  for locale-aware decimal separator + percent placement (project
  rule: "Date / number format: always via Intl; no hand-rolled
  templates").
* messages.ts: type / priority fields now route through sanitizeWsEnum
  with new MESSAGE_TYPE_VALUES / MESSAGE_PRIORITY_VALUES allowlists
  (previously unsafe `as` casts that bypassed allowlist validation
  entirely; same forward-compat pattern as approval/meeting/task).
* providers/crud-actions.ts: every mutation catch now logs via
  log.error BEFORE the toast, so a dismissed toast still leaves an
  observability trace. Connections store already had this; providers
  was missing it.
* error-boundary.tsx: every Button retry control gets explicit
  type="button" so it cannot accidentally submit a parent form.

CodeRabbit MINOR / Trivial findings:
* ApprovalCard countdown sync: setCountdown(approval.seconds_remaining)
  was called during render via prevSecondsRef. Moved into useEffect
  with microtask defer (matches connections-reconciliation pattern in
  WebhookReceiptsPage and satisfies the ESLint set-state-in-effect
  rule).
* WizardNavigation: disabled-Next-button caption now associates with
  the button via aria-describedby (useId-generated), so screen
  readers announce the reason when the disabled control is focused.
* useWorkflowEditorCallbacks: validation toast title uses proper
  pluralisation ("Validation found 1 error" vs "Validation found
  N errors") instead of the developer shorthand "error(s)".
* StatusBar a11y comment: "line 198-205" reference replaced with a
  descriptive pointer ("the health-button render below"); the line
  number was already drifting.
* tasks.ts shape guard: complexity / task_structure /
  coordination_topology stay strictly validated (NOT routed through
  sanitizeWsEnum) -- they're closed enums coupled to coordination
  + scheduling code paths that branch on the exact value, so dropping
  a frame on an unknown value is the safer failure mode. Added an
  inline comment explaining why these differ from
  status / priority / type / source.
* ChannelListItem: API changed from onClick: () => void to onSelect:
  (name: string) => void so callers pass a stable reference (the
  parent's onSelectChannel / handleSelect directly) instead of an
  inline arrow that breaks memo on every render. Tests + stories
  updated.

Verification:
* npm run lint: zero warnings
* npm run type-check: clean
* 2994 tests pass; local 76 leaks; CI projects ~96 (under 100).
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview May 2, 2026 14:17 — with GitHub Actions Inactive
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/src/stores/messages.ts (1)

141-148: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider whether a blank to field should also reject the payload.

The validation rejects messages when id, timestamp, sender, or channel sanitise to blank, but to (line 125) is not checked. If to is a required recipient identifier, a blank value could cause downstream routing issues or render incorrectly. If broadcast messages legitimately have an empty to, this is fine — otherwise add !to to the guard.

Proposed change if `to` is always required
- if (!id || !timestamp || !sender || !channel) {
+ if (!id || !timestamp || !sender || !to || !channel) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/messages.ts` around lines 141 - 148, The guard that rejects
sanitized-blank WS messages currently checks id, timestamp, sender, and channel
but omits the recipient field "to"; update the validation in the same
conditional (the if block that returns null) to also reject when "to" is blank
(e.g., include !to or to.length === 0 in the condition) so messages with an
empty recipient are dropped unless broadcasts are intentionally allowed; locate
the check around the variables id, timestamp, sender, channel in messages.ts and
add the "to" check to the same guard.
♻️ Duplicate comments (2)
web/src/pages/WebhookReceiptsPage.tsx (1)

54-103: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't seed fetch state from an unvalidated ?connection= value.

selected starts as urlConnection, so a cold mount can call reload() before connections has proved that the target exists. After that, the fallback effect at Lines 94-103 can still overwrite the deep-link with connections[0], and on a stale bookmark / zero-connection account the discarded first request can leave loading stuck true, so the empty state never renders. Initialising with '' and only applying the URL once the connection is present avoids both races.

Suggested fix
-  const [selected, setSelected] = useState<string>(urlConnection)
+  const [selected, setSelected] = useState<string>('')

   useEffect(() => {
     if (!urlConnection) return
     if (connections.length === 0) return
     if (!connections.some((c) => c.name === urlConnection)) return
@@
-  useEffect(() => {
-    const exists = selected !== '' && connections.some((c) => c.name === selected)
+  useEffect(() => {
+    const urlExists = urlConnection !== '' && connections.some((c) => c.name === urlConnection)
+    const exists = selected !== '' && connections.some((c) => c.name === selected)
     if (exists) return
     let cancelled = false
     void Promise.resolve().then(() => {
       if (cancelled) return
-      setSelected(connections[0]?.name ?? '')
+      setSelected(urlExists ? urlConnection : (connections[0]?.name ?? ''))
     })
     return () => { cancelled = true }
-  }, [connections, selected])
+  }, [connections, selected, urlConnection])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/WebhookReceiptsPage.tsx` around lines 54 - 103, The component
seeds selection from urlConnection causing a fetch race; change the initial
state for selected to '' instead of urlConnection so no fetch runs on cold mount
for an unvalidated URL value. Keep the existing URL-sync effect that reads
urlConnection and connections (the useEffect that checks connections.some and
then setSelected via a microtask) so the URL is applied only after the target
connection is present; leave the fallback effect (the one defaulting to
connections[0]) as-is to reconcile removed/stale selections.
web/src/pages/messages/ChannelSidebar.tsx (1)

91-94: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Memoize handleSelect or the drawer rows still miss the memo win.

handleSelect is recreated on every ChannelListBody render, so every mobile ChannelListItem still receives a new onSelect prop and re-renders even when only one row changed. Wrap it in useCallback so the drawer path gets the same row-level memoisation benefit as the desktop path.

💡 Suggested change
-import { useMemo, useState } from 'react'
+import { useCallback, useMemo, useState } from 'react'
@@
-  const handleSelect = (name: string) => {
+  const handleSelect = useCallback((name: string) => {
     onSelectChannel(name)
     onAfterSelect?.()
-  }
+  }, [onAfterSelect, onSelectChannel])
#!/bin/bash
# Verify that ChannelListItem is memoized and that ChannelSidebar still
# passes a freshly-created callback into the mobile path.
rg -nP --type=tsx -C2 'export const ChannelListItem = memo\(' web/src/pages/messages/ChannelListItem.tsx
rg -nP --type=tsx -C2 'const handleSelect = \(name: string\) =>' web/src/pages/messages/ChannelSidebar.tsx
rg -nP --type=tsx -C2 'onSelect=\{handleSelect\}' web/src/pages/messages/ChannelSidebar.tsx
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/messages/ChannelSidebar.tsx` around lines 91 - 94, handleSelect
in ChannelSidebar is recreated every render causing mobile ChannelListItem rows
to receive a new onSelect and lose memoization; wrap handleSelect in
React.useCallback inside ChannelSidebar so it returns the same function instance
unless its dependencies change (depend on onSelectChannel and onAfterSelect) so
ChannelListBody/ChannelListItem can benefit from memoization and avoid
unnecessary re-renders.
🤖 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/pages/approvals/ApprovalCard.tsx`:
- Around line 58-67: The ticking interval does not restart when
approval.seconds_remaining is updated, so refreshed values get immediately
decremented; update the ticking useEffect (the effect that watches shouldTick
and creates the interval for decrementing countdown) to also depend on
approval.seconds_remaining (or explicitly clear and recreate the interval when
approval.seconds_remaining changes) so that setCountdown is initialized from the
fresh seconds_remaining and the interval cadence resets; reference the useEffect
that sets countdown (uses setCountdown and approval.seconds_remaining) and the
ticking effect that checks shouldTick to ensure both react to changes in
approval.seconds_remaining and clear/replace the interval when it updates.

In `@web/src/stores/providers/crud-actions.ts`:
- Line 77: The log.error calls in web/src/stores/providers/crud-actions.ts
(e.g., the createProvider error path where log.error('createProvider failed:', {
name: data.name, error: getErrorMessage(err) })) are embedding untrusted fields
directly; wrap attacker-controlled fields with sanitizeForLog(...) before
passing them into the structured metadata object (e.g.,
sanitizeForLog(data.name), sanitizeForLog(data.preset_name), and
sanitizeForLog(getErrorMessage(err))). Update the analogous log.error usages
referenced (around the other occurrences noted) to use sanitizeForLog for each
untrusted value so all metadata is sanitized consistently.

---

Outside diff comments:
In `@web/src/stores/messages.ts`:
- Around line 141-148: The guard that rejects sanitized-blank WS messages
currently checks id, timestamp, sender, and channel but omits the recipient
field "to"; update the validation in the same conditional (the if block that
returns null) to also reject when "to" is blank (e.g., include !to or to.length
=== 0 in the condition) so messages with an empty recipient are dropped unless
broadcasts are intentionally allowed; locate the check around the variables id,
timestamp, sender, channel in messages.ts and add the "to" check to the same
guard.

---

Duplicate comments:
In `@web/src/pages/messages/ChannelSidebar.tsx`:
- Around line 91-94: handleSelect in ChannelSidebar is recreated every render
causing mobile ChannelListItem rows to receive a new onSelect and lose
memoization; wrap handleSelect in React.useCallback inside ChannelSidebar so it
returns the same function instance unless its dependencies change (depend on
onSelectChannel and onAfterSelect) so ChannelListBody/ChannelListItem can
benefit from memoization and avoid unnecessary re-renders.

In `@web/src/pages/WebhookReceiptsPage.tsx`:
- Around line 54-103: The component seeds selection from urlConnection causing a
fetch race; change the initial state for selected to '' instead of urlConnection
so no fetch runs on cold mount for an unvalidated URL value. Keep the existing
URL-sync effect that reads urlConnection and connections (the useEffect that
checks connections.some and then setSelected via a microtask) so the URL is
applied only after the target connection is present; leave the fallback effect
(the one defaulting to connections[0]) as-is to reconcile removed/stale
selections.
🪄 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: 4c447ea0-7d61-42bc-bf11-ca287ad415c7

📥 Commits

Reviewing files that changed from the base of the PR and between 6c45c78 and ab97e97.

📒 Files selected for processing (18)
  • .github/ci/web-async-leaks.max
  • web/src/__tests__/pages/messages/ChannelListItem.test.tsx
  • web/src/api/types/messages.ts
  • web/src/components/layout/StatusBar.tsx
  • web/src/components/ui/error-boundary.tsx
  • web/src/pages/UsersPage.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/messages/ChannelListItem.stories.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/pages/messages/ChannelSidebar.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/workflow-editor/useWorkflowEditorCallbacks.ts
  • web/src/stores/messages.ts
  • web/src/stores/providers/crud-actions.ts
  • web/src/stores/tasks.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 Web Assets (melange)
  • GitHub Check: CodSpeed Web benchmarks
  • GitHub Check: Lighthouse Dashboard
  • GitHub Check: Lighthouse Site
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{py,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{py,ts,tsx}: No default may privilege a single region, currency, or locale. Every user-facing format resolves from: user/company setting -> browser/system -> neutral fallback.
Currency: never hardcode ISO 4217 codes or symbols. Backend: DEFAULT_CURRENCY from synthorg.budget.currency or the runtime budget.currency setting. Frontend: DEFAULT_CURRENCY from @/utils/currencies or useSettingsStore().currency.
Field naming: no _usd suffix on money fields anywhere. The type carries money semantics; the value is in the operator's configured currency.
Locale: never hardcode BCP 47 tags or call bare .toLocaleString() / .toLocaleDateString() / .toLocaleTimeString(). Use helpers in @/utils/format which read getLocale() from @/utils/locale. The backend has no operator-tunable locale setting; backend Intl formatting uses the system locale plus the browser timezone. The company.name_locales list controls procedural-name generation only; it does not feed number / date / time formatting.
Timezone: store UTC; render via Intl without passing timeZone (browser tz wins).
Date / number format: always via Intl; no hand-rolled templates.

Files:

  • web/src/pages/messages/ChannelListItem.stories.tsx
  • web/src/api/types/messages.ts
  • web/src/pages/messages/ChannelSidebar.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/stores/providers/crud-actions.ts
  • web/src/pages/workflow-editor/useWorkflowEditorCallbacks.ts
  • web/src/components/ui/error-boundary.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/stores/tasks.ts
  • web/src/__tests__/pages/messages/ChannelListItem.test.tsx
  • web/src/stores/messages.ts
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/UsersPage.tsx
  • web/src/components/layout/StatusBar.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Units: metric only. Spelling: International / British English UI default (colour, behaviour, organise, centred, analyse, cancelled); document deviations. Spelling here is an editorial / UI-copy decision only; it does not affect runtime locale-sensitive formatting. Numbers, dates, times, currencies, and units still resolve via the user / company / browser / system fallback through @/utils/format, @/utils/locale, DEFAULT_CURRENCY, and useSettingsStore().currency, with no contradiction to the locale-neutral defaults above.

Files:

  • web/src/pages/messages/ChannelListItem.stories.tsx
  • web/src/api/types/messages.ts
  • web/src/pages/messages/ChannelSidebar.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/stores/providers/crud-actions.ts
  • web/src/pages/workflow-editor/useWorkflowEditorCallbacks.ts
  • web/src/components/ui/error-boundary.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/stores/tasks.ts
  • web/src/__tests__/pages/messages/ChannelListItem.test.tsx
  • web/src/stores/messages.ts
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/UsersPage.tsx
  • web/src/components/layout/StatusBar.tsx
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Reuse components from web/src/components/ui/ before creating new ones. Never hardcode hex colors, font-family, pixel spacing, Motion transitions, or BCP 47 locale strings; use design tokens, @/lib/motion presets, and the helpers in @/utils/format. Enforced by scripts/check_web_design_system.py (PostToolUse hook on every web/src/ edit).

Files:

  • web/src/pages/messages/ChannelListItem.stories.tsx
  • web/src/api/types/messages.ts
  • web/src/pages/messages/ChannelSidebar.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/stores/providers/crud-actions.ts
  • web/src/pages/workflow-editor/useWorkflowEditorCallbacks.ts
  • web/src/components/ui/error-boundary.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/stores/tasks.ts
  • web/src/__tests__/pages/messages/ChannelListItem.test.tsx
  • web/src/stores/messages.ts
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/UsersPage.tsx
  • web/src/components/layout/StatusBar.tsx
web/**/*.tsx

📄 CodeRabbit inference engine (web/CLAUDE.md)

Wrap the app in <CSPProvider nonce={getCspNonce()}> + <MotionConfig nonce> so every inline <style> tag injected by Base UI and Motion carries the per-request CSP nonce

Use Base UI's render prop as the polymorphism primitive throughout the dashboard; use the local <Slot> helper from components/ui/slot.tsx only for <Button asChild> ergonomic support, using @base-ui/react/merge-props

Files:

  • web/src/pages/messages/ChannelListItem.stories.tsx
  • web/src/pages/messages/ChannelSidebar.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/components/ui/error-boundary.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/__tests__/pages/messages/ChannelListItem.test.tsx
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/UsersPage.tsx
  • web/src/components/layout/StatusBar.tsx
web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Always use createLogger from @/lib/logger; never bare console.warn/console.error/console.debug in application code; always use variable name log

Pass dynamic/untrusted values as separate args to logger calls (not interpolated into the message string) so they go through sanitizeArg

Wrap attacker-controlled fields inside structured objects in sanitizeForLog() before embedding in logs

Be aware that TypeScript 6 has deprecated baseUrl and esModuleInterop is always true; types defaults to [] so vitest/globals etc. need explicit listing

Files:

  • web/src/pages/messages/ChannelListItem.stories.tsx
  • web/src/api/types/messages.ts
  • web/src/pages/messages/ChannelSidebar.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/stores/providers/crud-actions.ts
  • web/src/pages/workflow-editor/useWorkflowEditorCallbacks.ts
  • web/src/components/ui/error-boundary.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/stores/tasks.ts
  • web/src/__tests__/pages/messages/ChannelListItem.test.tsx
  • web/src/stores/messages.ts
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/UsersPage.tsx
  • web/src/components/layout/StatusBar.tsx
web/src/pages/**/*.tsx

📄 CodeRabbit inference engine (web/CLAUDE.md)

Use <ListHeader>, <SearchFilterSort>, <Pagination>, <BulkActionBar>, <MetadataGrid>, <Breadcrumbs> for list-page primitives; root container uses space-y-section-gap (majority pattern); <ErrorBanner> lands immediately after <ListHeader> before filter/pagination; pages with one-line mission statements pass via <ListHeader description="..." />

For list layout choice: use Kanban grouping for status-flow domains where each row's column conveys lifecycle phase (Tasks, Requests); use flat scrollable list for queues without explicit phase semantics (Escalations, Approvals)

Use useEmptyStateProps({ filteredCount, totalCount, filterActive, empty, filtered }) from @/hooks/use-empty-state-props to return EmptyStateProps | null so pages branch on a single value instead of duplicating the "no data ever" / "no data after filter" discriminator

Files:

  • web/src/pages/messages/ChannelListItem.stories.tsx
  • web/src/pages/messages/ChannelSidebar.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/messages/ChannelListItem.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/pages/budget/AgentSpendingTable.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/UsersPage.tsx
web/src/**/*.stories.tsx

📄 CodeRabbit inference engine (web/CLAUDE.md)

Use Storybook 10 which is ESM-only; essentials are built into core, but @storybook/addon-docs is now separate; imports moved to storybook/test and storybook/actions

Files:

  • web/src/pages/messages/ChannelListItem.stories.tsx
web/src/stores/**/*.ts

📄 CodeRabbit inference engine (web/CLAUDE.md)

All store mutation actions (create / update / delete) follow the stores/connections/crud-actions.ts pattern: wrap API calls in try / catch, success path updates state + emits success toast, failure path logs + emits error toast + returns sentinel (null for entity returns, false for delete); callers MUST NOT wrap store mutation calls in try / catch

Capture previous state synchronously for optimistic mutations and restore in catch block on failure

For list reads (fetch*), set error: string | null on the store instead of toasting

Use cursor-based pagination via PaginationMeta with opaque cursors; keep nextCursor + hasMore in state (not offset arithmetic); early-return when !hasMore || !nextCursor; display counts come from data.length not wire envelope total

If a store schedules timers or attaches event listeners, expose an equivalent cleanup hook for registration in the global test afterEach

For any new WebSocket payload handler that ingests untrusted strings, route through sanitizeWsString() or sanitizeWsEnum<T>(); raw (sanitizeWsString(x, n) ?? '') as EnumType casts are forbidden

Files:

  • web/src/stores/providers/crud-actions.ts
  • web/src/stores/tasks.ts
  • web/src/stores/messages.ts
web/src/components/ui/**/*.tsx

📄 CodeRabbit inference engine (web/CLAUDE.md)

ALWAYS reuse existing components from web/src/components/ui/ before creating new ones; NEVER hardcode hex colors, font-family declarations, pixel spacing, Motion transition durations, BCP 47 locale literals ('en-US'), or currency symbols / codes; use design tokens, @/lib/motion presets, helpers from @/utils/format, and DEFAULT_CURRENCY from @/utils/currencies

Every shared UI component must have a Props interface named <ComponentName>Props exported from the same file (e.g. AgentCardProps in agent-card.tsx) to make the contract greppable and let callers extend props

Base UI primitives are imported directly from @base-ui/react/<subpath> and use the native render prop for polymorphism; the local <Slot> helper is reserved only for <Button asChild>

Files:

  • web/src/components/ui/error-boundary.tsx
web/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Override MSW handlers per-case via server.use(...), never use vi.mock('@/api/endpoints/*')

Boot test setup with onUnhandledRequest: 'error' to catch unhandled MSW requests

Files:

  • web/src/__tests__/pages/messages/ChannelListItem.test.tsx
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T14:16:41.934Z
Learning: Use `<StatusBadge>` component (defaults to `role="img"` with aria-label; `decorative` for adjacent-labeled, `announce` for live WS updates) instead of recreating status dots inline
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T14:16:41.934Z
Learning: Use `<MetricCard>`, `<Sparkline>`, `<ProgressGauge>`, `<TokenUsageBar>` instead of recreating KPI displays inline
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T14:16:41.934Z
Learning: Use `<SectionCard>` (titled wrapper with icon and action slot), `<AgentCard>`, `<DeptHealthBar>` instead of recreating cards inline
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T14:16:41.934Z
Learning: Use `<InputField>`, `<SelectField>`, `<SliderField>`, `<ToggleField>`, `<SegmentedControl>`, `<TagInput>`, `<SearchInput>` instead of recreating form fields inline
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T14:16:41.934Z
Learning: Use `<Drawer width="compact|narrow|default|wide">` (Base UI) for slide-in panels; do NOT add inline `w-[40vw]` overrides
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T14:16:41.934Z
Learning: Use `<Skeleton>` family, `<EmptyState>`, `<ErrorBoundary>`, `<ErrorBanner>`, `<ProgressIndicator>` instead of recreating loading / empty / error states inline
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T14:16:41.934Z
Learning: Use `STATUS_COLORS` family from `@/styles/status-colors` (typed `Record<EnumValue, string>` lookups) instead of inline `Record<EnumValue, string>` constants per page
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T14:16:41.934Z
Learning: Use `<ConfirmDialog>` and `<Toast>` (Zustand-backed queue, NOT Base UI's Toast) for confirmations and notifications
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T14:16:41.934Z
Learning: Use `<CommandPalette>`, `<KeyboardShortcutHint>`, `<CommandCheatsheet>` instead of recreating Cmd+K / shortcuts functionality inline
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T14:16:41.934Z
Learning: Use `<AnimatedPresence>`, `<StaggerGroup>`, `<LiveRegion>` (debounced ARIA live for WS updates) for animation and live region updates
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T14:16:41.934Z
Learning: Do NOT adopt Base UI Toast component; use Zustand-backed queue instead
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T14:16:41.934Z
Learning: Do NOT adopt Base UI Meter component; use `<ProgressGauge>` instead
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T14:16:41.934Z
Learning: Do NOT adopt Base UI Select component; use native `<select>` for mobile picker UX
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T14:16:41.934Z
Learning: Do NOT currently adopt Base UI Combobox, Autocomplete, OTP Field, or Tooltip; revisit when needed
🔇 Additional comments (26)
web/src/components/layout/StatusBar.tsx (1)

37-43: A11y contract wording is now robust and accurate.

Good update—this avoids brittle line-number references and correctly documents the required visible-label + dot pairing with button aria-label, matching the implementation below.

web/src/pages/workflow-editor/useWorkflowEditorCallbacks.ts (1)

122-131: Pluralisation and conditional toast description look correct.

This cleanly resolves the prior error(s) UI copy issue and keeps the success toast concise.

web/src/components/ui/error-boundary.tsx (1)

100-100: Good fix: retry actions are now safe and consistent.

Adding type="button" on all “Try Again” controls removes unintended form-submit behaviour, and standardising on the shared Button keeps fallback UX consistent across levels.

Also applies to: 113-115, 133-135

web/src/pages/setup/WizardNavigation.tsx (1)

37-72: A11y linkage is correctly implemented for disabled “Next” reasoning.

useId() + conditional aria-describedby on the Next button, paired with a conditionally rendered <p id={reasonId}>, cleanly solves the screen-reader association without dangling ID references.

web/src/pages/UsersPage.tsx (4)

29-50: Good centralisation of role badge colour mapping.

Using ROLE_BADGE_COLORS[role] here keeps role styling consistent and avoids per-page colour drift.

Based on learnings: Use STATUS_COLORS family from @/styles/status-colors (typed Record<EnumValue, string> lookups) instead of inline Record<EnumValue, string> constants per page.


109-113: List-page header and container now align with page conventions.

Nice update to space-y-section-gap and ListHeader.description; this matches the shared list-page pattern.

As per coding guidelines: web/src/pages/**/*.tsx: root container uses space-y-section-gap and pages with one-line mission statements pass via <ListHeader description="..." />.


127-137: Search UI now correctly uses the list-page primitive wrapper.

SearchFilterSort + SearchInput integration looks clean and consistent with the page system.

As per coding guidelines: web/src/pages/**/*.tsx: Use <SearchFilterSort> for list-page primitives.


162-166: Responsive card-grid transition is solid.

Keeping ul/li semantics while adding md/lg density scaling is a good UX improvement.

.github/ci/web-async-leaks.max (1)

1-23: Well-documented threshold increase.

The ceiling adjustment is properly justified, with clear accounting for the +14 new tests (5 + 1 + 8), transparent local-vs-CI baseline explanation (~75 local → ~95-96 CI), and a concrete plan to reduce the threshold once issue #1468 lands. The mechanical nature of the increase (new test surface rather than regression) is well-explained.

web/src/stores/providers/crud-actions.ts (1)

80-80: Good consistency win on CRUD toast titles.

Using getCrudErrorTitle(...) across these mutation failures aligns provider CRUD behaviour with the shared error-title mapping and keeps 409/validation/auth messaging consistent.

Also applies to: 103-103, 130-130, 164-164

web/src/pages/budget/AgentSpendingTable.tsx (2)

49-49: Locale-aware name sorting looks good.

Using localeCompare with getLocale() is a solid improvement for user-consistent ordering.


148-148: The md:min-w-[40rem] guard is the right responsive adjustment.

This keeps small-screen behavior fluid while preserving table readability at medium+ widths.

web/src/pages/setup/SkipWizardForm.tsx (2)

30-46: Well-structured error handling for store mutations.

The implementation correctly addresses the store-mutation boundary:

  • Line 30 ensures the store has the trimmed name before submitCompany() reads it via get().companyName
  • Lines 39-46 properly inspect durable store state (companyResponse, companyError) rather than relying on exceptions
  • The null-coalescing fallback message at line 43 handles the edge case where companyError isn't set

This follows the pattern where the store owns error state and callers branch on that state rather than wrapping in try/catch.


55-63: Catch block correctly scoped to completion failures only.

Since submitCompany() never throws (per the store implementation), the catch block now handles only wizardCompleteSetup() failures. The partial-success messaging correctly indicates that the company exists and provides actionable recovery guidance via the wizard's Complete step.

web/src/stores/tasks.ts (4)

6-13: LGTM!

The imports are correctly updated to bring in sanitizeWsEnum from the dedicated utility module and TASK_SOURCE_VALUES from the enum definitions.


35-38: LGTM!

The updated comments clearly document the design decision to defer enum validation to sanitizeWsEnum rather than the shape guard, enabling graceful handling of new backend enum values during rolling deploys.


174-180: LGTM!

The source field correctly preserves undefined/null signals before applying sanitizeWsEnum, maintaining the optional semantics of the field.


260-316: LGTM!

The shape guard correctly differentiates between:

  • Display-facing enum fields (status, priority, type, source) that accept any string and defer to sanitizeWsEnum for forward compatibility
  • Behavioural enum fields (estimated_complexity, task_structure, coordination_topology) that retain strict allowlist validation because they drive orchestration logic

The comments clearly document this design rationale.

web/src/api/types/messages.ts (3)

15-19: LGTM!

The MESSAGE_TYPE_VALUES array correctly enumerates all values from the MessageType union with the as const satisfies pattern ensuring compile-time synchronisation.


23-25: LGTM!

The MESSAGE_PRIORITY_VALUES array correctly captures all priority levels.


29-31: LGTM!

The ATTACHMENT_TYPE_VALUES array correctly enumerates all attachment types.

web/src/stores/messages.ts (4)

6-14: LGTM!

The sanitiser imports are correctly sourced from the dedicated utility module, and the enum value arrays are imported from the types definition. The comment at lines 6-8 helpfully documents the rationale for this structure.


128-139: LGTM!

The sanitizeWsEnum calls use sensible neutral fallbacks: 'announcement' for message type and 'normal' for priority. The field options enable structured warning output for observability.


155-161: LGTM!

The attachment type is correctly sanitised via sanitizeWsEnum with 'file' as a sensible neutral fallback.


186-197: LGTM!

The returned Message object correctly uses the enum-sanitised values directly without type casts, as sanitizeWsEnum returns the properly typed value from the allowlist.

web/src/pages/WebhookReceiptsPage.tsx (1)

194-201: Good move on the error-banner placement.

This now matches the list-page layout pattern and keeps the retry affordance above the filters.

As per coding guidelines, "<ErrorBanner> lands immediately after <ListHeader> before filter/pagination".

Comment thread web/src/pages/approvals/ApprovalCard.tsx Outdated
Comment thread web/src/stores/providers/crud-actions.ts Outdated
…out (#1710)

Round 3 of CodeRabbit findings on PR #1718 plus a lazy-chart test
flake exposed by the round-2 ceiling raise.

CodeRabbit findings:
* providers/crud-actions.ts: every log.error metadata object now
  routes attacker-controlled name / preset_name through
  sanitizeForLog from @/utils/logging. The values flow from the
  CRUD form (operator-supplied) into structured logs; without
  sanitization, an embedded control / bidi-override sequence in a
  provider name would survive into log sinks. createProvider /
  createFromPreset / updateProvider / deleteProvider all updated.
* WebhookReceiptsPage: ``selected`` now seeds from '' instead of
  urlConnection. The seed effect immediately below already validates
  that the URL value exists in the connections list before applying
  it, so the previous direct-from-URL initialisation defeated the
  validation by triggering reload() on the first render against an
  unvalidated name. Initialising to '' lets the seed effect own the
  validation path on every code path (cold mount + later URL
  changes).
* messages.ts: ``!to`` added to the blank-rejection guard. The
  shape guard at line 80 already requires ``typeof c.to === 'string'``
  and parseWsMessage's contract docstring lists ``to`` as a required
  string, so a sanitized-blank ``to`` should reject the whole frame
  the same way a blank id / sender / channel does. The
  hasBlankTo flag is also surfaced in the structured log so the
  WS-drift signal points at the right field.
* ApprovalCard countdown: reset + interval-restart collapsed into
  ONE useEffect keyed on [seconds_remaining, isPending] so a refresh
  from the WS layer (e.g. seconds_remaining bumped 5 -> 60) restarts
  the interval on a clean 1-second cadence instead of inheriting the
  previous interval's mid-tick offset. setCountdown is deferred to a
  microtask per the ESLint set-state-in-effect rule; setInterval
  starts immediately so the visible tick cadence matches the prop.

Lazy-chart test flake:
* BudgetPage.test.tsx: 'renders Spend Burn section' failed once
  under heavy parallel load at 5860ms. SpendBurnChart and
  CostBreakdownChart are React.lazy + Suspense (the recharts
  split-chunk landed in this PR), so the dynamic import races the
  test's default 5s testTimeout when 8 workers are fighting for CPU.
  Bumped the findByText timeout 5000 -> 10000 AND added a 15000ms
  per-test timeout (third arg on it()) so Vitest's testTimeout
  doesn't kill the case before findByText completes. The comment
  block above the tests was updated to reflect the new threshold.

Verification:
* npm run lint: zero warnings
* npm run type-check: clean
* 2994 tests pass; local 75 leaks; under 100 ceiling.
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview May 2, 2026 15:01 — with GitHub Actions Inactive
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/src/pages/WebhookReceiptsPage.tsx (1)

71-105: ⚠️ Potential issue | 🔴 Critical

Fix the race condition where the URL deep-link (?connection=) is overwritten by the reconciliation effect on cold mount.

When connections load for the first time with a URL parameter like ?connection=SecondConnection, both effects schedule microtask updates:

  • Effect 1 (URL sync) schedules setSelected('SecondConnection') via functional update
  • Effect 2 (reconciliation) schedules setSelected(connections[0]?.name) via direct update

Both microtasks execute in the same batch; the direct update runs after the functional update and overwrites it, causing the URL deep-link to be lost and the first connection to be selected instead.

Add urlConnection to the reconciliation effect's dependency array (line 105) and skip the override when a valid URL-seeded connection exists:

}, [connections, selected, urlConnection])

Then modify the effect body to check:

const exists = selected !== '' && connections.some((c) => c.name === selected)
if (exists) return
// Only default to first connection if no URL param is actively forcing a different choice
if (urlConnection && connections.some((c) => c.name === urlConnection)) return
let cancelled = false
void Promise.resolve().then(() => {
  if (cancelled) return
  setSelected(connections[0]?.name ?? '')
})

This ensures the reconciliation effect respects the URL-seeded value instead of clobbering it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/WebhookReceiptsPage.tsx` around lines 71 - 105, The
reconciliation useEffect that defaults selected to the first connection can
clobber a URL-seeded value; update its dependency array to include urlConnection
and guard the effect so it returns early when a valid urlConnection exists
(i.e., when urlConnection is non-empty and connections.some(c => c.name ===
urlConnection)), then keep the existing microtask
setSelected(connections[0]?.name ?? '') behavior only when no URL-forced
connection applies; reference the existing useEffect handling
connections/selected, the urlConnection variable, and the setSelected state
updater to implement this change.
♻️ Duplicate comments (1)
web/src/pages/approvals/ApprovalCard.tsx (1)

61-86: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard the countdown against one last stale interval tick.

Line 76 clears future ticks, but it does not cancel a callback already queued from the previous interval. On a WS refresh that lands near the tick boundary, that stale callback can still run after setCountdown(approval.seconds_remaining) and immediately drop the refreshed value by one.

💡 Minimal fix
+  const countdownGenerationRef = useRef(0)
+
   useEffect(() => {
+    const generation = ++countdownGenerationRef.current
     let cancelled = false
     void Promise.resolve().then(() => {
-      if (cancelled) return
+      if (cancelled || generation !== countdownGenerationRef.current) return
       setCountdown(approval.seconds_remaining)
     })
     if (
       !isPending
       || approval.seconds_remaining === null
       || approval.seconds_remaining <= 0
     ) {
       return () => {
         cancelled = true
       }
     }
     const timer = setInterval(() => {
+      if (generation !== countdownGenerationRef.current) return
       setCountdown((prev) => {
         if (prev === null || prev <= 1) return 0
         return prev - 1
       })
     }, 1000)
🤖 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 61 - 86, The
interval's queued callback can still run and decrement a freshly-set value; fix
by guarding both the microtask and the interval callback with the effect-local
cancelled flag: inside the Promise.resolve().then() only call setCountdown if
cancelled is false, and at the start of the setInterval handler return early
when cancelled is true before calling setCountdown((prev) => ...). Update the
effect in ApprovalCard so the local cancelled boolean is checked in both places
(the immediate Promise callback and the timer callback) to prevent a stale tick
from mutating the refreshed approval.seconds_remaining.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@web/src/pages/WebhookReceiptsPage.tsx`:
- Around line 71-105: The reconciliation useEffect that defaults selected to the
first connection can clobber a URL-seeded value; update its dependency array to
include urlConnection and guard the effect so it returns early when a valid
urlConnection exists (i.e., when urlConnection is non-empty and
connections.some(c => c.name === urlConnection)), then keep the existing
microtask setSelected(connections[0]?.name ?? '') behavior only when no
URL-forced connection applies; reference the existing useEffect handling
connections/selected, the urlConnection variable, and the setSelected state
updater to implement this change.

---

Duplicate comments:
In `@web/src/pages/approvals/ApprovalCard.tsx`:
- Around line 61-86: The interval's queued callback can still run and decrement
a freshly-set value; fix by guarding both the microtask and the interval
callback with the effect-local cancelled flag: inside the
Promise.resolve().then() only call setCountdown if cancelled is false, and at
the start of the setInterval handler return early when cancelled is true before
calling setCountdown((prev) => ...). Update the effect in ApprovalCard so the
local cancelled boolean is checked in both places (the immediate Promise
callback and the timer callback) to prevent a stale tick from mutating the
refreshed approval.seconds_remaining.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 144cbb75-06ce-4368-be57-0c2b005c940c

📥 Commits

Reviewing files that changed from the base of the PR and between ab97e97 and da9e61f.

📒 Files selected for processing (5)
  • web/src/__tests__/pages/BudgetPage.test.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/stores/messages.ts
  • web/src/stores/providers/crud-actions.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). (7)
  • GitHub Check: Deploy Preview
  • GitHub Check: CodSpeed Web benchmarks
  • GitHub Check: Build Web Assets (melange)
  • GitHub Check: Dashboard Test
  • GitHub Check: Lighthouse Site
  • GitHub Check: Lighthouse Dashboard
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (13)
**/*.{py,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{py,ts,tsx}: No default may privilege a single region, currency, or locale. Every user-facing format resolves from: user/company setting -> browser/system -> neutral fallback.
Currency: never hardcode ISO 4217 codes or symbols. Backend: DEFAULT_CURRENCY from synthorg.budget.currency or the runtime budget.currency setting. Frontend: DEFAULT_CURRENCY from @/utils/currencies or useSettingsStore().currency.
Field naming: no _usd suffix on money fields anywhere. The type carries money semantics; the value is in the operator's configured currency.
Locale: never hardcode BCP 47 tags or call bare .toLocaleString() / .toLocaleDateString() / .toLocaleTimeString(). Use helpers in @/utils/format which read getLocale() from @/utils/locale. The backend has no operator-tunable locale setting; backend Intl formatting uses the system locale plus the browser timezone. The company.name_locales list controls procedural-name generation only; it does not feed number / date / time formatting.
Timezone: store UTC; render via Intl without passing timeZone (browser tz wins).
Date / number format: always via Intl; no hand-rolled templates.

Files:

  • web/src/__tests__/pages/BudgetPage.test.tsx
  • web/src/stores/providers/crud-actions.ts
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/stores/messages.ts
  • web/src/pages/approvals/ApprovalCard.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Units: metric only. Spelling: International / British English UI default (colour, behaviour, organise, centred, analyse, cancelled); document deviations. Spelling here is an editorial / UI-copy decision only; it does not affect runtime locale-sensitive formatting. Numbers, dates, times, currencies, and units still resolve via the user / company / browser / system fallback through @/utils/format, @/utils/locale, DEFAULT_CURRENCY, and useSettingsStore().currency, with no contradiction to the locale-neutral defaults above.

Files:

  • web/src/__tests__/pages/BudgetPage.test.tsx
  • web/src/stores/providers/crud-actions.ts
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/stores/messages.ts
  • web/src/pages/approvals/ApprovalCard.tsx
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Reuse components from web/src/components/ui/ before creating new ones. Never hardcode hex colors, font-family, pixel spacing, Motion transitions, or BCP 47 locale strings; use design tokens, @/lib/motion presets, and the helpers in @/utils/format. Enforced by scripts/check_web_design_system.py (PostToolUse hook on every web/src/ edit).

Always use createLogger from @/lib/logger and never use bare console.warn/console.error/console.debug in application code

Variable name for logger instances must always be log (e.g., const log = createLogger('module-name'))

Pass dynamic/untrusted values as separate arguments to logger methods rather than interpolating them into the message string to ensure they go through sanitizeArg

Wrap attacker-controlled fields inside structured objects with sanitizeForLog() before embedding them in log calls

Files:

  • web/src/__tests__/pages/BudgetPage.test.tsx
  • web/src/stores/providers/crud-actions.ts
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/stores/messages.ts
  • web/src/pages/approvals/ApprovalCard.tsx
web/src/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Test setup must boot with onUnhandledRequest: 'error' and override per-case via server.use(...), never vi.mock('@/api/endpoints/*')

Files:

  • web/src/__tests__/pages/BudgetPage.test.tsx
web/src/**/*.tsx

📄 CodeRabbit inference engine (web/CLAUDE.md)

Import Dialog, AlertDialog, Popover, Tabs, Menu, Drawer, and CSPProvider directly from @base-ui/react/<subpath> (adopted primitives)

Files:

  • web/src/__tests__/pages/BudgetPage.test.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
web/src/stores/**/*.ts

📄 CodeRabbit inference engine (web/CLAUDE.md)

All Zustand store mutation actions (create/update/delete) must wrap API calls in try/catch, emit success/error toasts on respective paths, and return sentinels (null for entity returns, false for delete operations)

Optimistic mutations in Zustand stores must capture previous state synchronously and restore it in the catch block

Any new store that schedules timers or attaches event listeners must expose a cleanup hook and register it in the global test afterEach

Files:

  • web/src/stores/providers/crud-actions.ts
  • web/src/stores/messages.ts
web/src/{api,stores}/**/*.ts

📄 CodeRabbit inference engine (web/CLAUDE.md)

List endpoints must use cursor-based pagination via PaginationMeta with nextCursor and hasMore in state; display counts must come from data.length rather than a wire total

Files:

  • web/src/stores/providers/crud-actions.ts
  • web/src/stores/messages.ts
web/src/{stores,utils}/**/*.ts

📄 CodeRabbit inference engine (web/CLAUDE.md)

Use sanitizeWsString() to clamp every WebSocket-supplied string (strips C0 controls + bidi-overrides + caps length)

Use sanitizeWsEnum<T>(value, allowlist, fallback, { field }) for enum-allowlist validation on WebSocket payloads with fallback for unknown values

Do not use raw (sanitizeWsString(x, n) ?? '') as EnumType casts; always route WebSocket payload handlers through sanitization helpers

Files:

  • web/src/stores/providers/crud-actions.ts
  • web/src/stores/messages.ts
web/src/{components,pages,hooks}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Callers MUST NOT wrap Zustand store mutation calls in try/catch; error handling and UX belongs entirely in the store

Files:

  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
web/src/{components,pages,styles}/**/*.{ts,tsx,css}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Never hardcode hex colors, font-family declarations, pixel spacing, Motion transition durations, BCP 47 locale literals, or currency symbols/codes; use design tokens, @/lib/motion presets, @/utils/format helpers, and DEFAULT_CURRENCY

Files:

  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
web/src/{components,pages}/**/*.tsx

📄 CodeRabbit inference engine (web/CLAUDE.md)

Use <StatusBadge> for status dots (defaults to role="img" with aria-label; decorative for adjacent-labeled, announce for live WebSocket updates)

Use <MetricCard>, <Sparkline>, <ProgressGauge>, and <TokenUsageBar> for KPI displays instead of creating inline versions

Use <SectionCard> for titled card wrappers with icon and action slots; use <AgentCard> and <DeptHealthBar> for domain-specific cards

Use <InputField>, <SelectField>, <SliderField>, <ToggleField>, <SegmentedControl>, <TagInput>, and <SearchInput> for form fields instead of custom implementations

Use <Drawer width="compact|narrow|default|wide"> for slide-in panels from Base UI; do NOT add inline w-[40vw] overrides

Use <Skeleton> family, <EmptyState>, <ErrorBoundary>, <ErrorBanner>, and <ProgressIndicator> for loading/empty/error states instead of inline versions

Use useEmptyStateProps({ filteredCount, totalCount, filterActive, empty, filtered }) from @/hooks/use-empty-state-props to branch on empty states instead of duplicating the 'no data ever' / 'no data after filter' discriminator

Use <ConfirmDialog> and <Toast> (Zustand-backed queue) for confirmations and notifications; do NOT use Base UI's Toast primitive

Use <CommandPalette>, <KeyboardShortcutHint>, and <CommandCheatsheet> for command palette and keyboard shortcuts instead of inline implementations

Use <AnimatedPresence>, <StaggerGroup>, and <LiveRegion> (debounced ARIA live for WebSocket updates) for animations and accessibility

Files:

  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
web/src/pages/**/*.tsx

📄 CodeRabbit inference engine (web/CLAUDE.md)

Use <ListHeader>, <SearchFilterSort>, <Pagination>, <BulkActionBar>, <MetadataGrid>, and <Breadcrumbs> for list-page primitives

List page root container must use space-y-section-gap class (prefer over flex flex-col gap-section-gap); place <ErrorBanner> immediately after <ListHeader> before filter/pagination rows; pass one-line mission statement via <ListHeader description="..." />

Files:

  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
web/src/{components,pages,styles}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Use typed STATUS_COLORS family from @/styles/status-colors for status/role/risk/urgency badge classes; never create inline Record<EnumValue, string> constants per page

Files:

  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T15:01:15.412Z
Learning: WebSocket wire protocol constants (`WS_PROTOCOL_VERSION`, `WS_MAX_MESSAGE_SIZE`, `WS_HEARTBEAT_INTERVAL_MS`, `WS_PONG_TIMEOUT_MS`, `LOG_SANITIZE_MAX_LENGTH`) must stay in lockstep with Python backend definitions in `src/synthorg/api/ws_models.py` and `src/synthorg/api/controllers/ws.py`
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T15:01:15.412Z
Learning: Always reuse existing components from `web/src/components/ui/` before creating new ones
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T15:01:15.412Z
Learning: Choose Kanban grouping for status-flow domains where each row's column conveys lifecycle phase (Tasks, Requests); use flat scrollable lists for queues without explicit phase semantics (Escalations, Approvals)
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T15:01:15.412Z
Learning: Do not adopt Base UI Toast, Meter, Select, Combobox, Autocomplete, OTP Field, or Tooltip; use custom equivalents (Zustand-backed Toast, ProgressGauge, native select, etc.) or defer until needed
🔇 Additional comments (10)
web/src/__tests__/pages/BudgetPage.test.tsx (1)

124-145: Good stabilization for lazy-loaded chart assertions.

Switching these two checks to async findByText with explicit timeout and per-test budget is appropriate for React.lazy/Suspense rendering and should reduce CI flakiness without masking real regressions.

web/src/stores/providers/crud-actions.ts (1)

78-79: Sanitize error metadata in CRUD failure logs.

getErrorMessage(err) is still logged unsanitized in the structured metadata payload across all four catch blocks.

Suggested patch
-        log.error('createProvider failed:', { name: sanitizeForLog(data.name), error: getErrorMessage(err) })
+        log.error('createProvider failed:', {
+          name: sanitizeForLog(data.name),
+          error: sanitizeForLog(getErrorMessage(err)),
+        })

-        log.error('createFromPreset failed:', {
-          name: sanitizeForLog(data.name),
-          preset: sanitizeForLog(data.preset_name),
-          error: getErrorMessage(err),
-        })
+        log.error('createFromPreset failed:', {
+          name: sanitizeForLog(data.name),
+          preset: sanitizeForLog(data.preset_name),
+          error: sanitizeForLog(getErrorMessage(err)),
+        })

-        log.error('updateProvider failed:', { name: sanitizeForLog(name), error: getErrorMessage(err) })
+        log.error('updateProvider failed:', {
+          name: sanitizeForLog(name),
+          error: sanitizeForLog(getErrorMessage(err)),
+        })

-        log.error('deleteProvider failed:', { name: sanitizeForLog(name), error: getErrorMessage(err) })
+        log.error('deleteProvider failed:', {
+          name: sanitizeForLog(name),
+          error: sanitizeForLog(getErrorMessage(err)),
+        })

As per coding guidelines web/src/**/*.{ts,tsx}: “Wrap attacker-controlled fields inside structured objects with sanitizeForLog() before embedding them in log calls”.

Also applies to: 101-105, 132-133, 166-167

web/src/stores/messages.ts (5)

6-14: LGTM!

The import refactoring correctly addresses the previous review feedback by sourcing sanitizers from the dedicated utility module rather than @/stores/notifications, eliminating cross-store coupling and circular-init risk. The allowlist imports from @/api/types/messages provide the typed constants needed for enum validation.


128-139: LGTM!

The migration from raw as casts to sanitizeWsEnum correctly implements the typed sanitization pattern. The fallback values ('announcement' for type, 'normal' for priority) are valid members of their respective allowlists and represent safe UI defaults. The field option provides diagnostic context for observability when unknown values are encountered.


141-148: LGTM!

The rejection condition correctly reflects that type and priority now have guaranteed fallbacks via sanitizeWsEnum, so only core identity and routing fields require non-blank validation. Adding hasBlankTo to the error context improves diagnostic completeness.


156-162: LGTM!

Consistent application of sanitizeWsEnum for attachment types with 'file' as a safe neutral fallback. The field name 'message.attachments[].type' clearly indicates array element context for debugging.


187-198: LGTM!

The explicit object construction correctly uses the typed values returned by sanitizeWsEnum without requiring unsafe casts. The comment at lines 183-186 correctly explains why a spread is avoided.

web/src/pages/WebhookReceiptsPage.tsx (3)

152-155: Well-structured sanitisation for attacker-controlled fields.

The connection name originates from URL/dropdown (operator-controlled) and is correctly wrapped with sanitizeForLog before structured logging, aligning with security guidelines.


191-203: LGTM — ErrorBanner placement and container layout follow conventions.

  • Root container uses space-y-section-gap as required
  • <ErrorBanner> placed immediately after <ListHeader> and before filter rows
  • Retry handler correctly wires to reload()

256-265: Accessible status rendering with decorative badge.

StatusBadge uses decorative since the adjacent <span> provides the visible text label ({row.status}). This prevents redundant screen-reader announcements while maintaining the visual indicator.

Round 4 of CodeRabbit findings on PR #1718. CI was already green for
da9e61f; this round only addresses the new review (id 4215155295)
that landed at 15:08 UTC.

CodeRabbit findings:
* WebhookReceiptsPage URL deep-link race (CRITICAL): the
  reconciliation effect (defaults selected -> connections[0]) could
  fire alongside the URL-seed effect on cold mount. Both schedule
  microtask updates; the direct setter in the reconciliation effect
  could land after the seed effect's functional setter and clobber
  the URL-forced choice. Now: ``urlConnection`` is in the dep array
  and the effect early-returns when a valid URL value exists in the
  loaded list, so the seed effect retains exclusive ownership of
  the URL-driven path.
* ApprovalCard countdown stale-tick guard (MINOR): the existing
  ``cancelled`` flag was checked in the microtask but not in the
  setInterval callback. A tick that fires between the prop refresh
  and the cleanup running could decrement a freshly-set countdown
  by one. Added the same ``cancelled`` early-return at the top of
  the timer callback so the stale tick is a no-op once the new
  effect's cleanup has fired.
* providers/crud-actions error metadata sanitization (MAJOR):
  round-3 wrapped name / preset_name with sanitizeForLog but left
  ``error: getErrorMessage(err)`` raw across all four catch blocks.
  Backend error strings can echo operator-supplied input back to
  the caller, so the message is itself attacker-reachable. All four
  call sites (createProvider, createFromPreset, updateProvider,
  deleteProvider) now route the error through sanitizeForLog as
  well.

Verification:
* npm run lint: zero warnings
* npm run type-check: clean
* 2994 tests pass; local 75 leaks; under 100 ceiling.
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview May 2, 2026 15:30 — with GitHub Actions Inactive
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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/pages/approvals/ApprovalCard.tsx`:
- Around line 79-89: The interval never gets cleared when countdown reaches 0;
update the effect in ApprovalCard (the setInterval/timer logic using cancelled
and setCountdown) to avoid creating the timer when countdown <= 0 or add a
separate useEffect watching countdown to clearInterval(timer) when it becomes 0;
specifically, reference the existing timer, setInterval, setCountdown and
countdown variables: either early-return from the effect if countdown <= 0 (so
no timer is created) or store the timer ID in a ref and
clearInterval(ref.current) inside a useEffect that triggers when countdown
changes to 0, and remove clearing logic from inside the state updater to
preserve React purity.
🪄 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: ebeb44d1-0ed2-4084-bc86-6b0f39346273

📥 Commits

Reviewing files that changed from the base of the PR and between da9e61f and c450e68.

📒 Files selected for processing (3)
  • web/src/pages/WebhookReceiptsPage.tsx
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/stores/providers/crud-actions.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). (7)
  • GitHub Check: Deploy Preview
  • GitHub Check: Lighthouse Site
  • GitHub Check: Lighthouse Dashboard
  • GitHub Check: Build Web Assets (melange)
  • GitHub Check: CodSpeed Web benchmarks
  • GitHub Check: Dashboard Test
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{py,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{py,ts,tsx}: No default may privilege a single region, currency, or locale. Every user-facing format resolves from: user/company setting -> browser/system -> neutral fallback.
Currency: never hardcode ISO 4217 codes or symbols. Backend: DEFAULT_CURRENCY from synthorg.budget.currency or the runtime budget.currency setting. Frontend: DEFAULT_CURRENCY from @/utils/currencies or useSettingsStore().currency.
Field naming: no _usd suffix on money fields anywhere. The type carries money semantics; the value is in the operator's configured currency.
Locale: never hardcode BCP 47 tags or call bare .toLocaleString() / .toLocaleDateString() / .toLocaleTimeString(). Use helpers in @/utils/format which read getLocale() from @/utils/locale. The backend has no operator-tunable locale setting; backend Intl formatting uses the system locale plus the browser timezone. The company.name_locales list controls procedural-name generation only; it does not feed number / date / time formatting.
Timezone: store UTC; render via Intl without passing timeZone (browser tz wins).
Date / number format: always via Intl; no hand-rolled templates.

Files:

  • web/src/stores/providers/crud-actions.ts
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Units: metric only. Spelling: International / British English UI default (colour, behaviour, organise, centred, analyse, cancelled); document deviations. Spelling here is an editorial / UI-copy decision only; it does not affect runtime locale-sensitive formatting. Numbers, dates, times, currencies, and units still resolve via the user / company / browser / system fallback through @/utils/format, @/utils/locale, DEFAULT_CURRENCY, and useSettingsStore().currency, with no contradiction to the locale-neutral defaults above.

Files:

  • web/src/stores/providers/crud-actions.ts
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Reuse components from web/src/components/ui/ before creating new ones. Never hardcode hex colors, font-family, pixel spacing, Motion transitions, or BCP 47 locale strings; use design tokens, @/lib/motion presets, and the helpers in @/utils/format. Enforced by scripts/check_web_design_system.py (PostToolUse hook on every web/src/ edit).

A PostToolUse hook (scripts/check_web_design_system.py) validates every web/src/ edit and flags hardcoded hex/rgba/fonts/Motion durations/locale literals/bare .toLocale*String() calls/missing Storybook stories/duplicate component patterns/complex .map() blocks; fix every violation before proceeding

Files:

  • web/src/stores/providers/crud-actions.ts
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Every inline <style> tag injected by Base UI and Motion must carry the per-request CSP nonce via <CSPProvider nonce={getCspNonce()}> and <MotionConfig nonce> wrapping in App.tsx

Use Base UI's render prop as the polymorphism primitive throughout the dashboard; only use the local <Slot> helper from components/ui/slot.tsx with @base-ui/react/merge-props for <Button asChild> ergonomic

Always use createLogger from @/lib/logger with variable name log; never use bare console.warn, console.error, or console.debug in application code (only logger.ts itself may use bare console methods)

Pass dynamic/untrusted values as separate args to logger methods (not interpolated into the message string) so they go through sanitizeArg; wrap attacker-controlled fields inside structured objects with sanitizeForLog() before embedding

All store mutation actions (create / update / delete) must follow the stores/connections/crud-actions.ts pattern: wrap API call in try / catch, success path updates state + emits success toast, failure path logs + emits error toast + returns sentinel (null for entity returns, false for delete)

Callers MUST NOT wrap store mutation calls in try / catch; the store owns the error UX

Optimistic mutations must capture previous state synchronously and restore in catch

List reads (fetch*) must set error: string | null on the store instead of toasting

Cursor pagination is mandatory: list endpoints use opaque cursor-based paging via PaginationMeta; stores keep nextCursor + hasMore in state (not offset arithmetic) and early-return when !hasMore || !nextCursor; display counts come from data.length

getLiveness() is always 200 while the process is alive; getReadiness() is 200 healthy / 503 unavailable (binary 'ok' | 'unavailable' outcome); any new caller must handle the 503 path explicitly

web/src/mocks/handlers/ must mirror web/src/api/endpoints/*.ts 1:1 with a default happy-path handler...

Files:

  • web/src/stores/providers/crud-actions.ts
  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
web/**/*pages/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Use <ListHeader> / <SearchFilterSort> / <Pagination> / <BulkActionBar> / <MetadataGrid> / <Breadcrumbs> for list-page primitives; root container uses space-y-section-gap; <ErrorBanner> lands immediately after <ListHeader>; pages with one-line mission statement pass it via <ListHeader description="..." />

For list pages, choose Kanban grouping for status-flow domains where each row's column conveys lifecycle phase (Tasks, Requests); use flat scrollable list for queues without explicit phase semantics (Escalations, Approvals)

Files:

  • web/src/pages/approvals/ApprovalCard.tsx
  • web/src/pages/WebhookReceiptsPage.tsx
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T15:29:29.802Z
Learning: Toast, Meter, Select, Combobox / Autocomplete / OTP Field / Tooltip are not adopted from Base UI; we use alternatives (Zustand-backed Toast queue, `<ProgressGauge>` for Meter, native `<select>` for Select, no current call sites for others)
Learnt from: CR
Repo: Aureliolo/synthorg

Timestamp: 2026-05-02T15:29:29.802Z
Learning: Minimum Node 20.19+, Vite 5+, Vitest 3+ are required; update tool versions in package.json and CI workflows accordingly
🔇 Additional comments (3)
web/src/stores/providers/crud-actions.ts (1)

16-17: Good cleanup: failure paths now follow the required logging and toast contract.

The updated catch blocks correctly log through log.error, sanitize attacker-controlled fields, apply getCrudErrorTitle(...), and return the expected sentinels.

Also applies to: 78-85, 104-112, 135-142, 172-179

web/src/pages/WebhookReceiptsPage.tsx (2)

50-113: Deep-link reconciliation looks solid.

The URL-seeded selection and fallback effect now co-ordinate correctly, so a valid ?connection= survives cold loads and mid-session query changes without being clobbered.


204-211: Error banner placement now matches the page layout contract.

Rendering <ErrorBanner> directly after <ListHeader> keeps the failure state consistent with the shared list-page pattern. As per coding guidelines, web/**/*pages/**/*.{ts,tsx}: "<ErrorBanner> lands immediately after <ListHeader>".

Comment thread web/src/pages/approvals/ApprovalCard.tsx Outdated
Round 5 of CodeRabbit findings on PR #1718. CI was already green for
c450e68; this round addresses the only new actionable finding from
review id 4215184268 / inline comment 3176836578.

CodeRabbit finding:
* ApprovalCard countdown leak (MAJOR): when ``countdown`` reached
  zero the setInterval callback kept firing every second
  indefinitely until the component unmounted or the prop refreshed.
  React bails on the no-op re-render (state stays at 0) but the
  timer itself continues, so an expired card kept mounted in the
  paginated approvals queue accumulates a dormant 1-Hz interval.
  Fix: stash the timer id in ``timerRef``, then clear it from a
  sibling effect that fires when ``countdown`` lands on zero.
  Mutating the timer from inside the ``setCountdown`` updater would
  violate React purity (state updaters must be side-effect free),
  so the cleanup lives in its own ``useEffect`` keyed on
  ``countdown`` instead.

Verification:
* npm run lint: zero warnings
* npm run type-check: clean
* 2994 tests pass; local 75 leaks; under 100 ceiling.
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview May 2, 2026 15:52 — with GitHub Actions Inactive
@Aureliolo Aureliolo merged commit 3a3591a into main May 2, 2026
75 checks passed
@Aureliolo Aureliolo deleted the fix/audit-cleanup-e-frontend branch May 2, 2026 16:09
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview May 2, 2026 16:09 — with GitHub Actions Inactive
Aureliolo pushed a commit that referenced this pull request May 3, 2026
<!-- HIGHLIGHTS_START -->
## Highlights

> _AI-generated summary (model: `openai/gpt-4.1-mini` via GitHub
Models). Commit-based changelog below._

### What you'll notice
- Frontend and UX polishing improves user interface responsiveness and
visual consistency.
- API hygiene and validation enhancements provide smoother and more
reliable interactions.

### What's new
- Introduced typed-boundary helpers enabling better type safety and
parse_typed workflows.
- Added codebase-audit skill prompt tuning for improved project
auditing.

### Under the hood
- Eliminated flaky tests caused by module-level state for more stable
test outcomes.
- Unified image tag management under CLI and Renovate for consistent
dependency updates.
- Added cross-PR file-overlap analysis to the review dependency pull
request skill.
- Updated multiple dependencies including Python, Web, CLI, and
container libraries.
- Improved CI tooling and lock file maintenance for better build
reliability.

<!-- HIGHLIGHTS_END -->

:robot: I have created a release *beep* *boop*
---


##
[0.7.8](v0.7.7...v0.7.8)
(2026-05-03)


### Features

* **api:** typed-boundary helper + codebase-audit skill prompt tuning
([#1712](#1712))
([40ee65b](40ee65b))
* **boundary:** RFC
[#1711](#1711) Phases 2 + 3
— typed boundaries via parse_typed
([#1720](#1720))
([7b9f409](7b9f409))


### Bug Fixes

* **api:** audit cleanup B -- API hygiene & validation
([#1719](#1719))
([3d790d9](3d790d9))
* audit cleanup C - persistence, concurrency & data integrity
([#1708](#1708))
([#1717](#1717))
([bcce097](bcce097))
* **test:** exterminate xdist-flaky tests with module-level state
([#1713](#1713))
([#1721](#1721))
([8d258dd](8d258dd))
* **web:** audit cleanup E -- frontend & UX polish
([#1710](#1710))
([#1718](#1718))
([3a3591a](3a3591a))


### Refactoring

* **cli:** single source of truth for DHI image tags + Renovate manager
([#1723](#1723))
([57980a2](57980a2))


### Documentation

* audit cleanup D -- public-facing & docs sync
([#1709](#1709))
([#1715](#1715))
([ade03b7](ade03b7))


### Tests

* **engine:** make TestDrainTimeout deterministic + preserve subclass
type in [@Ontology](https://github.com/ontology)_entity
([#1729](#1729))
([b00fb05](b00fb05))


### CI/CD

* Update CI tool dependencies
([#1703](#1703))
([355a9ff](355a9ff))


### Maintenance

* add cross-PR file-overlap analysis to review-dep-pr skill
([#1722](#1722))
([3861d8a](3861d8a))
* **ci:** unify apko-version under workflow env so Renovate manages it
everywhere ([#1724](#1724))
([9c0a7fd](9c0a7fd))
* consolidate DHI image-pin custom regex managers
([#1726](#1726))
([b8b0cba](b8b0cba))
* **deps:** update dependency chainguard-dev/melange to v0.50.4
([#1701](#1701))
([8cbf83a](8cbf83a))
* Lock file maintenance
([#1705](#1705))
([414cfea](414cfea))
* Lock file maintenance
([#1727](#1727))
([5cb1212](5cb1212))
* Update CLI dependencies
([#1702](#1702))
([9fb57b9](9fb57b9))
* Update Container dependencies
([#1698](#1698))
([6d24fd6](6d24fd6))
* Update dependency @eslint-react/eslint-plugin to v5
([#1704](#1704))
([1cb1294](1cb1294))
* Update Python dependencies
([#1699](#1699))
([8e7af3a](8e7af3a))
* Update Python dependencies to v4.15.0
([#1725](#1725))
([69164c8](69164c8))
* Update Web dependencies
([#1700](#1700))
([715300d](715300d))

---
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: synthorg-repo-bot[bot] <279117679+synthorg-repo-bot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Audit cleanup E: frontend & UX polish

1 participant