From 9813e4520216e6f4edc16e9bf67f9a3d100a6a43 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:26:54 +0000 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9A=A1=20Bolt:=20hoist=20buildTimelineGr?= =?UTF-8?q?oups=20useMemo=20up=20to=20page.tsx=20to=20avoid=20duplicate=20?= =?UTF-8?q?calculation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `buildTimelineGroups` was calculated redundantly inside both `EventList` and `SessionActivityRibbon` inside `useMemo` hooks. This commit moves the `useMemo` computation into the parent `page.tsx` component and passes down `groups` as a prop to its children, preventing duplicate O(n) array iterations per `events` change. --- .../dashboard/[orgSlug]/sessions/[sessionId]/page.tsx | 5 ++++- packages/web/src/components/dashboard/event-list.tsx | 11 ++++++----- .../components/dashboard/session-activity-ribbon.tsx | 8 ++++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/web/src/app/dashboard/[orgSlug]/sessions/[sessionId]/page.tsx b/packages/web/src/app/dashboard/[orgSlug]/sessions/[sessionId]/page.tsx index 1bea6e3e..4477ad5b 100644 --- a/packages/web/src/app/dashboard/[orgSlug]/sessions/[sessionId]/page.tsx +++ b/packages/web/src/app/dashboard/[orgSlug]/sessions/[sessionId]/page.tsx @@ -13,7 +13,7 @@ import { SessionFilesTab, } from '@/components/dashboard/session-files' import { useSessionDetail } from '@/hooks/use-dashboard-sessions' -import { messagesToTimeline, formatSlashCommandText } from '@/lib/timeline-events' +import { messagesToTimeline, buildTimelineGroups, formatSlashCommandText } from '@/lib/timeline-events' import { extractSessionFiles } from '@/lib/session-files' import { formatTokens, @@ -46,6 +46,7 @@ export default function OrgSessionDetailPage({ () => (data ? messagesToTimeline(data.messages) : []), [data], ) + const groups = useMemo(() => buildTimelineGroups(events), [events]) const files = useMemo(() => extractSessionFiles(events), [events]) const [selectedIdx, setSelectedIdx] = useState(0) const safeIdx = @@ -221,6 +222,7 @@ export default function OrgSessionDetailPage({
void; sessionStartedAt: string; @@ -50,11 +51,10 @@ function formatElapsed(timestamp: string, sessionStartedAt: string): string { } function buildFlatRows( - events: TimelineEvent[], + groups: TimelineGroup[], expandedGroups: Set, selectedIdx: number, ): FlatRow[] { - const groups = buildTimelineGroups(events); const rows: FlatRow[] = []; for (const group of groups) { if (group.kind === "single") { @@ -261,6 +261,7 @@ function Row({ export function EventList({ events, + groups, selectedIdx, onSelect, sessionStartedAt, @@ -268,8 +269,8 @@ export function EventList({ onToggleGroup, }: EventListProps) { const rows = useMemo( - () => buildFlatRows(events, expandedGroups, selectedIdx), - [events, expandedGroups, selectedIdx], + () => buildFlatRows(groups, expandedGroups, selectedIdx), + [groups, expandedGroups, selectedIdx], ); if (events.length === 0) { diff --git a/packages/web/src/components/dashboard/session-activity-ribbon.tsx b/packages/web/src/components/dashboard/session-activity-ribbon.tsx index 87a305ea..694e3806 100644 --- a/packages/web/src/components/dashboard/session-activity-ribbon.tsx +++ b/packages/web/src/components/dashboard/session-activity-ribbon.tsx @@ -1,16 +1,17 @@ 'use client' -import { useMemo, useState, useRef } from 'react' +import { useState, useRef } from 'react' import { formatSlashCommandText, - buildTimelineGroups, type TimelineEvent, + type TimelineGroup, } from '@/lib/timeline-events' import { formatTokens, formatCost, formatRelativeTime } from '@/lib/format' import { segmentVisuals } from './session-ribbon-visuals' type Props = { events: TimelineEvent[] + groups: TimelineGroup[] selectedIdx: number | null onSelect: (idx: number) => void sessionStartedAt: string @@ -147,6 +148,7 @@ function MergedTooltipBody({ export function SessionActivityRibbon({ events, + groups, selectedIdx, onSelect, sessionStartedAt, @@ -156,8 +158,6 @@ export function SessionActivityRibbon({ const [hover, setHover] = useState(null) const containerRef = useRef(null) - const groups = useMemo(() => buildTimelineGroups(events), [events]) - if (events.length === 0) return null const trackMouse = (e: React.MouseEvent) => { From 35479064d72abf110351706016c7bc9c9533a940 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 22 Jun 2026 20:11:44 +0000 Subject: [PATCH 2/6] refactor(web): optimize multiple reduce loops in weekly reports Replace chained `.reduce()` and `Object.values()` with a single `for...of` loop over `Object.keys()` to avoid unnecessary intermediate array allocations and improve iteration performance during report aggregation. --- packages/web/src/lib/server/weekly-report.ts | 23 ++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/web/src/lib/server/weekly-report.ts b/packages/web/src/lib/server/weekly-report.ts index 7eabddbd..eb95d36f 100644 --- a/packages/web/src/lib/server/weekly-report.ts +++ b/packages/web/src/lib/server/weekly-report.ts @@ -392,17 +392,22 @@ export async function getWeeklyReport( } // Insights — delegation - const totalAgentCalls = thisWeekRollups.reduce( - (sum, r) => sum + Object.values(r.agentCounts).reduce((a, b) => a + b, 0), - 0, - ) - const totalSkillCalls = thisWeekRollups.reduce( - (sum, r) => sum + Object.values(r.skillCounts).reduce((a, b) => a + b, 0), - 0, - ) + // ⚡ Bolt Optimization: + // 병목 지점: 기존 코드는 `thisWeekRollups`를 3번 순회하고, 매 순회마다 Object.values()로 중간 배열을 생성하여 메모리 할당 비용이 발생했습니다. + // 최적화 방법: 단일 for...of 루프와 Object.keys() 순회를 결합하여 N+1 순회를 1회 순회로 통합하고 중간 배열 할당을 제거했습니다. + // 기대 효과: `thisWeekRollups`의 크기가 클 경우, 불필요한 배열 생성 오버헤드와 O(N) 순회를 1/3로 줄여 리포트 생성 성능이 향상됩니다. + let totalAgentCalls = 0 + let totalSkillCalls = 0 const distinctSkillsThisWeek = new Set() + for (const r of thisWeekRollups) { - for (const k of Object.keys(r.skillCounts)) distinctSkillsThisWeek.add(k) + for (const k of Object.keys(r.agentCounts)) { + totalAgentCalls += r.agentCounts[k] + } + for (const k of Object.keys(r.skillCounts)) { + totalSkillCalls += r.skillCounts[k] + distinctSkillsThisWeek.add(k) + } } const insights: WeeklyInsights = { From 649df11c1c816fdb73fb61c8b201270dfbe86b82 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 22 Jun 2026 21:49:09 +0000 Subject: [PATCH 3/6] feat(ux): add focus-visible styles to overview stats toggle button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit overview-stats 컴포넌트의 설명 텍스트를 펼치거나 접는 버튼에 키보드 포커스 스타일이 누락되어 있어, 키보드 내비게이션 사용자에게 현재 포커스 위치를 명확히 보여주지 못하는 문제를 수정했습니다. Tailwind CSS의 `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm` 클래스를 추가하여 탭(Tab) 키 이동 시 포커스 링이 보이도록 접근성을 개선했습니다. --- .Jules/palette.md | 3 +++ packages/web/src/components/dashboard/overview-stats.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .Jules/palette.md diff --git a/.Jules/palette.md b/.Jules/palette.md new file mode 100644 index 00000000..b5ac773f --- /dev/null +++ b/.Jules/palette.md @@ -0,0 +1,3 @@ +## 2024-06-22 - Overview Stats 토글 버튼 키보드 접근성 개선 +**Learning:** `overview-stats` 컴포넌트의 설명 텍스트를 펼치거나 접는 ` diff --git a/packages/web/src/components/layout/org-header.tsx b/packages/web/src/components/layout/org-header.tsx index c33de1fc..7f57f267 100644 --- a/packages/web/src/components/layout/org-header.tsx +++ b/packages/web/src/components/layout/org-header.tsx @@ -18,6 +18,7 @@ export function OrgHeader({ orgName }: { orgName?: string }) { diff --git a/packages/web/src/components/layout/org-sidebar.tsx b/packages/web/src/components/layout/org-sidebar.tsx index 7f28d8d0..6d37e315 100644 --- a/packages/web/src/components/layout/org-sidebar.tsx +++ b/packages/web/src/components/layout/org-sidebar.tsx @@ -124,6 +124,7 @@ export function OrgSidebar() { @@ -150,6 +151,7 @@ export function OrgSidebar() { From 41d4e6ad02c8894b31b73b59a0008fe18a29cf6a Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:13:25 +0000 Subject: [PATCH 6/6] feat(ux): add aria attributes to ContextSection for better accessibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `aria-controls`를 추가하여 토글 버튼과 콘텐츠 영역 간의 연관 관계 설정 - `Chevron` 아이콘에 `aria-hidden="true"`를 부여하여 스크린 리더 불필요한 노이즈 최소화 - 100% 컴포넌트 테스트 커버리지 추가 --- packages/web/package.json | 3 + .../reports/context-section.test.tsx | 59 +++++ .../dashboard/reports/context-section.tsx | 10 +- packages/web/vitest.config.ts | 4 +- pnpm-lock.yaml | 238 +++++++++++++++++- 5 files changed, 296 insertions(+), 18 deletions(-) create mode 100644 packages/web/src/components/dashboard/reports/context-section.test.tsx diff --git a/packages/web/package.json b/packages/web/package.json index b5ff94f7..a59dc408 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -41,12 +41,15 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.2.2", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/bcryptjs": "^2", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "dotenv": "^17.4.2", "eslint-config-next": "^16.2.3", + "happy-dom": "^20.10.6", "prisma": "^6", "shadcn": "^4.10.0", "tailwindcss": "^4", diff --git a/packages/web/src/components/dashboard/reports/context-section.test.tsx b/packages/web/src/components/dashboard/reports/context-section.test.tsx new file mode 100644 index 00000000..8cd01f73 --- /dev/null +++ b/packages/web/src/components/dashboard/reports/context-section.test.tsx @@ -0,0 +1,59 @@ +import * as React from 'react' +import { render, screen, fireEvent, cleanup } from '@testing-library/react' +import { describe, it, expect, vi, afterEach } from 'vitest' +import { ContextSection } from './context-section' + +// Mock useId to return a stable ID +vi.mock('react', async () => { + const actual = await vi.importActual('react') + return { + ...actual, + useId: () => 'mock-id-123' + } +}) + +describe('ContextSection', () => { + afterEach(() => { + cleanup() + }) + + it('renders title and toggle button correctly', () => { + render(Test Content) + + expect(screen.getByText('Test Title')).toBeTruthy() + const button = screen.getByRole('button') + expect(button.getAttribute('aria-expanded')).toBe('false') + expect(button.getAttribute('aria-controls')).toBe('mock-id-123') + }) + + it('toggles content visibility on click', () => { + render(

Test Content

) + + expect(screen.queryByText('Test Content')).toBeNull() + + const button = screen.getByRole('button') + fireEvent.click(button) + + expect(button.getAttribute('aria-expanded')).toBe('true') + expect(screen.getByText('Test Content')).toBeTruthy() + + const contentContainer = screen.getByTestId('content').parentElement + expect(contentContainer?.getAttribute('id')).toBe('mock-id-123') + + fireEvent.click(button) + expect(button.getAttribute('aria-expanded')).toBe('false') + expect(screen.queryByText('Test Content')).toBeNull() + }) + + it('respects defaultOpen prop', () => { + render( + + Test Content + + ) + + const button = screen.getByRole('button') + expect(button.getAttribute('aria-expanded')).toBe('true') + expect(screen.getByText('Test Content')).toBeTruthy() + }) +}) diff --git a/packages/web/src/components/dashboard/reports/context-section.tsx b/packages/web/src/components/dashboard/reports/context-section.tsx index 2cdc21de..2f09d94e 100644 --- a/packages/web/src/components/dashboard/reports/context-section.tsx +++ b/packages/web/src/components/dashboard/reports/context-section.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, type ReactNode } from 'react' +import React, { useState, useId, type ReactNode } from 'react' import { ChevronDown, ChevronUp } from 'lucide-react' import { cn } from '@/lib/utils' @@ -12,6 +12,7 @@ interface ContextSectionProps { export function ContextSection({ title, children, defaultOpen = false }: ContextSectionProps) { const [open, setOpen] = useState(defaultOpen) + const contentId = useId() return (
@@ -23,16 +24,17 @@ export function ContextSection({ title, children, defaultOpen = false }: Context 'hover:bg-card-elevated transition-colors', )} aria-expanded={open} + aria-controls={contentId} >

{title}

{open ? ( - +