Skip to content

feat: Meetings page with timeline viz, token bars, contribution formatting (#788)#904

Merged
Aureliolo merged 6 commits intomainfrom
feat/meetings-page
Mar 28, 2026
Merged

feat: Meetings page with timeline viz, token bars, contribution formatting (#788)#904
Aureliolo merged 6 commits intomainfrom
feat/meetings-page

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

  • Meetings list page with horizontal timeline visualization, filterable card grid, summary metric cards, and trigger meeting dialog
  • Meeting detail page with agenda, token usage breakdown (per-participant stacked bars), formatted contribution dialogue grouped by phase, decisions, action items, and summary
  • TokenUsageBar shared component (components/ui/) -- reusable segmented horizontal meter bar with ARIA support
  • Zustand store with stale-request guards, WebSocket real-time updates, and polling
  • 16 Storybook stories for all visual components
  • Unit tests for utils, store, hooks, and both pages (1727 total, all passing)

Pre-reviewed by 5 agents (frontend-reviewer, issue-resolution-verifier, test-quality-reviewer, docs-consistency, silent-failure-hunter). 17 findings addressed in a follow-up commit.

Test plan

  • npm --prefix web run lint -- zero warnings
  • npm --prefix web run type-check -- zero errors
  • npm --prefix web run test -- 1727 tests pass
  • npm --prefix web run build -- production build succeeds
  • npm --prefix web run storybook:build -- all stories compile
  • Visual: timeline renders horizontal nodes with status colors
  • Visual: click meeting node navigates to detail page
  • Visual: token bars show per-participant breakdown with rank badges
  • Visual: contributions display with avatars, phase labels, turn numbers
  • Visual: trigger meeting dialog validates empty input

Closes #788

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 28, 2026

Walkthrough

Implements a complete Meetings feature: a populated Zustand meetings store with fetch/trigger/WebSocket handling and request-sequencing; new hooks (useMeetingsData, useMeetingDetailData); many UI components (timeline, timeline nodes, cards, detail header, agenda, decisions, action items, contributions, token breakdown, TokenUsageBar, filter bar, metric cards, trigger dialog) plus skeletons and Storybook stories; utilities in web/src/utils/meetings.ts (status/phase mappings, token computations, filtering/aggregation) and a runtime MEETING_STATUS_VALUES constant; test factory makeMeeting and extensive unit tests for hooks, store, utils, and pages; and updated MeetingsPage and MeetingDetailPage (data-driven rendering).

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 40.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly describes the main deliverables: a meetings page with timeline visualization, token usage bars, and contribution formatting, directly reflecting the primary changes and meeting the #788 objective.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering meetings list/detail pages, token bars, contributions, store, tests, and storybook stories with specific metrics and pre-review information.
Linked Issues check ✅ Passed The PR fully implements all coding objectives from issue #788: timeline visualization (MeetingTimeline/MeetingTimelineNode), token usage bars (TokenUsageBar, MeetingTokenBreakdown), formatted contributions (ContributionBubble, MeetingContributions), detail expansion (MeetingDetailPage), meeting trigger (TriggerMeetingDialog), and comprehensive unit tests.
Out of Scope Changes check ✅ Passed All changes are scoped to the #788 objectives. The minor update to formatLabel (supporting hyphens in addition to underscores) is in-scope as it enables proper label formatting for meeting types and agent IDs used throughout the new components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 28, 2026

Dependency Review

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

Snapshot Warnings

⚠️: No snapshots were found for the head SHA dd61f1e.
Ensure that dependencies are being submitted on PR branches. Re-running this action after a short time may resolve the issue. See the documentation for more information and troubleshooting advice.

Scanned Files

None

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 introduces the Meetings feature, featuring a list view with metrics and filtering, a detailed view for meeting minutes, and a Zustand store with real-time WebSocket support. The review identifies a potential division-by-zero error in the token usage bar and highlights a React anti-pattern where state setters were called directly during rendering. Additionally, several components were flagged for using non-unique keys in list rendering, which could lead to UI inconsistencies.

Comment on lines +38 to +58
{segments.map((segment, i) => {
const segPercent = total > 0 ? (segment.value / total) * 100 : 0
if (segPercent <= 0) return null
const colorClass = segment.color ?? SEGMENT_COLORS[i % SEGMENT_COLORS.length]
return (
<div
key={segment.label}
className={cn(
'h-full transition-all duration-[900ms]',
colorClass,
i === 0 && 'rounded-l-full',
i === segments.length - 1 && 'rounded-r-full',
)}
style={{
width: `${(segment.value / usedTokens) * 100}%`,
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
}}
title={`${segment.label}: ${segment.value.toLocaleString()} tokens`}
/>
)
})}
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.

high

There's a potential division-by-zero error and some confusing logic in the segment rendering loop.

  1. Division by zero: If usedTokens is 0 (e.g., from segments with values [10, -10]), the width calculation (segment.value / usedTokens) * 100 will result in Infinity or NaN, which is invalid for a CSS property.
  2. Confusing filter condition: The condition if (segPercent <= 0) return null depends on total, but whether a segment should be rendered should only depend on its own value. A segment with a positive value should always be rendered, regardless of the total.

I suggest simplifying the loop to directly check segment.value and guard against division by zero.

Suggested change
{segments.map((segment, i) => {
const segPercent = total > 0 ? (segment.value / total) * 100 : 0
if (segPercent <= 0) return null
const colorClass = segment.color ?? SEGMENT_COLORS[i % SEGMENT_COLORS.length]
return (
<div
key={segment.label}
className={cn(
'h-full transition-all duration-[900ms]',
colorClass,
i === 0 && 'rounded-l-full',
i === segments.length - 1 && 'rounded-r-full',
)}
style={{
width: `${(segment.value / usedTokens) * 100}%`,
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
}}
title={`${segment.label}: ${segment.value.toLocaleString()} tokens`}
/>
)
})}
{usedTokens > 0 && segments.map((segment, i) => {
if (segment.value <= 0) return null
const colorClass = segment.color ?? SEGMENT_COLORS[i % SEGMENT_COLORS.length]
return (
<div
key={segment.label}
className={cn(
'h-full transition-all duration-[900ms]',
colorClass,
i === 0 && 'rounded-l-full',
i === segments.length - 1 && 'rounded-r-full',
)}
style={{
width: `${(segment.value / usedTokens) * 100}%`,
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
}}
title={`${segment.label}: ${segment.value.toLocaleString()} tokens`}
/>
)
})}

Comment on lines +30 to +32
if (wsConnected && !wasConnected) {
setWasConnected(true)
}
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.

high

Calling a state setter (setWasConnected) directly within the component's render body is a side effect that can lead to unexpected behavior and is against React's rules of purity for render functions. State updates should be performed inside useEffect hooks or event handlers.

To correctly track if a WebSocket connection was previously established, you can use a useEffect hook. You will also need to import useEffect from react.

Suggested change
if (wsConnected && !wasConnected) {
setWasConnected(true)
}
useEffect(() => {
if (wsConnected) {
setWasConnected(true)
}
}, [wsConnected])

Comment on lines +39 to +41
if (wsConnected && !wasConnected) {
setWasConnected(true)
}
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.

high

Calling a state setter (setWasConnected) directly within the component's render body is a side effect that can lead to unexpected behavior and is against React's rules of purity for render functions. State updates should be performed inside useEffect hooks or event handlers.

To correctly track if a WebSocket connection was previously established, you can use a useEffect hook. You will also need to update your import from react to include useEffect.

Suggested change
if (wsConnected && !wasConnected) {
setWasConnected(true)
}
useEffect(() => {
if (wsConnected) {
setWasConnected(true)
}
}, [wsConnected])

<SectionCard title="Action Items" icon={ClipboardList} className={className}>
<ul className="space-y-3">
{actionItems.map((item) => (
<li key={`${item.assignee_id}-${item.description.slice(0, 30)}`} className="flex items-start gap-3">
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

Using a slice of the description for the key prop is not ideal, as it may not be unique if two action items have similar descriptions. Since the ActionItem type doesn't provide a unique ID, using the array index is a safer alternative for a stable list.

Suggested change
<li key={`${item.assignee_id}-${item.description.slice(0, 30)}`} className="flex items-start gap-3">
<li key={`${item.assignee_id}-${idx}`} className="flex items-start gap-3">

{agenda.items.length > 0 && (
<ol className="space-y-3">
{agenda.items.map((item, idx) => (
<li key={`agenda-${item.title}`} className="flex gap-3">
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

Using item.title as a key is not safe as agenda item titles may not be unique within a meeting. When a unique ID is not available, using the item's index in the array is a better fallback for a stable list, as it ensures a unique key for each rendered element. React uses keys to identify which items have changed, are added, or are removed.

Suggested change
<li key={`agenda-${item.title}`} className="flex gap-3">
<li key={`agenda-item-${idx}`} className="flex gap-3">

<SectionCard title="Decisions" icon={CheckCircle} className={className}>
<ol className="space-y-2">
{decisions.map((decision, idx) => (
<li key={`decision-${decision.slice(0, 40)}`} className="flex items-start gap-2">
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

Using a slice of the decision string as a key is not guaranteed to be unique, especially if multiple decisions start with the same text. When a unique ID is not available on the items, using the array index is a safer and more reliable way to provide a stable key for each element in a static list.

Suggested change
<li key={`decision-${decision.slice(0, 40)}`} className="flex items-start gap-2">
<li key={`decision-${idx}`} className="flex items-start gap-2">

Copy link
Copy Markdown

@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: 24

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/__tests__/hooks/useMeetingDetailData.test.ts`:
- Around line 67-70: Add a unit test for the wsSetupError scenario by rendering
useMeetingDetailData with the WebSocket hook mocked to return a non-null setup
error and asserting the hook exposes that error (check
result.current.wsSetupError) and any resulting state changes; locate the mock of
the WebSocket hook used in the tests (the function name used to provide
wsConnected/wsSetupError) and update or create a test case in
useMeetingDetailData.test that uses renderHook(() =>
useMeetingDetailData('meeting-1')) with the mock returning setupError to verify
the hook surface handles and returns wsSetupError as expected.
- Around line 59-65: The test currently uses a non-null assertion when accessing
vi.mocked(useWebSocket).mock.calls[0] in the 'sets up WebSocket with meetings
channel' case which can throw and obscure failures; update the test to first
assert the mock was called (e.g.,
expect(vi.mocked(useWebSocket).mock.calls.length).toBeGreaterThan(0) or
expect(vi.mocked(useWebSocket)).toHaveBeenCalled()) before reading the first
call, then read the first call into callArgs and continue to derive channels and
assert they equal ['meetings'] (references: useWebSocket, useMeetingDetailData,
renderHook, callArgs).

In `@web/src/__tests__/hooks/useMeetingsData.test.ts`:
- Around line 87-93: The test currently only asserts the channel name; instead
call the binding's handler to ensure events are forwarded to handleWsEvent:
after capturing callArgs from the mocked useWebSocket (as in the existing test),
pick callArgs.bindings[0] (or find the binding with channel === 'meetings'),
invoke its handler with a mock event object (e.g. { type: 'test', payload: {}
}), and assert that the mocked handleWsEvent (mockHandleWsEvent) was called with
that event; update the test to import or reference mockHandleWsEvent and use
expect(mockHandleWsEvent).toHaveBeenCalledWith(...) to validate correct
forwarding by useMeetingsData.

In `@web/src/components/ui/token-usage-bar.stories.tsx`:
- Around line 13-78: The TokenUsageBar story set is missing the required
shared-component state stories (hover, loading, error); add three new Story
exports named Hover, Loading, and Error alongside the existing
Default/Empty/etc. For Loading and Error create Story objects that pass the
appropriate flags via args (e.g., loading: true and error: true) and reuse
existing segments/total data; for Hover add a Hover story that either sets a
prop the component recognizes for hover or adds a Storybook play function that
uses userEvent.hover (or canvas.getByTestId to target the bar) to simulate
mouse-over so the hover styles appear. Ensure the new stories follow the same
export pattern as Default/Full (export const Hover: Story = { args: { ... } }
etc.) so Storybook shows all required component states.

In `@web/src/components/ui/token-usage-bar.tsx`:
- Around line 9-13: Export the TokenUsageBarProps interface so component props
follow the project's guideline: change the declaration of TokenUsageBarProps to
an exported interface (export interface TokenUsageBarProps { segments: readonly
TokenSegment[]; total: number; className?: string }) and ensure any usages of
TokenUsageBarProps (e.g., in the TokenUsageBar component signature) still refer
to the now-exported interface.
- Around line 38-57: The rounding bug occurs because the map uses the original
segments array index (i) even when some segments return null; fix by filtering
out non-positive segments first (const visible = segments.filter(s => s.value >
0)) and then map over visible so you can use the mapped index to apply rounded
classes (use j === 0 for 'rounded-l-full' and j === visible.length - 1 for
'rounded-r-full'); update color selection to use SEGMENT_COLORS[j %
SEGMENT_COLORS.length] and ensure keys are stable (e.g., use
`${segment.label}-${j}`) when rendering inside the same token-usage-bar
component that uses cn, SEGMENT_COLORS, and segments.
- Around line 43-44: The key uses segment.label which may not be unique; update
the JSX in TokenUsageBar (the <div key={segment.label}> for each segment) to use
a stable unique key such as combining the label with the index (e.g.,
`${segment.label}-${i}`) or use a real unique identifier on the segment
(segment.id) if available, ensuring each mapped child has a unique key even when
labels repeat.

In `@web/src/pages/MeetingDetailPage.tsx`:
- Around line 54-58: The status banner currently rendered as a plain div (the
JSX block with className "flex items-center gap-2 rounded-lg border
border-danger/30 bg-danger/5 px-4 py-2 text-sm text-danger" containing
<AlertTriangle /> and {error}) should be made accessible by either replacing it
with the shared alert/banner component or by adding ARIA attributes: add
role="alert" and aria-live="assertive" (or "polite" as appropriate) to the
container and ensure it is rendered when the error/disconnect state changes;
apply the same change to the similar disconnect banner block (the other JSX
banner around the WebSocket/fetch state) so screen readers will announce the
messages.
- Line 63: The component returns null when meeting is falsy which causes a blank
frame on the initial render before useMeetingDetailData's effect flips loading;
update MeetingDetailPage to stop returning null unconditionally: remove the
early "if (!meeting) return null" and instead branch on loading/error (e.g., if
(loading) render the skeleton component, if (error) render the error state) and
only avoid rendering detail UI when meeting is null after loading completes;
reference useMeetingDetailData, meeting, loading, and MeetingDetailPage when
making the change.

In `@web/src/pages/meetings/MeetingActionItems.tsx`:
- Around line 19-20: The action-item row JSX inside actionItems.map is too large
and uses a collision-prone key; extract that block into a new shared component
(e.g., ActionItemRow) placed under web/src/components/ui/, accept props (item,
idx, onChange handlers as needed), replace the in-map JSX with <ActionItemRow
item={item} idx={idx} ... /> and change the list key to a collision-free value
such as key={`${item.id ?? idx}-${item.assignee_id}` } (or include idx
explicitly) so each row is uniquely identified; update imports in
MeetingActionItems to use the new component and move any local helper logic into
the component or pass as props.

In `@web/src/pages/meetings/MeetingAgendaSection.tsx`:
- Around line 36-38: Presenter IDs containing hyphens are not formatted
correctly because formatLabel currently only splits on underscores; update the
formatting to handle both '-' and '_' by either (A) normalizing the presenter_id
before rendering in MeetingAgendaSection (replace '-' with '_' before calling
formatLabel) or (B) enhance the formatLabel function itself to split on both
separators (e.g., split on /[_-]/) so calls like formatLabel(item.presenter_id)
produce readable labels for values like "agent-alice" and "agent_bob".
- Around line 26-27: The map callback in MeetingAgendaSection.tsx expands into a
large JSX block and uses a fragile key `agenda-${item.title}`; extract that row
JSX into a small shared component (e.g., AgendaRow or MeetingAgendaRow) placed
under web/src/components/ui/, replace the inline JSX in agenda.items.map(...)
with a call to this new component, and change the key to a collision-free value
such as `${item.title}-${idx}` (or better a stable id if available) so the list
no longer creates complex (>8 line) JSX inside the map and avoids key
collisions.

In `@web/src/pages/meetings/MeetingCard.tsx`:
- Around line 48-55: Replace the inline status pill with the shared StatusBadge
component: import StatusBadge from "@/components/ui/status-badge" and replace
the <span ...>{getMeetingStatusLabel(meeting.status)}</span> with <StatusBadge
status={meeting.status}
className={badgeClass}>{getMeetingStatusLabel(meeting.status)}</StatusBadge>
(preserving the computed badgeClass and the getMeetingStatusLabel call); update
any imports and remove the unused span/code.

In `@web/src/pages/meetings/MeetingContributions.tsx`:
- Around line 38-56: The group container key
`${group.phase}-${group.items[0]?.turn_number}` can collide for non-contiguous
groups with the same phase and first turn_number; update the groups.map to
include the group index (or another stable unique value) in the key (e.g., use
the map index or a generated group id) so each outer div gets a truly unique
key; adjust the key expression used in the groups.map callback (the element that
renders the phase header and StaggerGroup) while leaving the inner StaggerItem
keys (which use contribution.agent_id, contribution.phase,
contribution.turn_number) unchanged.

In `@web/src/pages/meetings/MeetingDecisions.tsx`:
- Around line 15-16: The current key for list items in the decisions.map render
(the key expression in the <li> inside MeetingDecisions.tsx) uses
decision.slice(0, 40) which can collide; update the key to include the loop
index (e.g., combine idx with the prefix) so it becomes unique and stable for
this immutable list (e.g., use a key like
`decision-${idx}-${decision.slice(0,40)}` or at minimum include idx).

In `@web/src/pages/meetings/MeetingDetailHeader.tsx`:
- Line 64: MeetingDetailHeader currently renders participantCount twice (once in
the StatPill and again inside the timestamps section); remove the redundant
rendering in the timestamps block so participantCount is only shown via the
StatPill. Edit the MeetingDetailHeader JSX to delete the element that references
participantCount in the timestamps/timespan area (leave
StatPill(label="Participants" value={participantCount}) intact), and adjust
surrounding markup/CSS if needed so spacing/layout remains correct and any
snapshot/tests reflecting the timestamps are updated.

In `@web/src/pages/meetings/MeetingDetailSkeleton.tsx`:
- Line 30: The JSX div in MeetingDetailSkeleton.tsx currently uses an arbitrary
breakpoint token `max-[1023px]:grid-cols-1`; change the responsive approach to
use Tailwind's standard `lg:` breakpoint by making the default layout
`grid-cols-1` and switching to `lg:grid-cols-2` for larger screens — update the
className on the div inside the MeetingDetailSkeleton component accordingly
(replace `grid-cols-2 ... max-[1023px]:grid-cols-1` with `grid-cols-1 ...
lg:grid-cols-2` while keeping `grid` and `gap-grid-gap`).

In `@web/src/pages/meetings/MeetingFilterBar.tsx`:
- Around line 13-20: The file duplicates the runtime status allowlist
(ALL_STATUSES) already present as VALID_STATUSES in MeetingsPage.tsx; extract a
single exported array (e.g., VALID_STATUSES or ALLOWED_MEETING_STATUSES) from
web/src/utils/meetings.ts, export it, then replace ALL_STATUSES in
MeetingFilterBar.tsx and VALID_STATUSES usage in MeetingsPage.tsx with an import
from that utils file; update STATUS_OPTIONS to build from the shared array and
ensure both components import getMeetingStatusLabel as needed so the dropdown
and URL parsing use the same source of truth.

In `@web/src/pages/meetings/MeetingTimelineNode.stories.tsx`:
- Around line 16-34: Add a missing story for the remaining MeetingStatus value
by defining a new Story export (similar to Completed/InProgress/etc.) that uses
makeMeeting with status 'budget_exhausted' (e.g., export const BudgetExhausted:
Story = { args: { meeting: makeMeeting('6', { status: 'budget_exhausted' }) }
}). Place this alongside the other story exports in
MeetingTimelineNode.stories.tsx so the component has visual coverage for the
budget_exhausted state.

In `@web/src/pages/meetings/MeetingTokenBreakdown.tsx`:
- Around line 70-80: The inner progress-bar div is using cn() unnecessarily; in
MeetingTokenBreakdown.tsx replace the className expression on the inner <div>
that currently reads className={cn('h-full rounded-full bg-accent transition-all
duration-[900ms]')} with a plain string className="h-full rounded-full bg-accent
transition-all duration-[900ms]" (or keep cn(...) only if you plan to add
conditional classes later) so the unnecessary cn() call is removed while
preserving the same classes and styles.
- Around line 45-84: Extract the JSX inside the meeting.contribution_rank.map
into a new ParticipantTokenRow component and replace the inline JSX with a call
to that component; the new component should accept props (agentId, tokens,
share, rankLabel, formatLabel) or the whole meeting/agentId as needed, use the
existing helpers getParticipantTokenShare and formatLabel, render the Avatar,
rank badge (RANK_BADGES), tokens display and the progress bar with the same
classNames and inline style, and be placed in web/src/pages/meetings/ or
web/src/components/ui/ so the .map becomes a simple one-liner rendering
<ParticipantTokenRow ... /> for each agentId.

In `@web/src/pages/MeetingsPage.tsx`:
- Around line 134-149: The EmptyState branches incorrectly show "No meetings
yet" / "No matching meetings" when an initial load failed; update the
conditional renders that use filtered.length === 0 && hasFilters (and its
hasFilters=false counterpart) to also require that error is falsy (and
optionally not loading) before rendering; specifically modify the two blocks
that render EmptyState (referencing filtered, hasFilters, error, and
setTriggerOpen / handleFiltersChange) so they only show the empty-state when
there is no error (e.g., filtered.length === 0 && !hasFilters && !error).
- Around line 63-70: The handler handleTrigger currently swallows errors and
closes the dialog, so move setTriggerOpen(false) to run only after a successful
await triggerMeeting(...) and in the catch block call
useToastStore.getState().add(...) with the error description and then rethrow
the error (throw err) so ConfirmDialog's onConfirm sees the rejection; keep
references to triggerMeeting, setTriggerOpen, useToastStore.getState().add, and
getErrorMessage to locate where to make the change.

In `@web/src/stores/meetings.ts`:
- Around line 137-151: Refactor upsertMeeting to simplify the array update:
compute idx via s.meetings.findIndex, then build newMeetings deterministically —
if idx === -1 set newMeetings = [meeting, ...s.meetings], otherwise set
newMeetings = [...s.meetings.slice(0, idx), meeting, ...s.meetings.slice(idx +
1)]; set selectedMeeting to meeting when s.selectedMeeting?.meeting_id ===
meeting.meeting_id, and only increment total when idx === -1; update the state
return to use these values (referencing upsertMeeting, meetings,
selectedMeeting, total) to avoid the conditional spread + in-place mutation
pattern.
🪄 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: 7ca73c3f-591d-41f0-a0f6-663470b5456f

📥 Commits

Reviewing files that changed from the base of the PR and between 937974e and 4578342.

📒 Files selected for processing (46)
  • CLAUDE.md
  • web/src/__tests__/helpers/factories.ts
  • web/src/__tests__/hooks/useMeetingDetailData.test.ts
  • web/src/__tests__/hooks/useMeetingsData.test.ts
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/__tests__/utils/meetings.test.ts
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/hooks/useMeetingDetailData.ts
  • web/src/hooks/useMeetingsData.ts
  • web/src/pages/MeetingDetailPage.tsx
  • web/src/pages/MeetingsPage.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/stores/meetings.ts
  • web/src/utils/meetings.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Dashboard Test
  • GitHub Check: Build Backend
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Web
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Documentation must be written in Markdown and built with Zensical (config: mkdocs.yml); design spec lives in docs/design/ (11 pages linked from docs/DESIGN_SPEC.md)

Files:

  • CLAUDE.md
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Prefer interface for defining object shapes in TypeScript
Use camelCase for variable names and function identifiers

Files:

  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/__tests__/helpers/factories.ts
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/__tests__/hooks/useMeetingsData.test.ts
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/pages/MeetingsPage.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/__tests__/hooks/useMeetingDetailData.test.ts
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/__tests__/utils/meetings.test.ts
  • web/src/hooks/useMeetingDetailData.ts
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/hooks/useMeetingsData.ts
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/pages/MeetingDetailPage.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/utils/meetings.ts
  • web/src/stores/meetings.ts
web/src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.tsx: ALWAYS reuse existing shared components from web/src/components/ui/ before creating new ones (e.g., StatusBadge, MetricCard, AgentCard, DeptHealthBar, SectionCard)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success) or CSS variables (var(--so-accent)); NEVER hardcode hex values or rgba() in .tsx files
Use font-sans or font-mono for typography (maps to Geist tokens); NEVER set fontFamily directly in CSS
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing
Do NOT recreate complex (>8 line) JSX inside .map() blocks; extract to a shared component in web/src/components/ui/

Files:

  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/pages/MeetingsPage.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/pages/MeetingDetailPage.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingDecisions.tsx
**/*.{py,ts,tsx,go}

📄 CodeRabbit inference engine (CLAUDE.md)

Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript)

Files:

  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/__tests__/helpers/factories.ts
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/__tests__/hooks/useMeetingsData.test.ts
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/pages/MeetingsPage.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/__tests__/hooks/useMeetingDetailData.test.ts
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/__tests__/utils/meetings.test.ts
  • web/src/hooks/useMeetingDetailData.ts
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/hooks/useMeetingsData.ts
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/pages/MeetingDetailPage.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/utils/meetings.ts
  • web/src/stores/meetings.ts
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Import cn from @/lib/utils for conditional class merging in components

Files:

  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/__tests__/helpers/factories.ts
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/__tests__/hooks/useMeetingsData.test.ts
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/pages/MeetingsPage.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/__tests__/hooks/useMeetingDetailData.test.ts
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/__tests__/utils/meetings.test.ts
  • web/src/hooks/useMeetingDetailData.ts
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/hooks/useMeetingsData.ts
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/pages/MeetingDetailPage.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/utils/meetings.ts
  • web/src/stores/meetings.ts
web/src/pages/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Lazy-load page components and place page-scoped sub-components in pages/<page-name>/ subdirectories (e.g., pages/tasks/, pages/org-edit/)

Files:

  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/pages/MeetingsPage.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/pages/MeetingDetailPage.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingDecisions.tsx
web/src/components/ui/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/components/ui/**/*.tsx: New shared components must have a corresponding .stories.tsx Storybook file with all component states (default, hover, loading, error, empty)
Export component props as a TypeScript interface
Use design token variables exclusively for shadows and borders (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values

Files:

  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/components/ui/token-usage-bar.tsx
🧠 Learnings (7)
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : ALWAYS reuse existing shared components from `web/src/components/ui/` before creating new ones (e.g., `StatusBadge`, `MetricCard`, `AgentCard`, `DeptHealthBar`, `SectionCard`)

Applied to files:

  • CLAUDE.md
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/components/ui/token-usage-bar.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design token variables exclusively for shadows and borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); never hardcode shadow or border values

Applied to files:

  • CLAUDE.md
  • web/src/components/ui/token-usage-bar.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : New shared components must have a corresponding `.stories.tsx` Storybook file with all component states (default, hover, loading, error, empty)

Applied to files:

  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
📚 Learning: 2026-03-20T08:28:32.845Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T08:28:32.845Z
Learning: Applies to web/src/__tests__/**/*.{ts,js} : Dashboard testing: Vitest unit tests organized by feature under `web/src/__tests__/`. Use fast-check for property-based testing (`fc.assert` + `fc.property`).

Applied to files:

  • web/src/__tests__/hooks/useMeetingsData.test.ts
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/__tests__/hooks/useMeetingDetailData.test.ts
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/__tests__/utils/meetings.test.ts
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/pages/**/*.tsx : Lazy-load page components and place page-scoped sub-components in `pages/<page-name>/` subdirectories (e.g., `pages/tasks/`, `pages/org-edit/`)

Applied to files:

  • web/src/__tests__/pages/MeetingsPage.test.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : Export component props as a TypeScript interface

Applied to files:

  • web/src/pages/meetings/MeetingFilterBar.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Use density-aware spacing tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing

Applied to files:

  • web/src/components/ui/token-usage-bar.tsx
🔇 Additional comments (33)
web/src/pages/meetings/MeetingsSkeleton.tsx (1)

1-31: LGTM!

Clean skeleton component with proper accessibility (aria-label), responsive grid layout, and good reuse of shared UI components (Skeleton, SkeletonCard, SkeletonMetric). The semantic spacing token gap-grid-gap and responsive breakpoint classes follow the coding guidelines.

web/src/pages/meetings/MeetingsSkeleton.stories.tsx (1)

1-13: LGTM!

Proper Storybook CSF setup with typed Meta/StoryObj. A single Default story is appropriate for a skeleton component since it represents only the loading state.

web/src/__tests__/helpers/factories.ts (1)

115-168: LGTM!

Well-structured test factory with sensible defaults. The token math is internally consistent (contributions sum correctly), and all required MeetingResponse fields are provided. The spread override pattern enables flexible test data generation.

web/src/stores/meetings.ts (2)

109-135: LGTM — Solid WS payload validation.

The field-by-field type checks before casting to MeetingResponse provide good runtime safety. The validation appropriately focuses on required non-nullable fields while allowing nullable fields (minutes, error_message, meeting_duration_seconds) to pass through unchecked.


56-75: LGTM — Stale-request handling is correct.

The sequence counter pattern properly discards responses from superseded requests, preventing race conditions when rapid filter changes occur. The selectedMeeting reconciliation with fresh data is a nice touch.

web/src/utils/meetings.ts (1)

1-158: LGTM!

Comprehensive utility module with well-organized mapping tables, defensive token/duration helpers with proper edge-case handling (clamping, null checks), and clean filtering logic. The exported CSS class maps use Tailwind semantic colors as per guidelines.

web/src/pages/meetings/MeetingTimelineNode.tsx (1)

14-57: LGTM!

Well-structured timeline node with proper accessibility (aria-label, semantic <time> element), good use of shared utilities, and correct route parameter substitution. The pulse animation for in-progress meetings is a nice UX touch.

web/src/pages/meetings/MeetingTimeline.tsx (1)

11-32: LGTM!

Clean timeline implementation with proper empty-state handling, accessible decorative elements (aria-hidden, pointer-events-none), and smooth staggered animations. The snap-scroll behavior provides good UX for horizontal navigation.

web/src/pages/meetings/MeetingTimeline.stories.tsx (1)

1-45: Solid Storybook coverage and setup.

Typed metadata, router decorator, and the selected scenarios cover the main timeline states well.

web/src/__tests__/utils/meetings.test.ts (1)

1-240: Comprehensive utility test coverage.

The suite exercises both happy paths and edge cases across all exported meetings helpers.

web/src/pages/meetings/MeetingMetricCards.tsx (1)

1-32: Nice composition of shared metrics primitives.

Good reuse of MetricCard/StaggerGroup and clean derivation of aggregate values.

web/src/pages/meetings/MeetingMetricCards.stories.tsx (1)

1-27: Story set is clear and sufficient for metric states.

Default and Empty provide good baseline visual verification.

web/src/pages/meetings/MeetingCard.stories.tsx (1)

1-34: Good scenario coverage for card variants.

The stories exercise meaningful state combinations and routing context correctly.

CLAUDE.md (1)

268-268: Design-system inventory update is clear and accurate.

Adding TokenUsageBar to the component table keeps implementation and docs aligned.

web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx (1)

1-13: LGTM for skeleton Storybook entry.

The story is correctly typed and provides the expected loading-state showcase.

web/src/pages/meetings/MeetingDetailSkeleton.tsx (1)

1-36: Well-structured skeleton with proper accessibility and component reuse.

Good use of shared Skeleton and SkeletonCard components, aria-label for screen readers, and density-aware spacing tokens (gap-grid-gap).

web/src/pages/meetings/TriggerMeetingDialog.stories.tsx (2)

1-28: LGTM - Core stories are well-structured.

The Storybook metadata and story definitions follow CSF conventions correctly with proper typing via satisfies Meta<typeof TriggerMeetingDialog>.


21-28: 🧹 Nitpick | 🔵 Trivial

Consider adding a validation error story.

The PR mentions validation feedback was added to TriggerMeetingDialog. Consider adding a story that demonstrates the validation error state (e.g., empty event name submission) for complete visual documentation.

Based on learnings: "New shared components must have a corresponding .stories.tsx Storybook file with all component states (default, hover, loading, error, empty)".

⛔ Skipped due to learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : New shared components must have a corresponding `.stories.tsx` Storybook file with all component states (default, hover, loading, error, empty)
web/src/__tests__/hooks/useMeetingDetailData.test.ts (1)

1-86: Good hook test coverage overall.

The test suite properly validates mount behavior, state derivation from store, WebSocket channel bindings, and refetch on dependency change. The mock isolation pattern is clean.

web/src/pages/meetings/MeetingActionItems.stories.tsx (1)

1-33: LGTM - Comprehensive story coverage.

Stories cover the key states: multiple items with varied priorities, single item, and empty array. The assignee_id: null case in Default is a good edge case inclusion.

web/src/pages/meetings/ContributionBubble.stories.tsx (1)

1-68: LGTM - Well-organized phase coverage.

The DRY approach with a base object and spread overrides for each phase is clean. All five contribution phases are represented with meaningful content variations.

web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx (2)

1-33: Good use of test factory for story data.

Using makeMeeting from the test helpers provides consistent, realistic data structures across stories while allowing targeted overrides.


21-24: 🧹 Nitpick | 🔵 Trivial

Line likely exceeds 88-character limit.

Lines 22-23 may exceed the configured 88-character limit. Consider reformatting for readability:

       token_usage_by_participant: {
-        'agent-alice': 400, 'agent-bob': 350, 'agent-carol': 280,
-        'agent-dave': 150, 'agent-eve': 120,
+        'agent-alice': 400,
+        'agent-bob': 350,
+        'agent-carol': 280,
+        'agent-dave': 150,
+        'agent-eve': 120,
       },

As per coding guidelines: "Lines must not exceed 88 characters".

⛔ Skipped due to learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : New shared components must have a corresponding `.stories.tsx` Storybook file with all component states (default, hover, loading, error, empty)
web/src/pages/meetings/MeetingDecisions.stories.tsx (1)

1-31: LGTM - Clean story definitions.

Covers the essential states (multiple items, single item, empty) with clear, descriptive decision text that aids visual review.

web/src/pages/meetings/MeetingFilterBar.stories.tsx (1)

1-27: LGTM - Appropriate filter state coverage.

The Default (empty filters) and WithActiveFilters stories demonstrate the two key visual states for a filter bar component.

web/src/pages/meetings/MeetingContributions.tsx (1)

18-31: LGTM!

The groupByPhase helper correctly implements in-order phase grouping, preserving contribution sequence while creating logical phase boundaries. Clean implementation.

web/src/pages/meetings/MeetingTokenBreakdown.tsx (1)

1-41: LGTM!

Clean component structure with proper reuse of shared components (Avatar, SectionCard, TokenUsageBar), semantic Tailwind classes, and correct use of cn for class composition.

web/src/__tests__/stores/meetings.test.ts (3)

90-117: LGTM!

Excellent test coverage for stale-response rejection. The test correctly simulates out-of-order async resolution and verifies the store discards stale data while preserving fresh updates.


182-259: LGTM!

Comprehensive test coverage for WebSocket event handling, including malformed payloads, missing fields, arrays, and primitives. Properly validates the store's defensive validation logic and logging behavior.


1-39: LGTM!

Clean test setup with proper store reset, mock clearing, and mock restoration. The importApi() pattern correctly handles dynamic imports for mocked modules.

web/src/pages/meetings/MeetingDetailHeader.tsx (1)

22-56: LGTM!

Clean header implementation with proper accessibility (aria-label on back link, aria-hidden on decorative icons), semantic Tailwind classes, and appropriate reuse of shared components.

web/src/hooks/useMeetingsData.ts (2)

21-32: LGTM!

Good pattern using individual Zustand selectors to minimize re-renders, and using getState() in the effect to avoid stale closure issues with store actions.


46-56: LGTM!

Correct use of useMemo with empty deps for stable bindings, and getState() inside the handler to always access the latest store state.

Comment on lines +13 to +78
export const Default: Story = {
args: {
segments: [
{ label: 'agent-alice', value: 350 },
{ label: 'agent-bob', value: 300 },
],
total: 2000,
},
}

export const SingleSegment: Story = {
args: {
segments: [{ label: 'agent-alice', value: 650 }],
total: 2000,
},
}

export const Full: Story = {
args: {
segments: [
{ label: 'agent-alice', value: 1200 },
{ label: 'agent-bob', value: 800 },
],
total: 2000,
},
}

export const Overflow: Story = {
args: {
segments: [
{ label: 'agent-alice', value: 1500 },
{ label: 'agent-bob', value: 800 },
],
total: 2000,
},
}

export const Empty: Story = {
args: {
segments: [],
total: 2000,
},
}

export const ManyParticipants: Story = {
args: {
segments: [
{ label: 'agent-alice', value: 350 },
{ label: 'agent-bob', value: 300 },
{ label: 'agent-carol', value: 280 },
{ label: 'agent-dave', value: 150 },
{ label: 'agent-eve', value: 120 },
],
total: 5000,
},
}

export const CustomColors: Story = {
args: {
segments: [
{ label: 'Input', value: 380, color: 'bg-accent' },
{ label: 'Output', value: 270, color: 'bg-success' },
],
total: 2000,
},
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add the missing shared-component state stories.

TokenUsageBar is a new shared UI component, but this story set still omits the required hover, loading, and error variants. That leaves Storybook without the full regression surface the repo expects for shared components.

As per coding guidelines "New shared components must have a corresponding .stories.tsx Storybook file with all component states (default, hover, loading, error, empty)".

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

In `@web/src/components/ui/token-usage-bar.stories.tsx` around lines 13 - 78, The
TokenUsageBar story set is missing the required shared-component state stories
(hover, loading, error); add three new Story exports named Hover, Loading, and
Error alongside the existing Default/Empty/etc. For Loading and Error create
Story objects that pass the appropriate flags via args (e.g., loading: true and
error: true) and reuse existing segments/total data; for Hover add a Hover story
that either sets a prop the component recognizes for hover or adds a Storybook
play function that uses userEvent.hover (or canvas.getByTestId to target the
bar) to simulate mouse-over so the hover styles appear. Ensure the new stories
follow the same export pattern as Default/Full (export const Hover: Story = {
args: { ... } } etc.) so Storybook shows all required component states.

@Aureliolo Aureliolo force-pushed the feat/meetings-page branch from 4578342 to b30b032 Compare March 28, 2026 15:49
Copy link
Copy Markdown

@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: 10

♻️ Duplicate comments (4)
web/src/__tests__/hooks/useMeetingsData.test.ts (1)

87-93: 🧹 Nitpick | 🔵 Trivial

Test verifies channel but not handler forwarding.

This test confirms the meetings channel is bound but doesn't invoke the handler to verify events are forwarded to handleWsEvent. Consider invoking the binding's handler with a mock event and asserting mockHandleWsEvent was called.

♻️ Suggested enhancement
   it('sets up WebSocket with meetings channel', async () => {
     const { useWebSocket } = await import('@/hooks/useWebSocket')
     renderHook(() => useMeetingsData())
     const callArgs = vi.mocked(useWebSocket).mock.calls[0]![0]
     const channels = callArgs.bindings.map((b) => b.channel)
     expect(channels).toEqual(['meetings'])
+
+    // Verify handler forwards events to store
+    const mockEvent = { event_type: 'meeting_update', channel: 'meetings', timestamp: '', payload: {} }
+    callArgs.bindings[0].handler(mockEvent as any)
+    expect(mockHandleWsEvent).toHaveBeenCalledWith(mockEvent)
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/hooks/useMeetingsData.test.ts` around lines 87 - 93, The
test currently asserts the WebSocket channel binding but doesn't exercise the
binding's handler; update the test in useMeetingsData.test.ts to import/use the
mocked useWebSocket, extract the first binding from
vi.mocked(useWebSocket).mock.calls[0][0].bindings, call its handler with a fake
event payload, and then assert that mockHandleWsEvent (the mocked handleWsEvent
used by useMeetingsData) was called with that event; target the useMeetingsData
hook setup and the binding.handler function to verify events are forwarded to
handleWsEvent.
web/src/__tests__/hooks/useMeetingDetailData.test.ts (1)

69-72: 🧹 Nitpick | 🔵 Trivial

Add explicit wsSetupError coverage for hook contract completeness.

The suite asserts wsConnected but still doesn’t verify the error path exposed by the hook.

✅ Suggested test addition
+  it('returns wsSetupError from WebSocket hook', async () => {
+    const { useWebSocket } = await import('@/hooks/useWebSocket')
+    const setupError = new Error('ws init failed')
+    vi.mocked(useWebSocket).mockReturnValue({
+      connected: false,
+      reconnectExhausted: false,
+      setupError,
+    })
+
+    const { result } = renderHook(() => useMeetingDetailData('meeting-1'))
+    expect(result.current.wsSetupError).toBe(setupError)
+  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/hooks/useMeetingDetailData.test.ts` around lines 69 - 72,
The test only asserts wsConnected from the useMeetingDetailData hook; add
explicit coverage for the error path by asserting the hook exposes wsSetupError
as expected. Update the test case (or add a new one) that renders
useMeetingDetailData('meeting-1') and include an assertion on
result.current.wsSetupError (e.g., toBeNull / toBeUndefined or toEqual a mock
error depending on test setup) so the hook contract verifies both wsConnected
and wsSetupError states; reference the hook name useMeetingDetailData and the
property wsSetupError when adding the assertion.
web/src/pages/meetings/MeetingContributions.tsx (1)

39-40: ⚠️ Potential issue | 🟡 Minor

Outer phase-group key is still not unique.

Line 40 can still collide when the same phase reappears with the same first
turn and group size. React will then reconcile the later block against the
wrong subtree. Include groupIndex or another per-group id in the outer key.

🔧 Minimal fix
-        {groups.map((group) => (
-          <div key={`${group.phase}-${group.items[0]?.turn_number ?? 0}-${group.items.length}`}>
+        {groups.map((group, groupIndex) => (
+          <div key={`${groupIndex}-${group.phase}`}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/meetings/MeetingContributions.tsx` around lines 39 - 40, The
outer key for the groups map in MeetingContributions.tsx is not guaranteed
unique; update the key used inside groups.map (the JSX keyed <div> for each
group) to include a per-group identifier such as the groupIndex (e.g., the
second parameter of map) or a unique id from the group object so it combines
phase, items[0]?.turn_number, items.length and groupIndex (or group.id) to
ensure uniqueness and avoid React reconciliation collisions.
web/src/components/ui/token-usage-bar.stories.tsx (1)

13-78: 🛠️ Refactor suggestion | 🟠 Major

Add Hover, Loading, and Error stories for this shared component.

These stories cover data permutations, but the shared-component state set is
still incomplete. If TokenUsageBar does not expose dedicated props for those
states, use decorators/play functions or wrapper renders so Storybook still
captures them.

As per coding guidelines "New shared components must have a corresponding .stories.tsx Storybook file with all component states (default, hover, loading, error, empty)".

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

In `@web/src/components/ui/token-usage-bar.stories.tsx` around lines 13 - 78, Add
three new Story exports named Hover, Loading, and Error to
token-usage-bar.stories.tsx for the TokenUsageBar component: create Hover by
reusing an existing args set and applying either a decorator or a play function
to programmatically trigger hover on the bar element (use the Story export name
Hover and target a selector or test-id that TokenUsageBar renders), create
Loading by rendering the component in a loading state (either pass a dedicated
prop like loading if TokenUsageBar supports it or wrap the component in a small
wrapper/decorator that simulates the loading UI), and create Error by rendering
the component in an error state (either pass an error prop or use a
wrapper/decorator to show the error UI). Ensure each story is exported as a
Story object (Hover, Loading, Error) and include minimal args consistent with
existing stories so Storybook captures these states even if TokenUsageBar lacks
explicit props.
🤖 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/components/ui/token-usage-bar.tsx`:
- Around line 29-35: The meter currently sets aria-valuenow to the raw
usedTokens which can exceed aria-valuemax (total) or be invalid when total is 0;
update the TokenUsageBar JSX so aria-valuenow uses a clamped value (e.g.,
Math.min(Math.max(usedTokens, 0), Math.max(total, 0))) or use
usedPercent-derived clamped value scaled to total, and set aria-valuetext to the
human-readable raw string (e.g., `${usedTokens.toLocaleString()} of
${total.toLocaleString()}`) so assistive tech sees the true numbers; keep
role="meter" and other attributes but ensure aria-valuemin is 0 and
aria-valuemax is Math.max(total, 0) to avoid invalid ranges.

In `@web/src/pages/MeetingDetailPage.tsx`:
- Line 1: wasConnectedRef is being mutated during render which breaks React's
purity; move the mutation into a useEffect so the ref only updates after a
successful commit. Specifically, remove any writes to wasConnectedRef.current
from the render body of MeetingDetailPage and instead add a useEffect that
watches the connection state (the same state/prop used to decide the disconnect
banner) and sets wasConnectedRef.current = true when that state becomes
connected; keep the disconnect banner logic using wasConnectedRef but ensure it
now reflects only committed connections.

In `@web/src/pages/meetings/MeetingCard.tsx`:
- Around line 76-89: The token usage bar currently renders whenever
meeting.token_budget > 0 which shows a misleading 0% bar for meetings with no
usage data; update the conditional in MeetingCard so it also requires
meeting.minutes (or another truthy usage indicator) before rendering the
progress div—i.e., only render the token bar when both meeting.token_budget > 0
and meeting.minutes exist, leaving the tokenPercent logic and styles unchanged.

In `@web/src/pages/meetings/MeetingFilterBar.tsx`:
- Around line 35-39: The current cast to MeetingPageFilters when calling
onFiltersChange hides the real type mismatch from SelectField; instead remove
the blanket "as MeetingPageFilters" and make the selection value explicit—either
type the onChange parameter on the SelectField as (val: MeetingStatus |
undefined) and pass { ...filters, status: val } to onFiltersChange, or ensure
STATUS_OPTIONS is typed/mapped to MeetingStatus so the SelectField returns
MeetingStatus values; update the onChange handler in MeetingFilterBar (where
SelectField, onFiltersChange, MeetingPageFilters, STATUS_OPTIONS and
MEETING_STATUS_VALUES are referenced) accordingly.

In `@web/src/pages/meetings/MeetingMetricCards.tsx`:
- Around line 6-8: The MeetingMetricCards component's props
(MeetingMetricCardsProps) currently only accept meetings and should include an
optional className?: string to match other components; add className to the
interface and to the MeetingMetricCards function signature, then spread or apply
that className onto the component's root element (e.g., the top-level div or
fragment inside MeetingMetricCards) so parent components can pass custom styling
consistently with MeetingAgendaSection and MeetingDecisions.

In `@web/src/pages/meetings/MeetingsSkeleton.tsx`:
- Around line 10-14: Change the arbitrary breakpoint to mobile-first Tailwind:
in MeetingsSkeleton.tsx update the container div className from "grid
grid-cols-4 gap-grid-gap max-[1023px]:grid-cols-2" to make small screens two
columns by default and switch to four columns at the standard lg breakpoint
(1024px), e.g. "grid grid-cols-2 gap-grid-gap lg:grid-cols-4"; this affects the
JSX element that renders SkeletonMetric components.

In `@web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx`:
- Around line 21-24: The object literal token_usage_by_participant in
MeetingTokenBreakdown.stories.tsx currently has a line that exceeds 88
characters; reformat the entries for 'agent-alice'...'agent-eve' so each
key/value pair is on its own line (or split into multiple shorter lines) to keep
line length <=88 characters, preserving the exact keys and numeric values and
the surrounding object name token_usage_by_participant.

In `@web/src/pages/MeetingsPage.tsx`:
- Around line 97-101: The realtime-disconnected banner rendered when
(wsSetupError || (wasConnectedRef.current && !wsConnected)) && !loading lacks
live-region semantics; update the banner div (the element that currently
contains WifiOff and the {wsSetupError ?? ...} text) to include role="status"
and aria-live="polite" (or role="alert" if you prefer urgent announcement) so
screen readers are notified when websocket warnings appear.
- Around line 33-36: The ref wasConnectedRef is being mutated during render (if
(wsConnected) wasConnectedRef.current = true), which can produce incorrect state
across discarded renders; move that mutation into a useEffect that depends on
wsConnected so only committed renders update it—create a useEffect(() => { if
(wsConnected) wasConnectedRef.current = true }, [wsConnected]) adjacent to the
existing hooks (useState/useRef) and remove the in-render assignment; keep the
ref name wasConnectedRef and the dependency wsConnected so semantics remain
identical.

In `@web/src/stores/meetings.ts`:
- Around line 64-67: The long chained set call at the end of the fetch logic is
exceeding line-length; break the expression into multiple lines for readability
by computing the freshSelected beforehand (it already uses currentSelected and
result.data) and then call set with each property on its own line so the call to
set({ meetings: result.data, total: result.total, loading: false,
selectedMeeting: freshSelected }) is wrapped across lines; locate the
freshSelected variable and the set(...) invocation in this block and reformat
them so each property (meetings, total, loading, selectedMeeting) is on its own
line.

---

Duplicate comments:
In `@web/src/__tests__/hooks/useMeetingDetailData.test.ts`:
- Around line 69-72: The test only asserts wsConnected from the
useMeetingDetailData hook; add explicit coverage for the error path by asserting
the hook exposes wsSetupError as expected. Update the test case (or add a new
one) that renders useMeetingDetailData('meeting-1') and include an assertion on
result.current.wsSetupError (e.g., toBeNull / toBeUndefined or toEqual a mock
error depending on test setup) so the hook contract verifies both wsConnected
and wsSetupError states; reference the hook name useMeetingDetailData and the
property wsSetupError when adding the assertion.

In `@web/src/__tests__/hooks/useMeetingsData.test.ts`:
- Around line 87-93: The test currently asserts the WebSocket channel binding
but doesn't exercise the binding's handler; update the test in
useMeetingsData.test.ts to import/use the mocked useWebSocket, extract the first
binding from vi.mocked(useWebSocket).mock.calls[0][0].bindings, call its handler
with a fake event payload, and then assert that mockHandleWsEvent (the mocked
handleWsEvent used by useMeetingsData) was called with that event; target the
useMeetingsData hook setup and the binding.handler function to verify events are
forwarded to handleWsEvent.

In `@web/src/components/ui/token-usage-bar.stories.tsx`:
- Around line 13-78: Add three new Story exports named Hover, Loading, and Error
to token-usage-bar.stories.tsx for the TokenUsageBar component: create Hover by
reusing an existing args set and applying either a decorator or a play function
to programmatically trigger hover on the bar element (use the Story export name
Hover and target a selector or test-id that TokenUsageBar renders), create
Loading by rendering the component in a loading state (either pass a dedicated
prop like loading if TokenUsageBar supports it or wrap the component in a small
wrapper/decorator that simulates the loading UI), and create Error by rendering
the component in an error state (either pass an error prop or use a
wrapper/decorator to show the error UI). Ensure each story is exported as a
Story object (Hover, Loading, Error) and include minimal args consistent with
existing stories so Storybook captures these states even if TokenUsageBar lacks
explicit props.

In `@web/src/pages/meetings/MeetingContributions.tsx`:
- Around line 39-40: The outer key for the groups map in
MeetingContributions.tsx is not guaranteed unique; update the key used inside
groups.map (the JSX keyed <div> for each group) to include a per-group
identifier such as the groupIndex (e.g., the second parameter of map) or a
unique id from the group object so it combines phase, items[0]?.turn_number,
items.length and groupIndex (or group.id) to ensure uniqueness and avoid React
reconciliation collisions.
🪄 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: 8e356eee-e13f-4c45-9f97-28c79872a5e8

📥 Commits

Reviewing files that changed from the base of the PR and between 4578342 and b30b032.

📒 Files selected for processing (48)
  • CLAUDE.md
  • web/src/__tests__/helpers/factories.ts
  • web/src/__tests__/hooks/useMeetingDetailData.test.ts
  • web/src/__tests__/hooks/useMeetingsData.test.ts
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/__tests__/utils/meetings.test.ts
  • web/src/api/types.ts
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/hooks/useMeetingDetailData.ts
  • web/src/hooks/useMeetingsData.ts
  • web/src/pages/MeetingDetailPage.tsx
  • web/src/pages/MeetingsPage.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/stores/meetings.ts
  • web/src/utils/format.ts
  • web/src/utils/meetings.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: Dashboard Test
  • GitHub Check: Build Backend
  • GitHub Check: Build Web
  • GitHub Check: Build Sandbox
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (go)
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Documentation must be written in Markdown and built with Zensical (config: mkdocs.yml); design spec lives in docs/design/ (11 pages linked from docs/DESIGN_SPEC.md)

Files:

  • CLAUDE.md
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Prefer interface for defining object shapes in TypeScript
Use camelCase for variable names and function identifiers

Files:

  • web/src/utils/format.ts
  • web/src/api/types.ts
  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/__tests__/hooks/useMeetingDetailData.test.ts
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/__tests__/hooks/useMeetingsData.test.ts
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/__tests__/helpers/factories.ts
  • web/src/hooks/useMeetingsData.ts
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/__tests__/utils/meetings.test.ts
  • web/src/stores/meetings.ts
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/hooks/useMeetingDetailData.ts
  • web/src/pages/MeetingsPage.tsx
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/utils/meetings.ts
  • web/src/pages/MeetingDetailPage.tsx
**/*.{py,ts,tsx,go}

📄 CodeRabbit inference engine (CLAUDE.md)

Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript)

Files:

  • web/src/utils/format.ts
  • web/src/api/types.ts
  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/__tests__/hooks/useMeetingDetailData.test.ts
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/__tests__/hooks/useMeetingsData.test.ts
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/__tests__/helpers/factories.ts
  • web/src/hooks/useMeetingsData.ts
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/__tests__/utils/meetings.test.ts
  • web/src/stores/meetings.ts
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/hooks/useMeetingDetailData.ts
  • web/src/pages/MeetingsPage.tsx
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/utils/meetings.ts
  • web/src/pages/MeetingDetailPage.tsx
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Import cn from @/lib/utils for conditional class merging in components

Files:

  • web/src/utils/format.ts
  • web/src/api/types.ts
  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/__tests__/hooks/useMeetingDetailData.test.ts
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/__tests__/hooks/useMeetingsData.test.ts
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/__tests__/helpers/factories.ts
  • web/src/hooks/useMeetingsData.ts
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/__tests__/utils/meetings.test.ts
  • web/src/stores/meetings.ts
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/hooks/useMeetingDetailData.ts
  • web/src/pages/MeetingsPage.tsx
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/utils/meetings.ts
  • web/src/pages/MeetingDetailPage.tsx
web/src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.tsx: ALWAYS reuse existing shared components from web/src/components/ui/ before creating new ones (e.g., StatusBadge, MetricCard, AgentCard, DeptHealthBar, SectionCard)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success) or CSS variables (var(--so-accent)); NEVER hardcode hex values or rgba() in .tsx files
Use font-sans or font-mono for typography (maps to Geist tokens); NEVER set fontFamily directly in CSS
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing
Do NOT recreate complex (>8 line) JSX inside .map() blocks; extract to a shared component in web/src/components/ui/

Files:

  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/pages/MeetingsPage.tsx
  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/pages/MeetingDetailPage.tsx
web/src/pages/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Lazy-load page components and place page-scoped sub-components in pages/<page-name>/ subdirectories (e.g., pages/tasks/, pages/org-edit/)

Files:

  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/pages/MeetingsPage.tsx
  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/pages/MeetingDetailPage.tsx
web/src/components/ui/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/components/ui/**/*.tsx: New shared components must have a corresponding .stories.tsx Storybook file with all component states (default, hover, loading, error, empty)
Export component props as a TypeScript interface
Use design token variables exclusively for shadows and borders (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values

Files:

  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/components/ui/token-usage-bar.tsx
🧠 Learnings (12)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : New shared components must have a corresponding `.stories.tsx` Storybook file with all component states (default, hover, loading, error, empty)
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : ALWAYS reuse existing shared components from `web/src/components/ui/` before creating new ones (e.g., `StatusBadge`, `MetricCard`, `AgentCard`, `DeptHealthBar`, `SectionCard`)

Applied to files:

  • CLAUDE.md
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/pages/MeetingDetailPage.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design token variables exclusively for shadows and borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); never hardcode shadow or border values

Applied to files:

  • CLAUDE.md
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/components/ui/token-usage-bar.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to **/*.{ts,tsx} : Use camelCase for variable names and function identifiers

Applied to files:

  • web/src/utils/format.ts
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Use density-aware spacing tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/components/ui/token-usage-bar.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`) or CSS variables (`var(--so-accent)`); NEVER hardcode hex values or rgba() in .tsx files

Applied to files:

  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : New shared components must have a corresponding `.stories.tsx` Storybook file with all component states (default, hover, loading, error, empty)

Applied to files:

  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
📚 Learning: 2026-03-20T08:28:32.845Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T08:28:32.845Z
Learning: Applies to web/src/__tests__/**/*.{ts,js} : Dashboard testing: Vitest unit tests organized by feature under `web/src/__tests__/`. Use fast-check for property-based testing (`fc.assert` + `fc.property`).

Applied to files:

  • web/src/__tests__/hooks/useMeetingsData.test.ts
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/__tests__/utils/meetings.test.ts
  • web/src/__tests__/stores/meetings.test.ts
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Do NOT recreate complex (>8 line) JSX inside `.map()` blocks; extract to a shared component in `web/src/components/ui/`

Applied to files:

  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/pages/meetings/MeetingDecisions.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.{ts,tsx} : Import `cn` from `@/lib/utils` for conditional class merging in components

Applied to files:

  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : Export component props as a TypeScript interface

Applied to files:

  • web/src/components/ui/token-usage-bar.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to **/*.{ts,tsx} : Prefer `interface` for defining object shapes in TypeScript

Applied to files:

  • web/src/components/ui/token-usage-bar.tsx
🔇 Additional comments (32)
CLAUDE.md (1)

268-268: LGTM!

The TokenUsageBar component inventory entry is well-documented, following the established table format with clear description of its functionality (segmented meter, auto-colors, ARIA support, animated transitions).

web/src/utils/format.ts (1)

85-91: LGTM!

The enhancement to support kebab-case in addition to snake_case is a sensible extension. The regex /[_-]/ correctly handles both delimiters, and the docstring accurately documents the change.

web/src/api/types.ts (1)

896-899: LGTM!

The MEETING_STATUS_VALUES constant follows the established pattern for runtime value arrays in this file (e.g., SENIORITY_LEVEL_VALUES, AGENT_STATUS_VALUES). The as const satisfies readonly MeetingStatus[] pattern ensures type safety while providing runtime access.

web/src/pages/meetings/MeetingsSkeleton.tsx (1)

1-31: Overall structure looks good.

The skeleton correctly mirrors the expected page layout (header, metrics, timeline, card list) with appropriate ARIA labeling for accessibility. Component usage follows established patterns.

web/src/pages/meetings/MeetingsSkeleton.stories.tsx (1)

1-13: LGTM!

The Storybook story follows proper conventions for Storybook 10 with typed Meta/StoryObj and tags: ['autodocs']. For a skeleton component, a single Default story is appropriate since there are no additional states to demonstrate.

web/src/pages/meetings/MeetingDetailSkeleton.tsx (1)

1-36: LGTM!

The skeleton component correctly mirrors the meeting detail page structure with proper ARIA labeling. The responsive grid uses the standard lg: breakpoint as recommended, and all spacing follows Tailwind token conventions.

web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx (1)

1-13: LGTM!

Consistent with the other skeleton story files, properly typed and following Storybook 10 conventions.

web/src/pages/meetings/TriggerMeetingDialog.tsx (1)

1-62: LGTM!

The dialog component is well-structured:

  • Proper validation with clear error messaging
  • State reset on dialog close prevents stale data
  • Reuses existing ConfirmDialog and InputField components
  • Loading state forwarded correctly to prevent double-submission
web/src/pages/meetings/TriggerMeetingDialog.stories.tsx (1)

1-28: LGTM!

The Storybook file is well-structured with proper TypeScript typing using satisfies Meta<...> and covers the key dialog states (open and loading).

web/src/stores/meetings.ts (2)

115-141: WebSocket payload validation is appropriately defensive.

The runtime validation before upsert correctly guards against malformed payloads since WsEvent.payload is typed as Record<string, unknown>. The field checks cover essential MeetingResponse properties needed for rendering.


143-158: Array update logic is clean.

The upsertMeeting implementation correctly uses map() for immutable updates as previously suggested, and the conditional total increment only applies for new insertions.

web/src/hooks/useMeetingDetailData.ts (1)

1-51: LGTM!

The hook correctly:

  • Triggers fetch on mount and when meetingId changes
  • Creates stable WebSocket bindings with useMemo
  • Reads reactive state from the store
  • Exposes WebSocket connection status for UI feedback
web/src/hooks/useMeetingsData.ts (1)

21-72: LGTM!

The hook correctly orchestrates:

  • Initial fetch on mount using getState() to avoid stale closures
  • Polling lifecycle with explicit start()/stop() calls (matching usePolling contract per context snippet)
  • Stable WebSocket bindings via useMemo
  • Granular store selectors for optimal re-render behavior
web/src/__tests__/hooks/useMeetingsData.test.ts (1)

47-111: Good test coverage for hook state forwarding.

The tests verify all key behaviors: mount-time fetch, polling lifecycle, store state forwarding, and WebSocket setup.

web/src/utils/meetings.ts (1)

1-158: LGTM!

Utilities are well-structured:

  • Color/label maps use semantic classes per guidelines
  • Token calculations properly clamp to [0, 100] range
  • formatMeetingDuration correctly handles null/invalid values and delegates to formatUptime (which expects seconds per context snippet)
  • Filtering and aggregation helpers are straightforward
web/src/pages/meetings/MeetingFilterBar.tsx (1)

1-59: Good component structure and guideline compliance.

The component correctly reuses SelectField from shared components, uses interface for props, and applies semantic Tailwind classes for layout.

web/src/pages/meetings/MeetingFilterBar.stories.tsx (1)

1-27: LGTM!

The Storybook file provides good coverage with Default (empty filters) and WithActiveFilters (pre-selected values) stories, following CSF conventions with proper TypeScript typing.

web/src/pages/meetings/MeetingDetailHeader.tsx (1)

22-92: Header composition and null-guarding look solid.

Good use of shared primitives and safe handling of nullable minutes fields.

web/src/pages/meetings/MeetingTimelineNode.stories.tsx (1)

16-38: Status-variant story coverage is complete.

Nice inclusion of BudgetExhausted; this closes the full status matrix for the node.

web/src/pages/meetings/ContributionBubble.stories.tsx (1)

24-68: Phase-based story variants are well structured.

The shared base fixture plus targeted overrides keeps the scenarios clear and maintainable.

web/src/pages/meetings/MeetingCard.stories.tsx (1)

16-34: Story scenarios are practical and cover key card states.

The NoMinutes and failure variants are especially useful for regression coverage.

web/src/pages/meetings/MeetingDecisions.stories.tsx (1)

13-31: Decision-list story coverage is clean and sufficient.

Including the empty case is a good guard for null-render behavior.

web/src/pages/meetings/MeetingAgendaSection.stories.tsx (1)

24-46: Agenda story matrix is well covered.

Default/minimal/empty variants should give stable confidence for rendering paths.

web/src/__tests__/helpers/factories.ts (1)

126-179: makeMeeting is a strong shared fixture addition.

This should reduce duplication and keep meeting tests/stories consistent.

web/src/pages/meetings/MeetingAgendaSection.tsx (1)

26-43: Key collision risk and complex JSX in .map() remain unaddressed.

The key agenda-${item.title} can collide when two agenda items share the same title. Additionally, the 17-line JSX block inside .map() exceeds the 8-line guideline threshold.

As per coding guidelines, web/src/**/*.tsx: Do NOT recreate complex (>8 line) JSX inside .map() blocks; extract to a shared component.

Suggested fix for key collision
-            {agenda.items.map((item, idx) => (
-              <li key={`agenda-${item.title}`} className="flex gap-3">
+            {agenda.items.map((item, idx) => (
+              <li key={`agenda-${idx}-${item.title}`} className="flex gap-3">
web/src/pages/meetings/MeetingDecisions.tsx (1)

15-16: Key collision risk persists.

Adding decision.length helps but doesn't eliminate collisions—two decisions with the same 40-character prefix and identical length will still collide. Since this is an immutable list, including idx is the simplest fix.

Suggested fix
-          <li key={`decision-${decision.slice(0, 40)}-${decision.length}`} className="flex items-start gap-2">
+          <li key={`decision-${idx}`} className="flex items-start gap-2">
web/src/pages/meetings/MeetingActionItems.tsx (1)

19-38: Key collision risk and complex JSX in .map() remain.

The key ${item.assignee_id ?? 'unassigned'}-${item.description.slice(0, 40)}-${item.priority} can still collide when two action items share the same assignee, description prefix, and priority. Additionally, the 18-line JSX block inside .map() exceeds the 8-line guideline threshold.

As per coding guidelines, web/src/**/*.tsx: Do NOT recreate complex (>8 line) JSX inside .map() blocks; extract to a shared component.

Suggested key fix (include idx)
-          <li key={`${item.assignee_id ?? 'unassigned'}-${item.description.slice(0, 40)}-${item.priority}`} className="flex items-start gap-3">
+          <li key={`action-${idx}`} className="flex items-start gap-3">
web/src/pages/meetings/MeetingActionItems.stories.tsx (1)

1-33: LGTM!

Good coverage with Default, SingleItem, and Empty stories. The test data appropriately includes mixed assignee_id values (including null) and varied priorities to exercise different rendering paths.

web/src/pages/meetings/MeetingTimeline.tsx (1)

1-32: LGTM!

Clean implementation with proper use of meeting.meeting_id as a stable key, appropriate reuse of StaggerGroup/StaggerItem components, and a sensible early return for empty state.

web/src/pages/meetings/MeetingMetricCards.tsx (1)

10-31: LGTM!

Good reuse of existing MetricCard, StaggerGroup, and StaggerItem components. The responsive grid layout and locale-aware token formatting are well implemented.

web/src/pages/meetings/MeetingTimelineNode.tsx (1)

1-58: LGTM!

Well-structured component with:

  • Proper null handling via optional chaining (meeting.minutes?.participant_ids.length ?? 0)
  • Good accessibility attributes (aria-label, aria-hidden)
  • Complete status color coverage (verified via STATUS_DOT_CLASSES and MEETING_STATUS_COLOR_MAP)
  • Visual feedback with animate-pulse for in-progress meetings
web/src/__tests__/stores/meetings.test.ts (1)

43-53: No action required—the listMeetings contract is already correctly implemented and tested.

The store's fetchMeetings at lines 56-73 correctly unpacks the paginated result (using result.data and result.total), and the test mocks the paginated response structure appropriately. The mocks and assertions are consistent with the actual API contract.

			> Likely an incorrect or invalid review comment.

Comment on lines +97 to +101
{(wsSetupError || (wasConnectedRef.current && !wsConnected)) && !loading && (
<div className="flex items-center gap-2 rounded-lg border border-warning/30 bg-warning/5 px-4 py-2 text-sm text-warning">
<WifiOff className="size-4 shrink-0" />
{wsSetupError ?? 'Real-time updates disconnected. Data may be stale.'}
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Announce the websocket warning to assistive tech.

This banner can appear after initial render when the socket drops, but it has no
live-region semantics. Add role="status" with aria-live="polite" (or
role="alert" if you want it treated as urgent) so screen-reader users are told
that realtime data is stale.

🔧 Minimal fix
-      {(wsSetupError || (wasConnectedRef.current && !wsConnected)) && !loading && (
-        <div className="flex items-center gap-2 rounded-lg border border-warning/30 bg-warning/5 px-4 py-2 text-sm text-warning">
+      {(wsSetupError || (wasConnectedRef.current && !wsConnected)) && !loading && (
+        <div
+          role="status"
+          aria-live="polite"
+          className="flex items-center gap-2 rounded-lg border border-warning/30 bg-warning/5 px-4 py-2 text-sm text-warning"
+        >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{(wsSetupError || (wasConnectedRef.current && !wsConnected)) && !loading && (
<div className="flex items-center gap-2 rounded-lg border border-warning/30 bg-warning/5 px-4 py-2 text-sm text-warning">
<WifiOff className="size-4 shrink-0" />
{wsSetupError ?? 'Real-time updates disconnected. Data may be stale.'}
</div>
{(wsSetupError || (wasConnectedRef.current && !wsConnected)) && !loading && (
<div
role="status"
aria-live="polite"
className="flex items-center gap-2 rounded-lg border border-warning/30 bg-warning/5 px-4 py-2 text-sm text-warning"
>
<WifiOff className="size-4 shrink-0" />
{wsSetupError ?? 'Real-time updates disconnected. Data may be stale.'}
</div>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/MeetingsPage.tsx` around lines 97 - 101, The
realtime-disconnected banner rendered when (wsSetupError ||
(wasConnectedRef.current && !wsConnected)) && !loading lacks live-region
semantics; update the banner div (the element that currently contains WifiOff
and the {wsSetupError ?? ...} text) to include role="status" and
aria-live="polite" (or role="alert" if you prefer urgent announcement) so screen
readers are notified when websocket warnings appear.

Aureliolo and others added 4 commits March 28, 2026 17:09
…tting (#788)

Implement the full Meetings page and Meeting Detail page for the web
dashboard, replacing placeholder stubs with functional components.

List page features:
- Horizontal timeline visualization with status-colored meeting nodes
- Filterable meeting card grid (status + meeting type URL-synced filters)
- Summary metric cards (total, in-progress, completed, total tokens)
- Trigger meeting dialog (POST /meetings/trigger)
- Real-time updates via WebSocket (meetings channel) + 30s polling
- Loading skeleton, empty states, error/WS disconnect banners

Detail page features:
- Meeting header with back navigation, status badge, protocol label, stats
- Agenda section with numbered items and presenter info
- Token usage breakdown with stacked TokenUsageBar and per-participant bars
- Formatted contribution dialogue grouped by phase with agent avatars
- Decisions list and action items with priority badges
- Summary section, in-progress/scheduled notices, error display
- Each section wrapped in ErrorBoundary

New shared component:
- TokenUsageBar (components/ui/) -- reusable horizontal stacked progress bar

Foundation:
- Zustand store with stale-request prevention (listRequestSeq/detailRequestSeq)
- useMeetingsData and useMeetingDetailData hooks
- Meeting utility functions (status colors/labels, token math, filtering)
- makeMeeting test factory

Tests: utils, store, hooks, both pages (1724 total, all passing)
Storybook: stories for all new visual components (11 story files)

Closes #788

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pre-reviewed by 5 agents, 17 findings addressed:

- Strengthen WS payload validation (check token_budget, contribution_rank,
  token_usage_by_participant in addition to basic string fields)
- Add stale-request error logging instead of silent discard
- Remove unused triggerError store field (re-throw pattern is sufficient)
- Add debug logging for non-meeting WS events
- DRY: derive filter bar status options from getMeetingStatusLabel
- DRY: use formatLabel() consistently for meeting type names
- Add validation error feedback to TriggerMeetingDialog
- Handle missing meetingId with EmptyState instead of blank page
- Add ErrorBoundary wrappers to MeetingsPage sections
- Add CLAUDE.md component inventory entry for TokenUsageBar
- Add missing Storybook stories for 5 sub-components
- Add stale-response race condition test for fetchMeetings
- Add WS payload guard tests (array, primitive, missing fields)
- Add NaN/Infinity edge case tests for formatMeetingDuration
- Fix no-op WS disconnect test
- Add eslint-disable justification comment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tems)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Aureliolo Aureliolo force-pushed the feat/meetings-page branch from b30b032 to cb26f1c Compare March 28, 2026 16:09
Copy link
Copy Markdown

@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: 13

♻️ Duplicate comments (5)
web/src/pages/meetings/MeetingActionItems.tsx (1)

19-37: ⚠️ Potential issue | 🟠 Major

Extract mapped row component and fix collision-prone key.

Line 20 uses a non-unique derived key, and Lines 20-37 keep a large JSX block inside
.map(). This risks unstable reconciliation and violates the component-mapping rule.

Proposed refactor in this file
+import { ActionItemRow } from '@/components/ui/action-item-row'
@@
-        {actionItems.map((item) => (
-          <li key={`${item.assignee_id ?? 'unassigned'}-${item.description.slice(0, 40)}-${item.priority}`} className="flex items-start gap-3">
-            {item.assignee_id ? (
-              <Avatar name={item.assignee_id} size="sm" />
-            ) : (
-              <div className="flex size-6 items-center justify-center rounded-full bg-border text-micro text-muted-foreground">
-                ?
-              </div>
-            )}
-            <div className="min-w-0 flex-1">
-              <p className="text-sm text-foreground">{item.description}</p>
-              <div className="mt-1 flex items-center gap-2">
-                <PriorityBadge priority={item.priority} />
-                <span className="text-micro text-muted-foreground">
-                  {item.assignee_id ? formatLabel(item.assignee_id) : 'Unassigned'}
-                </span>
-              </div>
-            </div>
-          </li>
+        {actionItems.map((item, idx) => (
+          <ActionItemRow
+            key={`${item.assignee_id ?? 'unassigned'}-${idx}`}
+            item={item}
+          />
         ))}

As per coding guidelines, web/src/**/*.tsx: Do NOT recreate complex (>8 line)
JSX inside .map() blocks; extract to a shared component in
web/src/components/ui/.

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

In `@web/src/pages/meetings/MeetingActionItems.tsx` around lines 19 - 37, The
mapped JSX for actionItems should be extracted into a new presentational
component (e.g., MeetingActionItemRow) instead of inlining >8 lines inside
actionItems.map; create MeetingActionItemRow in web/src/components/ui/ that
accepts the item and helper props (assignee_id, description, priority,
formatLabel) and renders the Avatar/unknown avatar, description, PriorityBadge
and label, then replace the inline JSX with <MeetingActionItemRow item={item} />
in the map; also fix the unstable key by using a stable unique identifier from
the item (e.g., item.id or a combination like `${item.id}-${item.created_at}`)
instead of the collision-prone `${item.assignee_id ??
'unassigned'}-${item.description.slice(0,40)}-${item.priority}` so React
reconciliation is reliable.
web/src/pages/meetings/MeetingContributions.tsx (1)

39-40: ⚠️ Potential issue | 🟡 Minor

Potential key collision still exists for phase groups.

The current outer key can still duplicate when the same phase appears again with the
same first turn_number and group length, which can cause unstable reconciliation.

🔧 Proposed fix
-        {groups.map((group) => (
-          <div key={`${group.phase}-${group.items[0]?.turn_number ?? 0}-${group.items.length}`}>
+        {groups.map((group, groupIndex) => (
+          <div key={`${groupIndex}-${group.phase}`}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/meetings/MeetingContributions.tsx` around lines 39 - 40, The
outer key used in groups.map (currently using
`${group.phase}-${group.items[0]?.turn_number ?? 0}-${group.items.length}`) can
collide when the same phase repeats with identical first turn_number and length;
update the key to include a truly unique identifier for the group (for example
use an existing stable property like group.id if present, or append the map
index from groups.map as a fallback) so the key becomes unique across repeated
phases and prevents reconciliation issues.
web/src/pages/meetings/MeetingDecisions.tsx (1)

15-16: ⚠️ Potential issue | 🟡 Minor

Key collision risk persists.

The current key decision-${decision.slice(0, 40)}-${decision.length} can still collide when two decisions share the same 40-character prefix and total length. Since this list represents immutable meeting output that won't be reordered, using the index is the simplest safe approach.

♻️ Suggested fix
-          <li key={`decision-${decision.slice(0, 40)}-${decision.length}`} className="flex items-start gap-2">
+          <li key={idx} className="flex items-start gap-2">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/meetings/MeetingDecisions.tsx` around lines 15 - 16, The
generated key for list items in MeetingDecisions.tsx (inside the decisions.map
callback) can still collide; replace the computed key
`decision-${decision.slice(0, 40)}-${decision.length}` with a stable index-based
key (use the map index `idx`) since the decisions array is immutable and won't
be reordered—update the JSX inside the decisions.map to use `key={idx}` (or
`key={\`decision-${idx}\`}`) so each <li> has a unique, stable key.
web/src/pages/meetings/MeetingAgendaSection.tsx (1)

26-43: 🛠️ Refactor suggestion | 🟠 Major

Extract the agenda item row and use a collision-safe key.

The JSX inside .map() spans ~17 lines, exceeding the 8-line guideline. Additionally, keying by agenda-${item.title} alone can cause collisions if two agenda items share the same title.

♻️ Suggested refactor

Extract the row to a separate component and use a collision-safe key:

+interface AgendaItemRowProps {
+  item: MeetingAgenda['items'][number]
+  index: number
+}
+
+function AgendaItemRow({ item, index }: AgendaItemRowProps) {
+  return (
+    <li className="flex gap-3">
+      <span className="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full bg-accent/10 font-mono text-micro font-medium text-accent">
+        {index + 1}
+      </span>
+      <div className="min-w-0 flex-1">
+        <p className="text-sm font-medium text-foreground">{item.title}</p>
+        {item.description && (
+          <p className="text-xs text-muted-foreground">{item.description}</p>
+        )}
+        {item.presenter_id && (
+          <p className="mt-0.5 text-micro text-muted-foreground">
+            Presenter: {formatLabel(item.presenter_id)}
+          </p>
+        )}
+      </div>
+    </li>
+  )
+}

Then update the map:

           <ol className="space-y-3">
             {agenda.items.map((item, idx) => (
-              <li key={`agenda-${item.title}`} className="flex gap-3">
-                <span className="...">
-                  {idx + 1}
-                </span>
-                <div className="min-w-0 flex-1">
-                  ...
-                </div>
-              </li>
+              <AgendaItemRow
+                key={`agenda-${item.title}-${idx}`}
+                item={item}
+                index={idx}
+              />
             ))}
           </ol>

As per coding guidelines, web/src/**/*.tsx: Do NOT recreate complex (>8 line) JSX inside .map() blocks; extract to a shared component.

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

In `@web/src/pages/meetings/MeetingAgendaSection.tsx` around lines 26 - 43, The
map callback in MeetingAgendaSection renders a ~17-line JSX block; extract that
block into a new component (e.g., MeetingAgendaItemRow) that accepts props
{item, index} and reuses formatLabel for presenter display, then replace the
inline JSX in agenda.items.map(...) with a single <MeetingAgendaItemRow /> call;
also replace the fragile key={`agenda-${item.title}`} with a collision-safe key
such as key={`agenda-${item.id ?? idx}`} (or use a stable unique id field on
item) to avoid title collisions.
web/src/components/ui/token-usage-bar.tsx (1)

42-57: ⚠️ Potential issue | 🟡 Minor

Ensure unique keys when segment labels can repeat.

Using segment.label as the key assumes labels are unique. If callers pass duplicate labels (e.g., multiple agents with the same display name), React will warn about duplicate keys and may exhibit unexpected behavior during updates.

🔧 Proposed fix
           {visible.map((segment, i) => (
             <div
-              key={segment.label}
+              key={`${segment.label}-${i}`}
               className={cn(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/token-usage-bar.tsx` around lines 42 - 57, The key for
the mapped segments uses segment.label which can be non-unique; change the key
to a stable unique identifier (e.g., combine segment.label with the loop index
or a segment.id if available) in the visible.map callback so React keys are
unique and stable—update the key prop in the JSX where visible.map((segment, i)
=> ...) (the div with key={...}) to use a composite like `${segment.label}-${i}`
or prefer segment.id when present.
🤖 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/useMeetingsData.test.ts`:
- Line 96: The test's const fakeEvent declaration in useMeetingsData.test.ts is
a single long line exceeding 88 chars; split the object literal across multiple
lines (one property per line) so const fakeEvent = { channel: 'meetings' as
const, event_type: 'meeting.completed' as const, payload: {}, timestamp: new
Date().toISOString() } becomes a multiline object with channel, event_type,
payload, and timestamp on separate lines to satisfy the line-length rule while
keeping the same types and values.

In `@web/src/__tests__/stores/meetings.test.ts`:
- Around line 252-265: The test for "ignores event where meeting is an array" is
missing an assertion that console.debug was invoked; update the test to assert
that the spy (debugSpy) was called after invoking
useMeetingsStore.getState().handleWsEvent(event) so it mirrors the other test
pattern — i.e., keep the vi.spyOn(console, 'debug') mock, call handleWsEvent on
the meetings store with the event payload, then add an assertion that debugSpy
was called before calling debugSpy.mockRestore().

In `@web/src/__tests__/utils/meetings.test.ts`:
- Around line 139-143: The test mutates the factory-created object
(meeting.minutes!.total_tokens) which can introduce brittleness; instead call
makeMeeting with an override that sets minutes.total_tokens to 0 (or create a
deep-cloned meeting before mutating) so the factory output isn't mutated; update
the test that calls getParticipantTokenShare to use the override via
makeMeeting(...) (or clone) and verify it returns 0, referencing the makeMeeting
factory and the getParticipantTokenShare helper and the minutes.total_tokens
field.

In `@web/src/pages/meetings/MeetingCard.stories.tsx`:
- Line 10: The decorators array entry in MeetingCard.stories.tsx exceeds the
88-char line limit; split the JSX across multiple lines for readability by
formatting the decorators declaration so the array and the function component
are on separate lines (referencing decorators, Story and MemoryRouter), e.g.,
place the arrow function on its own line and break the JSX wrapper
(<MemoryRouter>...<Story />...</MemoryRouter>) into multiple indented lines to
keep each line under 88 characters.

In `@web/src/pages/meetings/MeetingContributions.stories.tsx`:
- Around line 16-17: The story data lines in MeetingContributions.stories.tsx
exceed the 88-char line length; break or wrap long object/property lines (e.g.,
the object with keys agent_id, content, phase, turn_number, input_tokens,
output_tokens, timestamp) so each source line is <=88 chars — for example place
each property on its own line, wrap long string values using template literals
or concatenation, or split arrays/objects across multiple lines; apply the same
wrapping to the other long story entries noted (around lines with the same
property names) to satisfy the repo max line length rule.

In `@web/src/pages/meetings/MeetingContributions.tsx`:
- Around line 39-56: Extract the large JSX block inside groups.map into a new
shared component (e.g., PhaseGroup or MeetingPhaseGroup) that accepts the group
object as a prop and renders the header with getPhaseLabel(group.phase), the
wrapper div with the same composite key logic, the StaggerGroup, StaggerItem
mapping, and ContributionBubble rendering; then replace the inline JSX in
groups.map with a single <PhaseGroup group={group} /> (or similar) and ensure
the new component preserves the same keys:
`${group.phase}-${group.items[0]?.turn_number ?? 0}-${group.items.length}` for
the outer wrapper and
`${contribution.agent_id}-${contribution.phase}-${contribution.turn_number}` for
each StaggerItem.

In `@web/src/pages/meetings/MeetingDetailHeader.tsx`:
- Around line 46-53: This code reimplements a status pill instead of using the
shared StatusBadge component; replace the custom <span> block in
MeetingDetailHeader (the element that uses statusBadgeClass and
getMeetingStatusLabel(meeting.status)) with the shared StatusBadge component,
passing the meeting.status (and any needed className/props) so the
label/appearance comes from the shared UI; add the StatusBadge import at the top
of the file and remove the custom span markup and statusBadgeClass-only styling
in favor of the shared component API.

In `@web/src/pages/meetings/MeetingMetricCards.tsx`:
- Line 18: The StaggerGroup JSX is concatenating className via template literal
which can mis-handle falsy values and duplicate classes; import the cn utility
from '@/lib/utils' and replace the template literal on the StaggerGroup element
to call cn('grid grid-cols-2 gap-grid-gap lg:grid-cols-4', className) so classes
are merged correctly; update the import list to include cn and modify the
StaggerGroup usage (reference: StaggerGroup and the className prop).

In `@web/src/pages/meetings/MeetingTimelineNode.stories.tsx`:
- Around line 20-26: Lines for the Story exports InProgress and Scheduled exceed
the 88-char limit; break the long args object expressions used with makeMeeting
into multiple lines so each property fits within the limit. Locate the
InProgress and Scheduled constants (export const InProgress: Story and export
const Scheduled: Story) and reformat their args: place the makeMeeting call on
its own line and put the overrides object on multiple lines (e.g., status and
meeting_type_name each on separate lines) while preserving the Story type,
trailing commas, and original argument order.

In `@web/src/pages/meetings/MeetingTimelineNode.tsx`:
- Around line 3-5: There are two separate imports from '@/utils/format'
(formatRelativeTime and formatLabel); replace them with a single consolidated
import that pulls both symbols from '@/utils/format' so the top of
MeetingTimelineNode.tsx imports formatRelativeTime and formatLabel together
(while leaving the other import getMeetingStatusColor/STATUS_DOT_CLASSES from
'@/utils/meetings' unchanged).

In `@web/src/pages/MeetingsPage.tsx`:
- Around line 92-97: The JSX alert block for the error message in
MeetingsPage.tsx has an overly long className string; locate the conditional
rendering that checks the error variable and split the className into multiple
lines or extract it into a constant (e.g., const ERROR_ALERT_CLASS = "flex
items-center gap-2 rounded-lg border border-danger/30 bg-danger/5 px-4 py-2
text-sm text-danger") and use that constant in the div, ensuring the resulting
JSX lines are <=88 chars and that lint/formatting still passes.
- Line 66: The long single-line call to useToastStore.getState().add(...)
exceeds the 88-char limit; break the expression into multiple lines for
readability and linting by splitting after useToastStore.getState().add( and
placing the toast object properties (variant, title, description) each on their
own indented lines, referencing the existing getErrorMessage(err) for
description and keeping the closing ) on its own line so the call in
MeetingsPage.tsx remains functionally identical.

In `@web/src/utils/meetings.ts`:
- Around line 81-93: computeTokenUsagePercent and getParticipantTokenShare
currently return 0 when meeting.minutes is null even if
meeting.token_usage_by_participant contains data; update both functions to fall
back to a derived total token count when meeting.minutes is missing by summing
Object.values(meeting.token_usage_by_participant) (or 0 if that map is empty)
and use that sum in place of meeting.minutes.total_tokens for percent
calculations; ensure you still respect meeting.token_budget and guard against
division by zero, and consider extracting the fallback-sum logic into a small
helper used by computeTokenUsagePercent and getParticipantTokenShare to avoid
duplication.

---

Duplicate comments:
In `@web/src/components/ui/token-usage-bar.tsx`:
- Around line 42-57: The key for the mapped segments uses segment.label which
can be non-unique; change the key to a stable unique identifier (e.g., combine
segment.label with the loop index or a segment.id if available) in the
visible.map callback so React keys are unique and stable—update the key prop in
the JSX where visible.map((segment, i) => ...) (the div with key={...}) to use a
composite like `${segment.label}-${i}` or prefer segment.id when present.

In `@web/src/pages/meetings/MeetingActionItems.tsx`:
- Around line 19-37: The mapped JSX for actionItems should be extracted into a
new presentational component (e.g., MeetingActionItemRow) instead of inlining >8
lines inside actionItems.map; create MeetingActionItemRow in
web/src/components/ui/ that accepts the item and helper props (assignee_id,
description, priority, formatLabel) and renders the Avatar/unknown avatar,
description, PriorityBadge and label, then replace the inline JSX with
<MeetingActionItemRow item={item} /> in the map; also fix the unstable key by
using a stable unique identifier from the item (e.g., item.id or a combination
like `${item.id}-${item.created_at}`) instead of the collision-prone
`${item.assignee_id ??
'unassigned'}-${item.description.slice(0,40)}-${item.priority}` so React
reconciliation is reliable.

In `@web/src/pages/meetings/MeetingAgendaSection.tsx`:
- Around line 26-43: The map callback in MeetingAgendaSection renders a ~17-line
JSX block; extract that block into a new component (e.g., MeetingAgendaItemRow)
that accepts props {item, index} and reuses formatLabel for presenter display,
then replace the inline JSX in agenda.items.map(...) with a single
<MeetingAgendaItemRow /> call; also replace the fragile
key={`agenda-${item.title}`} with a collision-safe key such as
key={`agenda-${item.id ?? idx}`} (or use a stable unique id field on item) to
avoid title collisions.

In `@web/src/pages/meetings/MeetingContributions.tsx`:
- Around line 39-40: The outer key used in groups.map (currently using
`${group.phase}-${group.items[0]?.turn_number ?? 0}-${group.items.length}`) can
collide when the same phase repeats with identical first turn_number and length;
update the key to include a truly unique identifier for the group (for example
use an existing stable property like group.id if present, or append the map
index from groups.map as a fallback) so the key becomes unique across repeated
phases and prevents reconciliation issues.

In `@web/src/pages/meetings/MeetingDecisions.tsx`:
- Around line 15-16: The generated key for list items in MeetingDecisions.tsx
(inside the decisions.map callback) can still collide; replace the computed key
`decision-${decision.slice(0, 40)}-${decision.length}` with a stable index-based
key (use the map index `idx`) since the decisions array is immutable and won't
be reordered—update the JSX inside the decisions.map to use `key={idx}` (or
`key={\`decision-${idx}\`}`) so each <li> has a unique, stable key.
🪄 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: 79f3f858-40df-4fb9-a5f9-6e7c117d86a8

📥 Commits

Reviewing files that changed from the base of the PR and between b30b032 and cb26f1c.

📒 Files selected for processing (48)
  • CLAUDE.md
  • web/src/__tests__/helpers/factories.ts
  • web/src/__tests__/hooks/useMeetingDetailData.test.ts
  • web/src/__tests__/hooks/useMeetingsData.test.ts
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/__tests__/utils/meetings.test.ts
  • web/src/api/types.ts
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/hooks/useMeetingDetailData.ts
  • web/src/hooks/useMeetingsData.ts
  • web/src/pages/MeetingDetailPage.tsx
  • web/src/pages/MeetingsPage.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/stores/meetings.ts
  • web/src/utils/format.ts
  • web/src/utils/meetings.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
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Backend
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Documentation must be written in Markdown and built with Zensical (config: mkdocs.yml); design spec lives in docs/design/ (11 pages linked from docs/DESIGN_SPEC.md)

Files:

  • CLAUDE.md
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Prefer interface for defining object shapes in TypeScript
Use camelCase for variable names and function identifiers

Files:

  • web/src/utils/format.ts
  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/api/types.ts
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/__tests__/hooks/useMeetingsData.test.ts
  • web/src/__tests__/helpers/factories.ts
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/__tests__/hooks/useMeetingDetailData.test.ts
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/hooks/useMeetingDetailData.ts
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/hooks/useMeetingsData.ts
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/pages/MeetingDetailPage.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/__tests__/utils/meetings.test.ts
  • web/src/pages/MeetingsPage.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/utils/meetings.ts
  • web/src/stores/meetings.ts
**/*.{py,ts,tsx,go}

📄 CodeRabbit inference engine (CLAUDE.md)

Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript)

Files:

  • web/src/utils/format.ts
  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/api/types.ts
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/__tests__/hooks/useMeetingsData.test.ts
  • web/src/__tests__/helpers/factories.ts
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/__tests__/hooks/useMeetingDetailData.test.ts
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/hooks/useMeetingDetailData.ts
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/hooks/useMeetingsData.ts
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/pages/MeetingDetailPage.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/__tests__/utils/meetings.test.ts
  • web/src/pages/MeetingsPage.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/utils/meetings.ts
  • web/src/stores/meetings.ts
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Import cn from @/lib/utils for conditional class merging in components

Files:

  • web/src/utils/format.ts
  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/api/types.ts
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/__tests__/hooks/useMeetingsData.test.ts
  • web/src/__tests__/helpers/factories.ts
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/__tests__/hooks/useMeetingDetailData.test.ts
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/hooks/useMeetingDetailData.ts
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/hooks/useMeetingsData.ts
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/pages/MeetingDetailPage.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/__tests__/utils/meetings.test.ts
  • web/src/pages/MeetingsPage.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
  • web/src/utils/meetings.ts
  • web/src/stores/meetings.ts
web/src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.tsx: ALWAYS reuse existing shared components from web/src/components/ui/ before creating new ones (e.g., StatusBadge, MetricCard, AgentCard, DeptHealthBar, SectionCard)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success) or CSS variables (var(--so-accent)); NEVER hardcode hex values or rgba() in .tsx files
Use font-sans or font-mono for typography (maps to Geist tokens); NEVER set fontFamily directly in CSS
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing
Do NOT recreate complex (>8 line) JSX inside .map() blocks; extract to a shared component in web/src/components/ui/

Files:

  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/pages/MeetingDetailPage.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/pages/MeetingsPage.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
web/src/pages/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Lazy-load page components and place page-scoped sub-components in pages/<page-name>/ subdirectories (e.g., pages/tasks/, pages/org-edit/)

Files:

  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/MeetingContributions.tsx
  • web/src/pages/meetings/MeetingTimeline.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/ContributionBubble.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/pages/meetings/MeetingDetailHeader.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
  • web/src/pages/MeetingDetailPage.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/pages/MeetingsPage.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
web/src/components/ui/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/components/ui/**/*.tsx: New shared components must have a corresponding .stories.tsx Storybook file with all component states (default, hover, loading, error, empty)
Export component props as a TypeScript interface
Use design token variables exclusively for shadows and borders (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values

Files:

  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/components/ui/token-usage-bar.tsx
🧠 Learnings (12)
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : ALWAYS reuse existing shared components from `web/src/components/ui/` before creating new ones (e.g., `StatusBadge`, `MetricCard`, `AgentCard`, `DeptHealthBar`, `SectionCard`)

Applied to files:

  • CLAUDE.md
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/MeetingDetailPage.tsx
  • web/src/pages/meetings/MeetingCard.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design token variables exclusively for shadows and borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); never hardcode shadow or border values

Applied to files:

  • CLAUDE.md
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/components/ui/token-usage-bar.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to **/*.{ts,tsx} : Use camelCase for variable names and function identifiers

Applied to files:

  • web/src/utils/format.ts
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Use density-aware spacing tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/components/ui/token-usage-bar.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`) or CSS variables (`var(--so-accent)`); NEVER hardcode hex values or rgba() in .tsx files

Applied to files:

  • web/src/pages/meetings/MeetingsSkeleton.tsx
  • web/src/pages/meetings/MeetingDetailSkeleton.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : New shared components must have a corresponding `.stories.tsx` Storybook file with all component states (default, hover, loading, error, empty)

Applied to files:

  • web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx
  • web/src/pages/meetings/MeetingsSkeleton.stories.tsx
  • web/src/pages/meetings/TriggerMeetingDialog.stories.tsx
  • web/src/pages/meetings/MeetingAgendaSection.stories.tsx
  • web/src/pages/meetings/MeetingDecisions.stories.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
  • web/src/pages/meetings/MeetingActionItems.stories.tsx
  • web/src/pages/meetings/ContributionBubble.stories.tsx
  • web/src/pages/meetings/MeetingCard.stories.tsx
  • web/src/pages/meetings/MeetingContributions.stories.tsx
  • web/src/pages/meetings/MeetingTimelineNode.stories.tsx
  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/components/ui/token-usage-bar.stories.tsx
  • web/src/pages/meetings/MeetingMetricCards.stories.tsx
  • web/src/pages/meetings/MeetingTimeline.stories.tsx
  • web/src/pages/meetings/MeetingFilterBar.stories.tsx
  • web/src/pages/meetings/MeetingDetailHeader.stories.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Do NOT recreate complex (>8 line) JSX inside `.map()` blocks; extract to a shared component in `web/src/components/ui/`

Applied to files:

  • web/src/pages/meetings/MeetingDecisions.tsx
  • web/src/pages/meetings/MeetingAgendaSection.tsx
  • web/src/pages/meetings/MeetingActionItems.tsx
  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
  • web/src/pages/MeetingDetailPage.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/pages/MeetingsPage.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to **/*.{py,ts,tsx,go} : Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript)

Applied to files:

  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
📚 Learning: 2026-03-21T11:08:01.542Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T11:08:01.542Z
Learning: Applies to **/*.{py,md} : Line length: 88 characters (ruff).

Applied to files:

  • web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx
📚 Learning: 2026-03-20T08:28:32.845Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T08:28:32.845Z
Learning: Applies to web/src/__tests__/**/*.{ts,js} : Dashboard testing: Vitest unit tests organized by feature under `web/src/__tests__/`. Use fast-check for property-based testing (`fc.assert` + `fc.property`).

Applied to files:

  • web/src/__tests__/pages/MeetingDetailPage.test.tsx
  • web/src/__tests__/pages/MeetingsPage.test.tsx
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/__tests__/utils/meetings.test.ts
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.{ts,tsx} : Import `cn` from `@/lib/utils` for conditional class merging in components

Applied to files:

  • web/src/pages/meetings/MeetingTokenBreakdown.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : Export component props as a TypeScript interface

Applied to files:

  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/components/ui/token-usage-bar.tsx
  • web/src/pages/meetings/MeetingFilterBar.tsx
🔇 Additional comments (44)
web/src/utils/format.ts (1)

85-90: Good extension of label formatting coverage.

Expanding formatLabel to support both snake_case and kebab-case is a clean, low-risk improvement for UI labels.

web/src/api/types.ts (1)

896-899: Typed status value list is implemented correctly.

Using a const tuple with satisfies readonly MeetingStatus[] is solid and prevents drift between runtime options and the union type.

web/src/__tests__/helpers/factories.ts (1)

126-179: makeMeeting is a strong test fixture addition.

The baseline payload is realistic and override-friendly, which should reduce duplication across meetings tests.

web/src/pages/meetings/MeetingActionItems.stories.tsx (1)

13-33: Story coverage for main states looks good.

Default, single-item, and empty states are all represented, which is the right baseline for this section component.

web/src/pages/meetings/ContributionBubble.tsx (1)

12-50: Component composition and styling are solid.

Phase badge mapping, avatar/header/content structure, and token stats rendering are clean and consistent.

web/src/pages/meetings/ContributionBubble.stories.tsx (1)

24-68: Phase-variant stories are well structured.

These stories exercise the component across key phase contexts and should help catch presentation regressions.

web/src/pages/meetings/MeetingFilterBar.tsx (1)

13-55: Looks good — filter mapping and option generation are clean.

The status/type "All" handling and filter updates are implemented correctly.

web/src/pages/meetings/MeetingFilterBar.stories.tsx (1)

4-27: Story setup is solid.

Typed metadata and practical state variants are in place.

CLAUDE.md (1)

268-268: Good documentation update.

The new TokenUsageBar inventory entry is clear and useful.

web/src/pages/meetings/MeetingDetailSkeleton.stories.tsx (1)

4-13: Looks good.

Typed Storybook config and default state are correctly set up.

web/src/pages/meetings/MeetingDetailHeader.stories.tsx (1)

10-10: ⚠️ Potential issue | 🟡 Minor

Reflow long lines to meet the 88-character limit.

A few lines in decorators/args are over the configured max length.

As per coding guidelines: **/*.{py,ts,tsx,go}: "Lines must not exceed 88
characters."

Also applies to: 21-21, 25-25

⛔ Skipped due to learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to **/*.{py,ts,tsx,go} : Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T11:08:01.542Z
Learning: Applies to **/*.{py,md} : Line length: 88 characters (ruff).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to src/synthorg/**/*.py : Keep functions under 50 lines and files under 800 lines
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Functions must be less than 50 lines; files must be less than 800 lines
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T13:48:18.114Z
Learning: Applies to **/*.py : Functions: < 50 lines, files < 800 lines.
web/src/pages/meetings/MeetingsSkeleton.tsx (1)

1-31: LGTM!

Clean skeleton implementation that follows coding guidelines: uses density-aware spacing tokens (gap-grid-gap, space-y-6), reuses shared UI components, and implements accessible ARIA labeling. The mobile-first responsive grid approach is correctly applied.

web/src/pages/meetings/TriggerMeetingDialog.stories.tsx (1)

1-28: LGTM!

Storybook stories appropriately cover the dialog's primary interactive states (open and loading). The CSF3 pattern with typed Meta and StoryObj is correctly applied.

web/src/pages/meetings/MeetingsSkeleton.stories.tsx (1)

1-13: LGTM!

Single Default story is appropriate for a skeleton component, which inherently represents only the loading state.

web/src/__tests__/hooks/useMeetingsData.test.ts (1)

1-117: Test coverage is comprehensive.

The test suite thoroughly covers mount behavior, state selectors, polling lifecycle, and WebSocket event forwarding. The handler verification (lines 94-98) properly asserts that events are forwarded to handleWsEvent.

web/src/pages/meetings/MeetingDetailSkeleton.tsx (1)

1-36: LGTM!

Well-structured skeleton with proper accessibility labeling, density-aware spacing tokens, and mobile-first responsive layout. Correctly reuses shared skeleton components.

web/src/pages/meetings/MeetingDecisions.stories.tsx (1)

1-31: LGTM!

Stories appropriately cover all rendering branches: multiple decisions, single decision, and the empty state (which triggers the null return). Good coverage for visual testing.

web/src/pages/meetings/MeetingDecisions.tsx (1)

1-26: Component structure is clean.

Good use of SectionCard for consistency, semantic color tokens (text-success, bg-success/10), and early return for empty state. The JSX within .map() is concise and doesn't need extraction. As per coding guidelines, the interface keyword is correctly used for the props type.

web/src/pages/meetings/MeetingCard.stories.tsx (1)

1-34: Good story coverage for meeting card variants.

Stories appropriately cover the component's key visual states: completed, in-progress, failed, scheduled (no minutes), and high token usage. Using makeMeeting factory ensures consistent test data across stories and tests.

web/src/pages/meetings/MeetingAgendaSection.stories.tsx (1)

1-46: LGTM!

The Storybook file is well-structured with proper TypeScript typing using satisfies Meta<> and StoryObj<typeof meta>. The three stories provide good coverage of the component's rendering branches: full agenda with multiple items, minimal single-item agenda, and empty items array.

web/src/pages/meetings/MeetingTimelineNode.stories.tsx (1)

36-38: Good addition of BudgetExhausted story.

This completes the visual coverage for all six MeetingStatus values, addressing the previous review feedback.

web/src/pages/meetings/MeetingTokenBreakdown.stories.tsx (1)

1-36: LGTM!

The stories provide good coverage with default, many participants, and high usage scenarios. The previous line length concerns have been addressed with proper formatting.

web/src/pages/meetings/MeetingTimeline.tsx (1)

1-32: LGTM!

Clean implementation with proper use of cn for class merging, ARIA attributes on decorative elements, and early return for the empty state. The component correctly composes StaggerGroup and MeetingTimelineNode.

web/src/__tests__/hooks/useMeetingDetailData.test.ts (1)

1-101: LGTM!

Comprehensive test coverage for the hook including mount behavior, state mapping from the store, WebSocket channel wiring, error scenarios, and refetch on ID change. Previous review concerns about non-null assertions and wsSetupError coverage have been addressed.

web/src/pages/meetings/MeetingMetricCards.stories.tsx (1)

1-27: LGTM!

The stories provide appropriate coverage with a mix of statuses that exercise the metric calculations (completed and in_progress counts) and an empty state variant.

web/src/components/ui/token-usage-bar.stories.tsx (1)

1-78: Missing hover, loading, and error story variants.

This shared UI component's Storybook file is missing the required hover, loading, and error state stories per project guidelines. However, this has already been flagged in a previous review.

web/src/hooks/useMeetingDetailData.ts (1)

1-51: LGTM!

The hook is well-structured with proper separation of concerns:

  • Correctly uses Zustand selectors for reactive state
  • Effect-based fetch on meetingId changes
  • WebSocket bindings are properly memoized with stable dependencies
  • Return type is explicitly defined via interface
web/src/__tests__/pages/MeetingsPage.test.tsx (1)

1-100: LGTM!

Comprehensive test coverage for the MeetingsPage including:

  • Loading states with and without existing data
  • Error and WebSocket setup error rendering
  • Empty states with and without active filters
  • Metric cards and meeting card rendering

The mock pattern correctly matches the UseMeetingsDataReturn interface, and the beforeEach properly resets state between tests.

web/src/pages/meetings/TriggerMeetingDialog.tsx (1)

1-62: LGTM!

The dialog properly handles:

  • Input validation with trimmed value check
  • State reset on close via handleOpenChange
  • Clearing validation error on user input (Line 54)
  • Delegation to ConfirmDialog for loading state and button disabling

Good integration with the ConfirmDialog component which handles keeping the dialog open on rejection.

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

1-158: Past review issues have been addressed.

Good work addressing the previous review comments:

  • wasConnectedRef mutation moved to useEffect (Lines 36-38)
  • Error is re-thrown in handleTrigger to keep dialog open on failure (Line 67)
  • Empty states suppressed when error is set (Lines 132, 141)
  • WebSocket warning banner has role="status" for accessibility (Line 100)
web/src/pages/meetings/MeetingTimeline.stories.tsx (1)

1-45: LGTM!

Good story coverage for the page-scoped timeline component:

  • FewMeetings with varied statuses and types
  • ManyMeetings for scroll/overflow behavior
  • SingleMeeting edge case
  • Empty state

The MemoryRouter decorator correctly provides routing context for the component.

web/src/__tests__/pages/MeetingDetailPage.test.tsx (1)

1-121: LGTM!

Excellent test coverage for the MeetingDetailPage including:

  • Loading state rendering
  • All content sections (Agenda, Contributions, Token Usage, Decisions, Action Items, Summary)
  • Error states (hook-level error, meeting-level error_message)
  • Status-specific UI (in-progress notice)
  • Navigation elements (back link)
  • WebSocket setup error display

The mock setup correctly matches the UseMeetingDetailDataReturn interface.

web/src/hooks/useMeetingsData.ts (1)

1-72: LGTM!

The hook is well-structured with:

  • Proper initial data fetch on mount
  • Polling with usePolling (30s interval)
  • WebSocket bindings for real-time updates
  • All store state properly exposed via selectors
  • Type-safe channel definition using satisfies readonly WsChannel[]

The getState() pattern in callbacks is correct for avoiding stale closure issues with Zustand.

web/src/pages/meetings/MeetingTokenBreakdown.tsx (1)

1-95: LGTM!

The component is well-structured with good separation of concerns. The ParticipantTokenRow extraction addresses the >8 line JSX guideline, semantic Tailwind classes are used consistently, and shared components (SectionCard, Avatar, TokenUsageBar) are properly reused.

web/src/pages/meetings/MeetingTimelineNode.tsx (1)

14-57: LGTM!

The timeline node component is well-implemented with proper accessibility attributes (aria-label, aria-hidden), semantic Tailwind classes, and clean status-based styling logic.

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

19-151: LGTM!

The page implementation is solid with proper handling of loading, error, and empty states. Previous review concerns have been addressed: wasConnectedRef is now updated in useEffect, alert banners have role="alert" for accessibility, and the loading/skeleton flow is correct.

web/src/components/ui/token-usage-bar.tsx (1)

1-61: Well-implemented accessible meter component.

Good work on the ARIA implementation with clamped aria-valuenow/aria-valuemax and human-readable aria-valuetext. The filtered segment approach correctly handles edge rounding.

web/src/pages/meetings/MeetingCard.tsx (2)

21-101: Good implementation with proper token bar gating.

The component correctly gates the token usage bar on meeting.minutes existence (line 76), preventing misleading 0% bars for meetings without usage data.


48-55: StatusBadge cannot be used here—it's incompatible with MeetingStatus types.

The StatusBadge component is designed specifically for AgentRuntimeStatus (active, idle, error, offline), but MeetingCard uses MeetingStatus (scheduled, in_progress, completed, failed, cancelled, budget_exhausted). The current inline badge implementation with STATUS_BADGE_CLASSES from utils/meetings is the correct approach for meeting statuses.

			> Likely an incorrect or invalid review comment.
web/src/__tests__/stores/meetings.test.ts (1)

1-331: Comprehensive test coverage for the meetings store.

The test suite thoroughly covers all store actions including stale-request handling, WebSocket event processing with various payload shapes, and upsert behavior. Good use of the makeMeeting factory for consistent fixtures.

web/src/__tests__/utils/meetings.test.ts (1)

1-240: Excellent test coverage for meeting utilities.

The test suite comprehensively covers all utility functions with good edge case handling (null, zero, NaN, Infinity). The it.each pattern keeps mapping tests concise and complete.

web/src/stores/meetings.ts (3)

120-146: Well-implemented WebSocket event validation.

The defensive validation in handleWsEvent properly guards against malformed payloads with clear logging for debugging. The shape validation covers the essential fields required for MeetingResponse.


56-80: Solid stale-request handling pattern.

The request sequence tracking correctly discards out-of-order responses, preventing race conditions when rapid navigation triggers overlapping fetches. The selectedMeeting sync logic (lines 63-66) preserves detail data when the meeting is still in the list.


148-163: Clean upsert implementation.

The refactored upsertMeeting using .map() is more readable than the previous spread+mutation pattern, and correctly handles both insert (prepend) and update (in-place) cases.


// Verify the binding handler forwards events to handleWsEvent
const binding = callArgs.bindings[0]!
const fakeEvent = { channel: 'meetings' as const, event_type: 'meeting.completed' as const, payload: {}, timestamp: new Date().toISOString() }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Line exceeds 88 character limit.

This line is approximately 140 characters, exceeding the configured 88-character limit. Consider breaking it into multiple lines.

♻️ Suggested fix
-    const fakeEvent = { channel: 'meetings' as const, event_type: 'meeting.completed' as const, payload: {}, timestamp: new Date().toISOString() }
+    const fakeEvent = {
+      channel: 'meetings' as const,
+      event_type: 'meeting.completed' as const,
+      payload: {},
+      timestamp: new Date().toISOString(),
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fakeEvent = { channel: 'meetings' as const, event_type: 'meeting.completed' as const, payload: {}, timestamp: new Date().toISOString() }
const fakeEvent = {
channel: 'meetings' as const,
event_type: 'meeting.completed' as const,
payload: {},
timestamp: new Date().toISOString(),
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/hooks/useMeetingsData.test.ts` at line 96, The test's const
fakeEvent declaration in useMeetingsData.test.ts is a single long line exceeding
88 chars; split the object literal across multiple lines (one property per line)
so const fakeEvent = { channel: 'meetings' as const, event_type:
'meeting.completed' as const, payload: {}, timestamp: new Date().toISOString() }
becomes a multiline object with channel, event_type, payload, and timestamp on
separate lines to satisfy the line-length rule while keeping the same types and
values.

title: 'Meetings/MeetingCard',
component: MeetingCard,
tags: ['autodocs'],
decorators: [(Story) => <MemoryRouter><div className="max-w-md"><Story /></div></MemoryRouter>],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Line exceeds 88 character limit.

The decorator line is slightly over the 88-character limit. Consider breaking it for readability.

♻️ Suggested fix
-  decorators: [(Story) => <MemoryRouter><div className="max-w-md"><Story /></div></MemoryRouter>],
+  decorators: [
+    (Story) => (
+      <MemoryRouter>
+        <div className="max-w-md">
+          <Story />
+        </div>
+      </MemoryRouter>
+    ),
+  ],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
decorators: [(Story) => <MemoryRouter><div className="max-w-md"><Story /></div></MemoryRouter>],
decorators: [
(Story) => (
<MemoryRouter>
<div className="max-w-md">
<Story />
</div>
</MemoryRouter>
),
],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/meetings/MeetingCard.stories.tsx` at line 10, The decorators
array entry in MeetingCard.stories.tsx exceeds the 88-char line limit; split the
JSX across multiple lines for readability by formatting the decorators
declaration so the array and the function component are on separate lines
(referencing decorators, Story and MemoryRouter), e.g., place the arrow function
on its own line and break the JSX wrapper (<MemoryRouter>...<Story
/>...</MemoryRouter>) into multiple indented lines to keep each line under 88
characters.

Comment on lines +16 to +17
agent_id: 'agent-alice', content: 'Completed the API endpoint work.', phase: 'round_robin_turn',
turn_number: 1, input_tokens: 200, output_tokens: 150, timestamp: '2026-03-25T10:01:00Z',
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Wrap long story data lines to satisfy max line length.

Multiple lines exceed the repository 88-character limit.

As per coding guidelines: **/*.{py,ts,tsx,go}: "Lines must not exceed 88
characters."

Also applies to: 20-21, 24-25, 35-36, 39-40, 43-44, 47-48, 51-52

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

In `@web/src/pages/meetings/MeetingContributions.stories.tsx` around lines 16 -
17, The story data lines in MeetingContributions.stories.tsx exceed the 88-char
line length; break or wrap long object/property lines (e.g., the object with
keys agent_id, content, phase, turn_number, input_tokens, output_tokens,
timestamp) so each source line is <=88 chars — for example place each property
on its own line, wrap long string values using template literals or
concatenation, or split arrays/objects across multiple lines; apply the same
wrapping to the other long story entries noted (around lines with the same
property names) to satisfy the repo max line length rule.

Comment on lines +20 to +26
export const InProgress: Story = {
args: { meeting: makeMeeting('2', { status: 'in_progress', meeting_type_name: 'sprint_planning' }) },
}

export const Scheduled: Story = {
args: { meeting: makeMeeting('3', { status: 'scheduled', meeting_type_name: 'code_review' }) },
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Lines exceed 88-character limit.

Lines 21 and 25 exceed the configured line length. Consider reformatting for compliance.

♻️ Suggested reformatting
 export const InProgress: Story = {
-  args: { meeting: makeMeeting('2', { status: 'in_progress', meeting_type_name: 'sprint_planning' }) },
+  args: {
+    meeting: makeMeeting('2', {
+      status: 'in_progress',
+      meeting_type_name: 'sprint_planning',
+    }),
+  },
 }
 
 export const Scheduled: Story = {
-  args: { meeting: makeMeeting('3', { status: 'scheduled', meeting_type_name: 'code_review' }) },
+  args: {
+    meeting: makeMeeting('3', {
+      status: 'scheduled',
+      meeting_type_name: 'code_review',
+    }),
+  },
 }

As per coding guidelines, **/*.{ts,tsx}: Lines must not exceed 88 characters.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const InProgress: Story = {
args: { meeting: makeMeeting('2', { status: 'in_progress', meeting_type_name: 'sprint_planning' }) },
}
export const Scheduled: Story = {
args: { meeting: makeMeeting('3', { status: 'scheduled', meeting_type_name: 'code_review' }) },
}
export const InProgress: Story = {
args: {
meeting: makeMeeting('2', {
status: 'in_progress',
meeting_type_name: 'sprint_planning',
}),
},
}
export const Scheduled: Story = {
args: {
meeting: makeMeeting('3', {
status: 'scheduled',
meeting_type_name: 'code_review',
}),
},
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/meetings/MeetingTimelineNode.stories.tsx` around lines 20 - 26,
Lines for the Story exports InProgress and Scheduled exceed the 88-char limit;
break the long args object expressions used with makeMeeting into multiple lines
so each property fits within the limit. Locate the InProgress and Scheduled
constants (export const InProgress: Story and export const Scheduled: Story) and
reformat their args: place the makeMeeting call on its own line and put the
overrides object on multiple lines (e.g., status and meeting_type_name each on
separate lines) while preserving the Story type, trailing commas, and original
argument order.

setTriggerOpen(false)
useToastStore.getState().add({ variant: 'success', title: 'Meeting triggered' })
} catch (err) {
useToastStore.getState().add({ variant: 'error', title: 'Failed to trigger meeting', description: getErrorMessage(err) })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify line length of line 66
sed -n '66p' web/src/pages/MeetingsPage.tsx | wc -c

Repository: Aureliolo/synthorg

Length of output: 65


Break line 66 to comply with 88-character limit.

The line contains 127 characters and needs to be split across multiple lines.

Suggested fix
     } catch (err) {
-      useToastStore.getState().add({ variant: 'error', title: 'Failed to trigger meeting', description: getErrorMessage(err) })
+      useToastStore.getState().add({
+        variant: 'error',
+        title: 'Failed to trigger meeting',
+        description: getErrorMessage(err),
+      })
       throw err
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/MeetingsPage.tsx` at line 66, The long single-line call to
useToastStore.getState().add(...) exceeds the 88-char limit; break the
expression into multiple lines for readability and linting by splitting after
useToastStore.getState().add( and placing the toast object properties (variant,
title, description) each on their own indented lines, referencing the existing
getErrorMessage(err) for description and keeping the closing ) on its own line
so the call in MeetingsPage.tsx remains functionally identical.

Comment on lines +92 to +97
{error && (
<div role="alert" className="flex items-center gap-2 rounded-lg border border-danger/30 bg-danger/5 px-4 py-2 text-sm text-danger">
<AlertTriangle className="size-4 shrink-0" />
{error}
</div>
)}
Copy link
Copy Markdown

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 line lengths in this range
awk 'NR>=92 && NR<=97 {print NR": "length" chars"}' web/src/pages/MeetingsPage.tsx

Repository: Aureliolo/synthorg

Length of output: 139


🏁 Script executed:

sed -n '93p' web/src/pages/MeetingsPage.tsx

Repository: Aureliolo/synthorg

Length of output: 201


Line 93 exceeds the 88 character limit (currently 139 chars).

Break the className string across multiple lines or extract a constant to comply with the configured line length enforcement.

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

In `@web/src/pages/MeetingsPage.tsx` around lines 92 - 97, The JSX alert block for
the error message in MeetingsPage.tsx has an overly long className string;
locate the conditional rendering that checks the error variable and split the
className into multiple lines or extract it into a constant (e.g., const
ERROR_ALERT_CLASS = "flex items-center gap-2 rounded-lg border border-danger/30
bg-danger/5 px-4 py-2 text-sm text-danger") and use that constant in the div,
ensuring the resulting JSX lines are <=88 chars and that lint/formatting still
passes.

Comment on lines +81 to +93
export function computeTokenUsagePercent(meeting: MeetingResponse): number {
if (!meeting.minutes || meeting.token_budget <= 0) return 0
const pct = (meeting.minutes.total_tokens / meeting.token_budget) * 100
return Math.min(100, Math.max(0, pct))
}

export function getParticipantTokenShare(meeting: MeetingResponse, agentId: string): number {
if (!meeting.minutes || meeting.minutes.total_tokens <= 0) return 0
const usage = meeting.token_usage_by_participant[agentId]
if (usage === undefined || usage <= 0) return 0
const pct = (usage / meeting.minutes.total_tokens) * 100
return Math.min(100, Math.max(0, pct))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Token helpers undercount usage when minutes is null.

Line 82, Line 88, and Line 155 hard-depend on meeting.minutes, so meetings with
token_usage_by_participant but no minutes get reported as 0 tokens.

Proposed fix
+function getMeetingTotalTokens(meeting: MeetingResponse): number {
+  if (meeting.minutes && meeting.minutes.total_tokens > 0) {
+    return meeting.minutes.total_tokens
+  }
+  return Object.values(meeting.token_usage_by_participant).reduce(
+    (sum, value) => sum + (value > 0 ? value : 0),
+    0,
+  )
+}
+
 export function computeTokenUsagePercent(meeting: MeetingResponse): number {
-  if (!meeting.minutes || meeting.token_budget <= 0) return 0
-  const pct = (meeting.minutes.total_tokens / meeting.token_budget) * 100
+  if (meeting.token_budget <= 0) return 0
+  const total = getMeetingTotalTokens(meeting)
+  if (total <= 0) return 0
+  const pct = (total / meeting.token_budget) * 100
   return Math.min(100, Math.max(0, pct))
 }
 
 export function getParticipantTokenShare(meeting: MeetingResponse, agentId: string): number {
-  if (!meeting.minutes || meeting.minutes.total_tokens <= 0) return 0
+  const total = getMeetingTotalTokens(meeting)
+  if (total <= 0) return 0
   const usage = meeting.token_usage_by_participant[agentId]
   if (usage === undefined || usage <= 0) return 0
-  const pct = (usage / meeting.minutes.total_tokens) * 100
+  const pct = (usage / total) * 100
   return Math.min(100, Math.max(0, pct))
 }
@@
 export function totalTokensUsed(meetings: readonly MeetingResponse[]): number {
   let total = 0
   for (const m of meetings) {
-    if (m.minutes) total += m.minutes.total_tokens
+    total += getMeetingTotalTokens(m)
   }
   return total
 }

Also applies to: 152-157

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

In `@web/src/utils/meetings.ts` around lines 81 - 93, computeTokenUsagePercent and
getParticipantTokenShare currently return 0 when meeting.minutes is null even if
meeting.token_usage_by_participant contains data; update both functions to fall
back to a derived total token count when meeting.minutes is missing by summing
Object.values(meeting.token_usage_by_participant) (or 0 if that map is empty)
and use that sum in place of meeting.minutes.total_tokens for percent
calculations; ensure you still respect meeting.token_budget and guard against
division by zero, and consider extracting the fallback-sum logic into a small
helper used by computeTokenUsagePercent and getParticipantTokenShare to avoid
duplication.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@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: 7

🤖 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__/stores/meetings.test.ts`:
- Around line 268-281: Add an assertion that the mocked console.debug was called
in the test "ignores event where meeting is a primitive": after calling
useMeetingsStore.getState().handleWsEvent(event) assert debugSpy was invoked
(e.g., expect(debugSpy).toHaveBeenCalled()) before calling
debugSpy.mockRestore(); this references the debugSpy mock and the
useMeetingsStore.getState().handleWsEvent(event) invocation so you can locate
and update the test.
- Around line 172-200: Two test lines exceed the 88-char limit in the
triggerMeeting tests; shorten them by breaking long expressions into smaller
statements or intermediate variables. For the mocked resolve line
(vi.mocked(api.triggerMeeting).mockResolvedValue(triggered)) assign the mock
result to a const or call mockResolvedValue on a separate line, and for the
expect/rejects assertion (await
expect(...triggerMeeting(...)).rejects.toThrow('Trigger failed')) split the call
and the expect into two statements by assigning the promise to a const before
awaiting the rejects check or wrap arguments across multiple lines; update
references to api.triggerMeeting and useMeetingsStore.getState().triggerMeeting
accordingly so the logic is unchanged but each line stays under 88 chars.
- Around line 42-118: Several test lines exceed the 88-char limit; shorten long
lines in the 'fetchMeetings' tests by breaking up long expressions into multiple
statements or variables. Specifically, for usages of
vi.mocked(api.listMeetings).mockResolvedValue(...) and
expect(...).toHaveBeenCalledWith(...) and the mockImplementationOnce Promise
resolver in the tests (look for calls to listMeetings, mockResolvedValue,
mockImplementationOnce, and the two fetchMeetings calls in the "rejects stale
responses..." test), move long object literals to a const (e.g., const response
= { data: ..., total: ..., offset: ..., limit: ... }) or split chained
calls/arguments across lines so each line stays <=88 chars, and replace any
single-line long expect/assert or mockResolvedValue with multi-line formatting
or temporary variables to reduce line length.

In `@web/src/__tests__/utils/meetings.test.ts`:
- Around line 201-204: The failing test line in the "combines multiple filters
with AND" case exceeds the 88-character limit; shorten it by breaking the long
expect expression into multiple lines or extracting the mapped IDs into a short
variable before asserting. In meetings.test.ts locate the test using
filterMeetings(meetings, { status: 'completed', meetingType: 'sprint_planning' }
as MeetingPageFilters) and change the long expect(result.map((m) =>
m.meeting_id)).toEqual(['3']) into a multi-line assertion or an intermediate
const ids = result.map(m => m.meeting_id); expect(ids).toEqual(['3']) so no line
is longer than 88 characters.

In `@web/src/pages/meetings/MeetingMetricCards.tsx`:
- Line 19: The JSX line with StaggerGroup exceeds the 88-char limit; split the
long className expression into multiple lines or extract it to a named variable
to shorten the JSX. For example, move the class string into a const (e.g.,
gridClasses) or format the StaggerGroup props across lines so the call to
StaggerGroup (component StaggerGroup, using cn and className) is under 88
characters while preserving the same classes and className merging behavior.

In `@web/src/pages/meetings/MeetingTimelineNode.tsx`:
- Line 16: The participantCount calculation accesses
meeting.minutes.participant_ids.length directly and can throw if participant_ids
is null/undefined; update the expression used to compute participantCount (the
variable initialized as participantCount) to use defensive optional chaining on
participant_ids (e.g., access participant_ids?.length) so it safely falls back
to 0 when participant_ids is missing.
- Around line 23-28: The JSX class string passed to cn inside the
MeetingTimelineNode component (the className={cn(...)} call) has a single string
on line 24 that exceeds the 88-character limit; fix it by splitting the long
class string into multiple shorter strings or items in the cn call (e.g., break
the `'group flex shrink-0 flex-col items-center gap-1.5 rounded-lg border
border-border bg-card px-4 py-3'` into two or more smaller strings or separate
tokens), keeping the same classes and preserving the existing cn(..., className)
structure so formatting/behavior doesn't change.
🪄 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: 09431348-7cf3-4934-a1d3-9d71b791f968

📥 Commits

Reviewing files that changed from the base of the PR and between cb26f1c and 45c06aa.

📒 Files selected for processing (4)
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/__tests__/utils/meetings.test.ts
  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Dashboard Test
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Prefer interface for defining object shapes in TypeScript
Use camelCase for variable names and function identifiers

Files:

  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/__tests__/utils/meetings.test.ts
web/src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.tsx: ALWAYS reuse existing shared components from web/src/components/ui/ before creating new ones (e.g., StatusBadge, MetricCard, AgentCard, DeptHealthBar, SectionCard)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success) or CSS variables (var(--so-accent)); NEVER hardcode hex values or rgba() in .tsx files
Use font-sans or font-mono for typography (maps to Geist tokens); NEVER set fontFamily directly in CSS
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing
Do NOT recreate complex (>8 line) JSX inside .map() blocks; extract to a shared component in web/src/components/ui/

Files:

  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
**/*.{py,ts,tsx,go}

📄 CodeRabbit inference engine (CLAUDE.md)

Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript)

Files:

  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/__tests__/utils/meetings.test.ts
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Import cn from @/lib/utils for conditional class merging in components

Files:

  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
  • web/src/__tests__/stores/meetings.test.ts
  • web/src/__tests__/utils/meetings.test.ts
web/src/pages/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Lazy-load page components and place page-scoped sub-components in pages/<page-name>/ subdirectories (e.g., pages/tasks/, pages/org-edit/)

Files:

  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
🧠 Learnings (8)
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : ALWAYS reuse existing shared components from `web/src/components/ui/` before creating new ones (e.g., `StatusBadge`, `MetricCard`, `AgentCard`, `DeptHealthBar`, `SectionCard`)

Applied to files:

  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Use density-aware spacing tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/meetings/MeetingMetricCards.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.{ts,tsx} : Import `cn` from `@/lib/utils` for conditional class merging in components

Applied to files:

  • web/src/pages/meetings/MeetingMetricCards.tsx
  • web/src/pages/meetings/MeetingTimelineNode.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`) or CSS variables (`var(--so-accent)`); NEVER hardcode hex values or rgba() in .tsx files

Applied to files:

  • web/src/pages/meetings/MeetingMetricCards.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Do NOT recreate complex (>8 line) JSX inside `.map()` blocks; extract to a shared component in `web/src/components/ui/`

Applied to files:

  • web/src/pages/meetings/MeetingMetricCards.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design token variables exclusively for shadows and borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); never hardcode shadow or border values

Applied to files:

  • web/src/pages/meetings/MeetingMetricCards.tsx
📚 Learning: 2026-03-20T08:28:32.845Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T08:28:32.845Z
Learning: Applies to web/src/__tests__/**/*.{ts,js} : Dashboard testing: Vitest unit tests organized by feature under `web/src/__tests__/`. Use fast-check for property-based testing (`fc.assert` + `fc.property`).

Applied to files:

  • web/src/__tests__/stores/meetings.test.ts
  • web/src/__tests__/utils/meetings.test.ts
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : New shared components must have a corresponding `.stories.tsx` Storybook file with all component states (default, hover, loading, error, empty)

Applied to files:

  • web/src/__tests__/stores/meetings.test.ts
🔇 Additional comments (6)
web/src/pages/meetings/MeetingTimelineNode.tsx (3)

1-6: Imports look good.

The consolidated import from @/utils/format (line 3) addresses the previous review feedback. All imports are properly organized and follow project conventions.


8-11: Interface definition follows guidelines.

Uses interface for the props shape and camelCase naming as required.


20-56: Well-structured accessible component.

Good use of:

  • aria-label with descriptive meeting context (line 29)
  • aria-hidden="true" on the decorative status dot (line 38)
  • Semantic <time> element with dateTime attribute (lines 48-53)
  • cn utility for conditional class merging
  • Tailwind semantic color classes (text-foreground, text-muted-foreground, bg-card)
  • font-mono for the timestamp display

The conditional animate-pulse for active meetings and proper null handling for startedAt are well implemented.

web/src/__tests__/stores/meetings.test.ts (3)

1-38: LGTM!

The test setup follows Vitest best practices. The dynamic import pattern ensures the mocked API module is loaded correctly, and resetStore properly resets both request sequences and store state between tests.


122-168: LGTM!

The fetchMeeting tests properly validate detail fetching with loading states, error handling, and stale response rejection using detailRequestSeq. The test pattern correctly mirrors the fetchMeetings suite.


286-332: LGTM!

The upsertMeeting tests comprehensively validate all upsert scenarios: inserting new meetings at the beginning, updating existing meetings in place, and conditionally syncing selectedMeeting based on ID matching. The test coverage aligns well with the expected Zustand store behavior.

Comment on lines +42 to +118
describe('fetchMeetings', () => {
it('sets loading and stores results', async () => {
const api = await importApi()
const items = [makeMeeting('1'), makeMeeting('2')]
vi.mocked(api.listMeetings).mockResolvedValue({ data: items, total: 2, offset: 0, limit: 100 })

await useMeetingsStore.getState().fetchMeetings()

const state = useMeetingsStore.getState()
expect(state.loading).toBe(false)
expect(state.meetings).toHaveLength(2)
expect(state.total).toBe(2)
expect(state.error).toBeNull()
})

it('sets error on failure', async () => {
const api = await importApi()
vi.mocked(api.listMeetings).mockRejectedValue(new Error('Network error'))

await useMeetingsStore.getState().fetchMeetings()

const state = useMeetingsStore.getState()
expect(state.loading).toBe(false)
expect(state.error).toBe('Network error')
})

it('passes filters to API', async () => {
const api = await importApi()
vi.mocked(api.listMeetings).mockResolvedValue({ data: [], total: 0, offset: 0, limit: 100 })

await useMeetingsStore.getState().fetchMeetings({ status: 'completed', limit: 50 })

expect(api.listMeetings).toHaveBeenCalledWith({ status: 'completed', limit: 50 })
})

it('syncs selectedMeeting with fresh data', async () => {
const api = await importApi()
const old = makeMeeting('1', { status: 'in_progress' })
useMeetingsStore.setState({ selectedMeeting: old })

const fresh = makeMeeting('1', { status: 'completed' })
vi.mocked(api.listMeetings).mockResolvedValue({ data: [fresh], total: 1, offset: 0, limit: 100 })

await useMeetingsStore.getState().fetchMeetings()

expect(useMeetingsStore.getState().selectedMeeting?.status).toBe('completed')
})

it('rejects stale responses when a newer fetch starts', async () => {
const api = await importApi()
const staleItems = [makeMeeting('stale')]
const freshItems = [makeMeeting('fresh')]

// First call resolves slowly, second call resolves immediately
let resolveFirst!: (v: { data: typeof staleItems; total: number; offset: number; limit: number }) => void
vi.mocked(api.listMeetings)
.mockImplementationOnce(() => new Promise((r) => { resolveFirst = r }))
.mockResolvedValueOnce({ data: freshItems, total: 1, offset: 0, limit: 100 })

// Fire first request (slow)
const firstPromise = useMeetingsStore.getState().fetchMeetings()
// Fire second request (fast) -- this increments listRequestSeq
const secondPromise = useMeetingsStore.getState().fetchMeetings()

// Second resolves first
await secondPromise
expect(useMeetingsStore.getState().meetings[0]!.meeting_id).toBe('fresh')

// Now resolve the first (stale) request
resolveFirst({ data: staleItems, total: 1, offset: 0, limit: 100 })
await firstPromise

// Store should still have fresh data, not stale
expect(useMeetingsStore.getState().meetings[0]!.meeting_id).toBe('fresh')
expect(useMeetingsStore.getState().meetings).toHaveLength(1)
})
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix line length violations.

Multiple lines exceed the 88-character limit enforced by ESLint configuration:

  • Line 46: 102 characters
  • Line 70: 101 characters
  • Line 83: 107 characters
  • Line 96: 113 characters
  • Line 99: 89 characters

As per coding guidelines, lines must not exceed 88 characters for TypeScript files.

📏 Proposed fixes to shorten lines
-    vi.mocked(api.listMeetings).mockResolvedValue({ data: items, total: 2, offset: 0, limit: 100 })
+    vi.mocked(api.listMeetings).mockResolvedValue({
+      data: items,
+      total: 2,
+      offset: 0,
+      limit: 100,
+    })
-    vi.mocked(api.listMeetings).mockResolvedValue({ data: [], total: 0, offset: 0, limit: 100 })
+    vi.mocked(api.listMeetings).mockResolvedValue({
+      data: [],
+      total: 0,
+      offset: 0,
+      limit: 100,
+    })
-    vi.mocked(api.listMeetings).mockResolvedValue({ data: [fresh], total: 1, offset: 0, limit: 100 })
+    vi.mocked(api.listMeetings).mockResolvedValue({
+      data: [fresh],
+      total: 1,
+      offset: 0,
+      limit: 100,
+    })
-    let resolveFirst!: (v: { data: typeof staleItems; total: number; offset: number; limit: number }) => void
+    let resolveFirst!: (v: {
+      data: typeof staleItems
+      total: number
+      offset: number
+      limit: number
+    }) => void
-      .mockResolvedValueOnce({ data: freshItems, total: 1, offset: 0, limit: 100 })
+      .mockResolvedValueOnce({
+        data: freshItems,
+        total: 1,
+        offset: 0,
+        limit: 100,
+      })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('fetchMeetings', () => {
it('sets loading and stores results', async () => {
const api = await importApi()
const items = [makeMeeting('1'), makeMeeting('2')]
vi.mocked(api.listMeetings).mockResolvedValue({ data: items, total: 2, offset: 0, limit: 100 })
await useMeetingsStore.getState().fetchMeetings()
const state = useMeetingsStore.getState()
expect(state.loading).toBe(false)
expect(state.meetings).toHaveLength(2)
expect(state.total).toBe(2)
expect(state.error).toBeNull()
})
it('sets error on failure', async () => {
const api = await importApi()
vi.mocked(api.listMeetings).mockRejectedValue(new Error('Network error'))
await useMeetingsStore.getState().fetchMeetings()
const state = useMeetingsStore.getState()
expect(state.loading).toBe(false)
expect(state.error).toBe('Network error')
})
it('passes filters to API', async () => {
const api = await importApi()
vi.mocked(api.listMeetings).mockResolvedValue({ data: [], total: 0, offset: 0, limit: 100 })
await useMeetingsStore.getState().fetchMeetings({ status: 'completed', limit: 50 })
expect(api.listMeetings).toHaveBeenCalledWith({ status: 'completed', limit: 50 })
})
it('syncs selectedMeeting with fresh data', async () => {
const api = await importApi()
const old = makeMeeting('1', { status: 'in_progress' })
useMeetingsStore.setState({ selectedMeeting: old })
const fresh = makeMeeting('1', { status: 'completed' })
vi.mocked(api.listMeetings).mockResolvedValue({ data: [fresh], total: 1, offset: 0, limit: 100 })
await useMeetingsStore.getState().fetchMeetings()
expect(useMeetingsStore.getState().selectedMeeting?.status).toBe('completed')
})
it('rejects stale responses when a newer fetch starts', async () => {
const api = await importApi()
const staleItems = [makeMeeting('stale')]
const freshItems = [makeMeeting('fresh')]
// First call resolves slowly, second call resolves immediately
let resolveFirst!: (v: { data: typeof staleItems; total: number; offset: number; limit: number }) => void
vi.mocked(api.listMeetings)
.mockImplementationOnce(() => new Promise((r) => { resolveFirst = r }))
.mockResolvedValueOnce({ data: freshItems, total: 1, offset: 0, limit: 100 })
// Fire first request (slow)
const firstPromise = useMeetingsStore.getState().fetchMeetings()
// Fire second request (fast) -- this increments listRequestSeq
const secondPromise = useMeetingsStore.getState().fetchMeetings()
// Second resolves first
await secondPromise
expect(useMeetingsStore.getState().meetings[0]!.meeting_id).toBe('fresh')
// Now resolve the first (stale) request
resolveFirst({ data: staleItems, total: 1, offset: 0, limit: 100 })
await firstPromise
// Store should still have fresh data, not stale
expect(useMeetingsStore.getState().meetings[0]!.meeting_id).toBe('fresh')
expect(useMeetingsStore.getState().meetings).toHaveLength(1)
})
})
describe('fetchMeetings', () => {
it('sets loading and stores results', async () => {
const api = await importApi()
const items = [makeMeeting('1'), makeMeeting('2')]
vi.mocked(api.listMeetings).mockResolvedValue({
data: items,
total: 2,
offset: 0,
limit: 100,
})
await useMeetingsStore.getState().fetchMeetings()
const state = useMeetingsStore.getState()
expect(state.loading).toBe(false)
expect(state.meetings).toHaveLength(2)
expect(state.total).toBe(2)
expect(state.error).toBeNull()
})
it('sets error on failure', async () => {
const api = await importApi()
vi.mocked(api.listMeetings).mockRejectedValue(new Error('Network error'))
await useMeetingsStore.getState().fetchMeetings()
const state = useMeetingsStore.getState()
expect(state.loading).toBe(false)
expect(state.error).toBe('Network error')
})
it('passes filters to API', async () => {
const api = await importApi()
vi.mocked(api.listMeetings).mockResolvedValue({
data: [],
total: 0,
offset: 0,
limit: 100,
})
await useMeetingsStore.getState().fetchMeetings({ status: 'completed', limit: 50 })
expect(api.listMeetings).toHaveBeenCalledWith({ status: 'completed', limit: 50 })
})
it('syncs selectedMeeting with fresh data', async () => {
const api = await importApi()
const old = makeMeeting('1', { status: 'in_progress' })
useMeetingsStore.setState({ selectedMeeting: old })
const fresh = makeMeeting('1', { status: 'completed' })
vi.mocked(api.listMeetings).mockResolvedValue({
data: [fresh],
total: 1,
offset: 0,
limit: 100,
})
await useMeetingsStore.getState().fetchMeetings()
expect(useMeetingsStore.getState().selectedMeeting?.status).toBe('completed')
})
it('rejects stale responses when a newer fetch starts', async () => {
const api = await importApi()
const staleItems = [makeMeeting('stale')]
const freshItems = [makeMeeting('fresh')]
// First call resolves slowly, second call resolves immediately
let resolveFirst!: (v: {
data: typeof staleItems
total: number
offset: number
limit: number
}) => void
vi.mocked(api.listMeetings)
.mockImplementationOnce(() => new Promise((r) => { resolveFirst = r }))
.mockResolvedValueOnce({
data: freshItems,
total: 1,
offset: 0,
limit: 100,
})
// Fire first request (slow)
const firstPromise = useMeetingsStore.getState().fetchMeetings()
// Fire second request (fast) -- this increments listRequestSeq
const secondPromise = useMeetingsStore.getState().fetchMeetings()
// Second resolves first
await secondPromise
expect(useMeetingsStore.getState().meetings[0]!.meeting_id).toBe('fresh')
// Now resolve the first (stale) request
resolveFirst({ data: staleItems, total: 1, offset: 0, limit: 100 })
await firstPromise
// Store should still have fresh data, not stale
expect(useMeetingsStore.getState().meetings[0]!.meeting_id).toBe('fresh')
expect(useMeetingsStore.getState().meetings).toHaveLength(1)
})
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/stores/meetings.test.ts` around lines 42 - 118, Several
test lines exceed the 88-char limit; shorten long lines in the 'fetchMeetings'
tests by breaking up long expressions into multiple statements or variables.
Specifically, for usages of vi.mocked(api.listMeetings).mockResolvedValue(...)
and expect(...).toHaveBeenCalledWith(...) and the mockImplementationOnce Promise
resolver in the tests (look for calls to listMeetings, mockResolvedValue,
mockImplementationOnce, and the two fetchMeetings calls in the "rejects stale
responses..." test), move long object literals to a const (e.g., const response
= { data: ..., total: ..., offset: ..., limit: ... }) or split chained
calls/arguments across lines so each line stays <=88 chars, and replace any
single-line long expect/assert or mockResolvedValue with multi-line formatting
or temporary variables to reduce line length.

Comment on lines +172 to +200
describe('triggerMeeting', () => {
it('calls API and prepends results', async () => {
const api = await importApi()
const existing = makeMeeting('old')
useMeetingsStore.setState({ meetings: [existing], total: 1 })

const triggered = [makeMeeting('new')]
vi.mocked(api.triggerMeeting).mockResolvedValue(triggered)

const result = await useMeetingsStore.getState().triggerMeeting({ event_name: 'test_event' })

expect(result).toHaveLength(1)
const state = useMeetingsStore.getState()
expect(state.meetings).toHaveLength(2)
expect(state.meetings[0]!.meeting_id).toBe('new')
expect(state.total).toBe(2)
expect(state.triggering).toBe(false)
})

it('re-throws on failure and resets triggering', async () => {
const api = await importApi()
vi.mocked(api.triggerMeeting).mockRejectedValue(new Error('Trigger failed'))

await expect(useMeetingsStore.getState().triggerMeeting({ event_name: 'bad_event' })).rejects.toThrow('Trigger failed')

const state = useMeetingsStore.getState()
expect(state.triggering).toBe(false)
})
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix line length violations.

Lines 181 and 195 exceed the 88-character limit:

  • Line 181: 100 characters
  • Line 195: 135 characters
📏 Proposed fixes to shorten lines
-    const result = await useMeetingsStore.getState().triggerMeeting({ event_name: 'test_event' })
+    const result = await useMeetingsStore
+      .getState()
+      .triggerMeeting({ event_name: 'test_event' })
-    await expect(useMeetingsStore.getState().triggerMeeting({ event_name: 'bad_event' })).rejects.toThrow('Trigger failed')
+    await expect(
+      useMeetingsStore.getState().triggerMeeting({
+        event_name: 'bad_event',
+      })
+    ).rejects.toThrow('Trigger failed')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('triggerMeeting', () => {
it('calls API and prepends results', async () => {
const api = await importApi()
const existing = makeMeeting('old')
useMeetingsStore.setState({ meetings: [existing], total: 1 })
const triggered = [makeMeeting('new')]
vi.mocked(api.triggerMeeting).mockResolvedValue(triggered)
const result = await useMeetingsStore.getState().triggerMeeting({ event_name: 'test_event' })
expect(result).toHaveLength(1)
const state = useMeetingsStore.getState()
expect(state.meetings).toHaveLength(2)
expect(state.meetings[0]!.meeting_id).toBe('new')
expect(state.total).toBe(2)
expect(state.triggering).toBe(false)
})
it('re-throws on failure and resets triggering', async () => {
const api = await importApi()
vi.mocked(api.triggerMeeting).mockRejectedValue(new Error('Trigger failed'))
await expect(useMeetingsStore.getState().triggerMeeting({ event_name: 'bad_event' })).rejects.toThrow('Trigger failed')
const state = useMeetingsStore.getState()
expect(state.triggering).toBe(false)
})
})
describe('triggerMeeting', () => {
it('calls API and prepends results', async () => {
const api = await importApi()
const existing = makeMeeting('old')
useMeetingsStore.setState({ meetings: [existing], total: 1 })
const triggered = [makeMeeting('new')]
vi.mocked(api.triggerMeeting).mockResolvedValue(triggered)
const result = await useMeetingsStore
.getState()
.triggerMeeting({ event_name: 'test_event' })
expect(result).toHaveLength(1)
const state = useMeetingsStore.getState()
expect(state.meetings).toHaveLength(2)
expect(state.meetings[0]!.meeting_id).toBe('new')
expect(state.total).toBe(2)
expect(state.triggering).toBe(false)
})
it('re-throws on failure and resets triggering', async () => {
const api = await importApi()
vi.mocked(api.triggerMeeting).mockRejectedValue(new Error('Trigger failed'))
await expect(
useMeetingsStore.getState().triggerMeeting({
event_name: 'bad_event',
})
).rejects.toThrow('Trigger failed')
const state = useMeetingsStore.getState()
expect(state.triggering).toBe(false)
})
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/stores/meetings.test.ts` around lines 172 - 200, Two test
lines exceed the 88-char limit in the triggerMeeting tests; shorten them by
breaking long expressions into smaller statements or intermediate variables. For
the mocked resolve line
(vi.mocked(api.triggerMeeting).mockResolvedValue(triggered)) assign the mock
result to a const or call mockResolvedValue on a separate line, and for the
expect/rejects assertion (await
expect(...triggerMeeting(...)).rejects.toThrow('Trigger failed')) split the call
and the expect into two statements by assigning the promise to a const before
awaiting the rejects check or wrap arguments across multiple lines; update
references to api.triggerMeeting and useMeetingsStore.getState().triggerMeeting
accordingly so the logic is unchanged but each line stays under 88 chars.

Comment on lines +201 to +204
it('combines multiple filters with AND', () => {
const result = filterMeetings(meetings, { status: 'completed', meetingType: 'sprint_planning' } as MeetingPageFilters)
expect(result.map((m) => m.meeting_id)).toEqual(['3'])
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Line exceeds 88-character limit.

Line 202 is approximately 123 characters long, significantly exceeding the enforced 88-character limit.

📏 Proposed fix
  it('combines multiple filters with AND', () => {
-    const result = filterMeetings(meetings, { status: 'completed', meetingType: 'sprint_planning' } as MeetingPageFilters)
+    const result = filterMeetings(meetings, {
+      status: 'completed',
+      meetingType: 'sprint_planning',
+    } as MeetingPageFilters)
    expect(result.map((m) => m.meeting_id)).toEqual(['3'])
  })

As per coding guidelines: **/*.{py,ts,tsx,go}: Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('combines multiple filters with AND', () => {
const result = filterMeetings(meetings, { status: 'completed', meetingType: 'sprint_planning' } as MeetingPageFilters)
expect(result.map((m) => m.meeting_id)).toEqual(['3'])
})
it('combines multiple filters with AND', () => {
const result = filterMeetings(meetings, {
status: 'completed',
meetingType: 'sprint_planning',
} as MeetingPageFilters)
expect(result.map((m) => m.meeting_id)).toEqual(['3'])
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/utils/meetings.test.ts` around lines 201 - 204, The failing
test line in the "combines multiple filters with AND" case exceeds the
88-character limit; shorten it by breaking the long expect expression into
multiple lines or extracting the mapped IDs into a short variable before
asserting. In meetings.test.ts locate the test using filterMeetings(meetings, {
status: 'completed', meetingType: 'sprint_planning' } as MeetingPageFilters) and
change the long expect(result.map((m) => m.meeting_id)).toEqual(['3']) into a
multi-line assertion or an intermediate const ids = result.map(m =>
m.meeting_id); expect(ids).toEqual(['3']) so no line is longer than 88
characters.

const tokens = totalTokensUsed(meetings)

return (
<StaggerGroup className={cn('grid grid-cols-2 gap-grid-gap lg:grid-cols-4', className)}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Line exceeds 88-character limit.

This line is 97 characters long, violating the project's enforced line-length limit.

📏 Proposed fix
-    <StaggerGroup className={cn('grid grid-cols-2 gap-grid-gap lg:grid-cols-4', className)}>
+    <StaggerGroup
+      className={cn('grid grid-cols-2 gap-grid-gap lg:grid-cols-4', className)}
+    >

As per coding guidelines: **/*.{py,ts,tsx,go}: Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<StaggerGroup className={cn('grid grid-cols-2 gap-grid-gap lg:grid-cols-4', className)}>
<StaggerGroup
className={cn('grid grid-cols-2 gap-grid-gap lg:grid-cols-4', className)}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/meetings/MeetingMetricCards.tsx` at line 19, The JSX line with
StaggerGroup exceeds the 88-char limit; split the long className expression into
multiple lines or extract it to a named variable to shorten the JSX. For
example, move the class string into a const (e.g., gridClasses) or format the
StaggerGroup props across lines so the call to StaggerGroup (component
StaggerGroup, using cn and className) is under 88 characters while preserving
the same classes and className merging behavior.

export function MeetingTimelineNode({ meeting, className }: MeetingTimelineNodeProps) {
const color = getMeetingStatusColor(meeting.status)
const dotClass = STATUS_DOT_CLASSES[color]
const participantCount = meeting.minutes?.participant_ids.length ?? 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add defensive optional chaining on participant_ids.

If meeting.minutes exists but participant_ids is unexpectedly undefined or null, accessing .length directly will throw. Add ?. before .length for defensive safety.

🛡️ Proposed fix
-  const participantCount = meeting.minutes?.participant_ids.length ?? 0
+  const participantCount = meeting.minutes?.participant_ids?.length ?? 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const participantCount = meeting.minutes?.participant_ids.length ?? 0
const participantCount = meeting.minutes?.participant_ids?.length ?? 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/meetings/MeetingTimelineNode.tsx` at line 16, The
participantCount calculation accesses meeting.minutes.participant_ids.length
directly and can throw if participant_ids is null/undefined; update the
expression used to compute participantCount (the variable initialized as
participantCount) to use defensive optional chaining on participant_ids (e.g.,
access participant_ids?.length) so it safely falls back to 0 when
participant_ids is missing.

Comment on lines +23 to +28
className={cn(
'group flex shrink-0 flex-col items-center gap-1.5 rounded-lg border border-border bg-card px-4 py-3',
'transition-colors duration-200 hover:bg-card-hover hover:border-bright',
'w-36 snap-start',
className,
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Line 24 exceeds 88-character limit.

The class string on line 24 is approximately 103 characters, exceeding the 88-character limit per coding guidelines. Split across multiple lines.

♻️ Proposed fix
       className={cn(
-        'group flex shrink-0 flex-col items-center gap-1.5 rounded-lg border border-border bg-card px-4 py-3',
+        'group flex shrink-0 flex-col items-center gap-1.5',
+        'rounded-lg border border-border bg-card px-4 py-3',
         'transition-colors duration-200 hover:bg-card-hover hover:border-bright',
         'w-36 snap-start',
         className,
       )}

As per coding guidelines: "Lines must not exceed 88 characters."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
className={cn(
'group flex shrink-0 flex-col items-center gap-1.5 rounded-lg border border-border bg-card px-4 py-3',
'transition-colors duration-200 hover:bg-card-hover hover:border-bright',
'w-36 snap-start',
className,
)}
className={cn(
'group flex shrink-0 flex-col items-center gap-1.5',
'rounded-lg border border-border bg-card px-4 py-3',
'transition-colors duration-200 hover:bg-card-hover hover:border-bright',
'w-36 snap-start',
className,
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/meetings/MeetingTimelineNode.tsx` around lines 23 - 28, The JSX
class string passed to cn inside the MeetingTimelineNode component (the
className={cn(...)} call) has a single string on line 24 that exceeds the
88-character limit; fix it by splitting the long class string into multiple
shorter strings or items in the cn call (e.g., break the `'group flex shrink-0
flex-col items-center gap-1.5 rounded-lg border border-border bg-card px-4
py-3'` into two or more smaller strings or separate tokens), keeping the same
classes and preserving the existing cn(..., className) structure so
formatting/behavior doesn't change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@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.

♻️ Duplicate comments (2)
web/src/__tests__/stores/meetings.test.ts (2)

172-200: ⚠️ Potential issue | 🟠 Major

Fix line length violations in triggerMeeting tests.

Lines 181 and 195 exceed the 88-character limit:

  • Line 181: 100 characters
  • Line 195: 135 characters
📏 Proposed fixes to shorten lines
-    const result = await useMeetingsStore.getState().triggerMeeting({ event_name: 'test_event' })
+    const result = await useMeetingsStore
+      .getState()
+      .triggerMeeting({ event_name: 'test_event' })
-    await expect(useMeetingsStore.getState().triggerMeeting({ event_name: 'bad_event' })).rejects.toThrow('Trigger failed')
+    await expect(
+      useMeetingsStore
+        .getState()
+        .triggerMeeting({ event_name: 'bad_event' }),
+    ).rejects.toThrow('Trigger failed')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/stores/meetings.test.ts` around lines 172 - 200, Shorten
the two long test lines by splitting or extracting values: for the first case
shorten the call to triggerMeeting (use a local const for the payload like const
payload = { event_name: 'test_event' } and pass payload to
useMeetingsStore.getState().triggerMeeting) and for the rejection case store the
error in a const (e.g. const err = new Error('Trigger failed')) and use
vi.mocked(api.triggerMeeting).mockRejectedValue(err) and await
expect(...).rejects.toThrow(err.message) so no single line exceeds the 88-char
limit; update references to useMeetingsStore.getState().triggerMeeting and
vi.mocked(api.triggerMeeting) accordingly.

42-118: ⚠️ Potential issue | 🟠 Major

Fix line length violations in fetchMeetings tests.

Multiple lines exceed the 88-character limit enforced by ESLint configuration:

  • Line 46: 102 characters
  • Line 70: 101 characters
  • Line 83: 107 characters
  • Line 96: 113 characters
  • Line 99: 89 characters

As per coding guidelines, lines must not exceed 88 characters for TypeScript files.

📏 Proposed fixes to shorten lines
-    vi.mocked(api.listMeetings).mockResolvedValue({ data: items, total: 2, offset: 0, limit: 100 })
+    vi.mocked(api.listMeetings).mockResolvedValue({
+      data: items,
+      total: 2,
+      offset: 0,
+      limit: 100,
+    })
-    vi.mocked(api.listMeetings).mockResolvedValue({ data: [], total: 0, offset: 0, limit: 100 })
+    vi.mocked(api.listMeetings).mockResolvedValue({
+      data: [],
+      total: 0,
+      offset: 0,
+      limit: 100,
+    })
-    vi.mocked(api.listMeetings).mockResolvedValue({ data: [fresh], total: 1, offset: 0, limit: 100 })
+    vi.mocked(api.listMeetings).mockResolvedValue({
+      data: [fresh],
+      total: 1,
+      offset: 0,
+      limit: 100,
+    })
-    let resolveFirst!: (v: { data: typeof staleItems; total: number; offset: number; limit: number }) => void
+    let resolveFirst!: (v: {
+      data: typeof staleItems
+      total: number
+      offset: number
+      limit: number
+    }) => void
-      .mockResolvedValueOnce({ data: freshItems, total: 1, offset: 0, limit: 100 })
+      .mockResolvedValueOnce({
+        data: freshItems,
+        total: 1,
+        offset: 0,
+        limit: 100,
+      })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/stores/meetings.test.ts` around lines 42 - 118, Tests in
the fetchMeetings suite have lines exceeding the 88-char limit; shorten them by
breaking long literals and calls into multiple lines and extracting parts to
local variables. Specifically, in tests referencing
useMeetingsStore.getState().fetchMeetings(),
vi.mocked(api.listMeetings).mockResolvedValue(...), mockImplementationOnce(() =>
new Promise(...)), and the long expect(...) lines, split objects (e.g., { data:
[...], total: 1, offset: 0, limit: 100 }) across multiple lines, extract long
arrays like staleItems/freshItems or the mock response objects to named
constants, and wrap long expect/assert calls onto multiple lines so no single
line exceeds 88 chars; update the occurrences around makeMeeting, importApi,
resolveFirst, and api.listMeetings to keep intent unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@web/src/__tests__/stores/meetings.test.ts`:
- Around line 172-200: Shorten the two long test lines by splitting or
extracting values: for the first case shorten the call to triggerMeeting (use a
local const for the payload like const payload = { event_name: 'test_event' }
and pass payload to useMeetingsStore.getState().triggerMeeting) and for the
rejection case store the error in a const (e.g. const err = new Error('Trigger
failed')) and use vi.mocked(api.triggerMeeting).mockRejectedValue(err) and await
expect(...).rejects.toThrow(err.message) so no single line exceeds the 88-char
limit; update references to useMeetingsStore.getState().triggerMeeting and
vi.mocked(api.triggerMeeting) accordingly.
- Around line 42-118: Tests in the fetchMeetings suite have lines exceeding the
88-char limit; shorten them by breaking long literals and calls into multiple
lines and extracting parts to local variables. Specifically, in tests
referencing useMeetingsStore.getState().fetchMeetings(),
vi.mocked(api.listMeetings).mockResolvedValue(...), mockImplementationOnce(() =>
new Promise(...)), and the long expect(...) lines, split objects (e.g., { data:
[...], total: 1, offset: 0, limit: 100 }) across multiple lines, extract long
arrays like staleItems/freshItems or the mock response objects to named
constants, and wrap long expect/assert calls onto multiple lines so no single
line exceeds 88 chars; update the occurrences around makeMeeting, importApi,
resolveFirst, and api.listMeetings to keep intent unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a4689d92-e8b5-4496-adb8-c9a1609f421d

📥 Commits

Reviewing files that changed from the base of the PR and between 45c06aa and dd61f1e.

📒 Files selected for processing (1)
  • web/src/__tests__/stores/meetings.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Dashboard Test
  • GitHub Check: Build Backend
  • GitHub Check: Build Web
  • GitHub Check: Build Sandbox
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Prefer interface for defining object shapes in TypeScript
Use camelCase for variable names and function identifiers

Files:

  • web/src/__tests__/stores/meetings.test.ts
**/*.{py,ts,tsx,go}

📄 CodeRabbit inference engine (CLAUDE.md)

Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript)

Files:

  • web/src/__tests__/stores/meetings.test.ts
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Import cn from @/lib/utils for conditional class merging in components

Files:

  • web/src/__tests__/stores/meetings.test.ts
🧠 Learnings (3)
📚 Learning: 2026-03-20T08:28:32.845Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T08:28:32.845Z
Learning: Applies to web/src/__tests__/**/*.{ts,js} : Dashboard testing: Vitest unit tests organized by feature under `web/src/__tests__/`. Use fast-check for property-based testing (`fc.assert` + `fc.property`).

Applied to files:

  • web/src/__tests__/stores/meetings.test.ts
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : New shared components must have a corresponding `.stories.tsx` Storybook file with all component states (default, hover, loading, error, empty)

Applied to files:

  • web/src/__tests__/stores/meetings.test.ts
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to **/*.{py,ts,tsx,go} : Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript)

Applied to files:

  • web/src/__tests__/stores/meetings.test.ts

@Aureliolo Aureliolo merged commit b207f46 into main Mar 28, 2026
29 checks passed
@Aureliolo Aureliolo deleted the feat/meetings-page branch March 28, 2026 16:53
Aureliolo added a commit that referenced this pull request Mar 30, 2026
🤖 I have created a release *beep* *boop*
---
#MAJOR CHANGES; We got a somewhat working webui :)

##
[0.5.0](v0.4.9...v0.5.0)
(2026-03-30)


### Features

* add analytics trends and budget forecast API endpoints
([#798](#798))
([16b61f5](16b61f5))
* add department policies to default templates
([#852](#852))
([7a41548](7a41548))
* add remaining activity event types (task_started, tool_used,
delegation, cost_incurred)
([#832](#832))
([4252fac](4252fac))
* agent performance, activity, and history API endpoints
([#811](#811))
([9b75c1d](9b75c1d))
* Agent Profiles and Detail pages (biography, career, performance)
([#874](#874))
([62d7880](62d7880))
* app shell, Storybook, and CI/CD pipeline
([#819](#819))
([d4dde90](d4dde90))
* Approvals page with risk grouping, urgency indicators, batch actions
([#889](#889))
([4e9673d](4e9673d))
* Budget Panel page (P&L dashboard, breakdown charts, forecast)
([#890](#890))
([b63b0f1](b63b0f1))
* build infrastructure layer (API client, auth, WebSocket)
([#815](#815))
([9f01d3e](9f01d3e))
* CLI global options infrastructure, UI modes, exit codes, env vars
([#891](#891))
([fef4fc5](fef4fc5))
* CodeMirror editor and theme preferences toggle
([#905](#905),
[#807](#807))
([#909](#909))
([41fbedc](41fbedc))
* Company page (department/agent management)
([#888](#888))
([cfb88b0](cfb88b0))
* comprehensive hint coverage across all CLI commands
([#900](#900))
([937974e](937974e))
* config system extensions, per-command flags for
init/start/stop/status/logs
([#895](#895))
([32f83fe](32f83fe))
* configurable currency system replacing hardcoded USD
([#854](#854))
([b372551](b372551))
* Dashboard page (metric cards, activity feed, budget burn)
([#861](#861))
([7d519d5](7d519d5))
* department health, provider status, and activity feed endpoints
([#818](#818))
([6d5f196](6d5f196))
* design tokens and core UI components
([#833](#833))
([ed887f2](ed887f2))
* extend approval, meeting, and budget API responses
([#834](#834))
([31472bf](31472bf))
* frontend polish -- real-time UX, accessibility, responsive,
performance ([#790](#790),
[#792](#792),
[#791](#791),
[#793](#793))
([#917](#917))
([f04a537](f04a537))
* implement human roles and access control levels
([#856](#856))
([d6d8a06](d6d8a06))
* implement semantic conflict detection in workspace merge
([#860](#860))
([d97283b](d97283b))
* interaction components and animation patterns
([#853](#853))
([82d4b01](82d4b01))
* Login page + first-run bootstrap + Company page
([#789](#789),
[#888](#888))
([#896](#896))
([8758e8d](8758e8d))
* Meetings page with timeline viz, token bars, contribution formatting
([#788](#788))
([#904](#904))
([b207f46](b207f46))
* Messages page with threading, channel badges, sender indicators
([#787](#787))
([#903](#903))
([28293ad](28293ad))
* Org Chart force-directed view and drag-drop reassignment
([#872](#872),
[#873](#873))
([#912](#912))
([a68a938](a68a938))
* Org Chart page (living nodes, status, CRUD, department health)
([#870](#870))
([0acbdae](0acbdae))
* per-command flags for remaining commands, auto-behavior wiring,
help/discoverability
([#897](#897))
([3f7afa2](3f7afa2))
* Providers page with backend rework -- health, CRUD, subscription auth
([#893](#893))
([9f8dd98](9f8dd98))
* scaffold React + Vite + TypeScript + Tailwind project
([#799](#799))
([bd151aa](bd151aa))
* Settings page with search, dependency indicators, grouped rendering
([#784](#784))
([#902](#902))
([a7b9870](a7b9870))
* Setup Wizard rebuild with template comparison, cost estimator, theme
customization ([#879](#879))
([ae8b50b](ae8b50b))
* setup wizard UX -- template filters, card metadata, provider form
reuse ([#910](#910))
([7f04676](7f04676))
* setup wizard UX overhaul -- mode choice, step reorder, provider fixes
([#907](#907))
([ee964c4](ee964c4))
* structured ModelRequirement in template agent configs
([#795](#795))
([7433548](7433548))
* Task Board page (rich Kanban, filtering, dependency viz)
([#871](#871))
([04a19b0](04a19b0))


### Bug Fixes

* align frontend types with backend and debounce WS refetches
([#916](#916))
([134c11b](134c11b))
* auto-cleanup targets newly pulled images instead of old ones
([#884](#884))
([50e6591](50e6591))
* correct wipe backup-skip flow and harden error handling
([#808](#808))
([c05860f](c05860f))
* improve provider setup in wizard, subscription auth, dashboard bugs
([#914](#914))
([87bf8e6](87bf8e6))
* improve update channel detection and add config get command
([#814](#814))
([6b137f0](6b137f0))
* resolve all ESLint warnings, add zero-warnings enforcement
([#899](#899))
([079b46a](079b46a))
* subscription auth uses api_key, base URL optional for cloud providers
([#915](#915))
([f0098dd](f0098dd))


### Refactoring

* semantic analyzer cleanup -- shared filtering, concurrency, extraction
([#908](#908))
([81372bf](81372bf))


### Documentation

* brand identity and UX design system from
[#765](#765) exploration
([#804](#804))
([389a9f4](389a9f4))
* page structure and information architecture for v0.5.0 dashboard
([#809](#809))
([f8d6d4a](f8d6d4a))
* write UX design guidelines with WCAG-verified color system
([#816](#816))
([4a4594e](4a4594e))


### Tests

* add unit tests for agent hooks and page components
([#875](#875))
([#901](#901))
([1d81546](1d81546))


### CI/CD

* bump actions/deploy-pages from 4.0.5 to 5.0.0 in the major group
([#831](#831))
([01c19de](01c19de))
* bump astral-sh/setup-uv from 7.6.0 to 8.0.0 in
/.github/actions/setup-python-uv in the all group
([#920](#920))
([5f6ba54](5f6ba54))
* bump codecov/codecov-action from 5.5.3 to 6.0.0 in the major group
([#868](#868))
([f22a181](f22a181))
* bump github/codeql-action from 4.34.1 to 4.35.0 in the all group
([#883](#883))
([87a4890](87a4890))
* bump sigstore/cosign-installer from 4.1.0 to 4.1.1 in the
minor-and-patch group
([#830](#830))
([7a69050](7a69050))
* bump the all group with 3 updates
([#923](#923))
([ff27c8e](ff27c8e))
* bump wrangler from 4.76.0 to 4.77.0 in /.github in the minor-and-patch
group ([#822](#822))
([07d43eb](07d43eb))
* bump wrangler from 4.77.0 to 4.78.0 in /.github in the all group
([#882](#882))
([f84118d](f84118d))


### Maintenance

* add design system enforcement hook and component inventory
([#846](#846))
([15abc43](15abc43))
* add dev-only auth bypass for frontend testing
([#885](#885))
([6cdcd8a](6cdcd8a))
* add pre-push rebase check hook
([#855](#855))
([b637a04](b637a04))
* backend hardening -- eviction/size-caps and model validation
([#911](#911))
([81253d9](81253d9))
* bump axios from 1.13.6 to 1.14.0 in /web in the all group across 1
directory ([#922](#922))
([b1b0232](b1b0232))
* bump brace-expansion from 5.0.4 to 5.0.5 in /web
([#862](#862))
([ba4a565](ba4a565))
* bump eslint-plugin-react-refresh from 0.4.26 to 0.5.2 in /web
([#801](#801))
([7574bb5](7574bb5))
* bump faker from 40.11.0 to 40.11.1 in the minor-and-patch group
([#803](#803))
([14d322e](14d322e))
* bump https://github.com/astral-sh/ruff-pre-commit from v0.15.7 to
0.15.8 ([#864](#864))
([f52901e](f52901e))
* bump nginxinc/nginx-unprivileged from `6582a34` to `f99cc61` in
/docker/web in the all group
([#919](#919))
([df85e4f](df85e4f))
* bump nginxinc/nginx-unprivileged from `ccbac1a` to `6582a34` in
/docker/web ([#800](#800))
([f4e9450](f4e9450))
* bump node from `44bcbf4` to `71be405` in /docker/sandbox
([#827](#827))
([91bec67](91bec67))
* bump node from `5209bca` to `cf38e1f` in /docker/web
([#863](#863))
([66d6043](66d6043))
* bump picomatch in /site
([#842](#842))
([5f20bcc](5f20bcc))
* bump recharts 2-&gt;3 and @types/node 22-&gt;25 in /web
([#802](#802))
([a908800](a908800))
* Bump requests from 2.32.5 to 2.33.0
([#843](#843))
([41daf69](41daf69))
* bump smol-toml from 1.6.0 to 1.6.1 in /site
([#826](#826))
([3e5dbe4](3e5dbe4))
* bump the all group with 3 updates
([#921](#921))
([7bace0b](7bace0b))
* bump the minor-and-patch group across 1 directory with 2 updates
([#829](#829))
([93e611f](93e611f))
* bump the minor-and-patch group across 1 directory with 3 updates
([#841](#841))
([7010c8e](7010c8e))
* bump the minor-and-patch group across 1 directory with 3 updates
([#869](#869))
([548cee5](548cee5))
* bump the minor-and-patch group in /site with 2 updates
([#865](#865))
([9558101](9558101))
* bump the minor-and-patch group with 2 updates
([#867](#867))
([4830706](4830706))
* consolidate Dependabot groups to 1 PR per ecosystem
([06d2556](06d2556))
* consolidate Dependabot groups to 1 PR per ecosystem
([#881](#881))
([06d2556](06d2556))
* improve worktree skill with full dep sync and status enhancements
([#906](#906))
([772c625](772c625))
* remove Vue remnants and document framework decision
([#851](#851))
([bf2adf6](bf2adf6))
* update web dependencies and fix brace-expansion CVE
([#880](#880))
([a7a0ed6](a7a0ed6))
* upgrade to Storybook 10 and TypeScript 6
([#845](#845))
([52d95f2](52d95f2))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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.

feat: Meetings page (timeline viz, token bars, contribution formatting)

1 participant