1 line
224 KiB
JSON
1 line
224 KiB
JSON
[{"filePath":"/home/trav/repos/noteflow/client/coverage/block-navigation.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/coverage/prettify.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/coverage/sorter.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/eslint.config.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/playwright.config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/postcss.config.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/App.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/cached-adapter.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/cached-adapter.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/connection-state.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/connection-state.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/constants.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/helpers.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/helpers.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/index.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":17,"column":47,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":17,"endColumn":74}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\n\nconst setConnectionMode = vi.fn();\nconst setConnectionServerUrl = vi.fn();\nconst setAPIInstance = vi.fn();\nconst startReconnection = vi.fn();\nconst startTauriEventBridge = vi.fn().mockResolvedValue(undefined);\nconst preferences = { initialize: vi.fn().mockResolvedValue(undefined) };\nconst getConnectionState = vi.fn(() => ({ mode: 'cached' }));\n\nconst mockAPI = { kind: 'mock' };\nconst cachedAPI = { kind: 'cached' };\n\nlet initializeTauriAPI = vi.fn();\n\nvi.mock('./tauri-adapter', () => ({\n initializeTauriAPI: (...args: unknown[]) => initializeTauriAPI(...args),\n createTauriAPI: vi.fn(),\n isTauriEnvironment: vi.fn(),\n}));\n\nvi.mock('./mock-adapter', () => ({ mockAPI }));\nvi.mock('./cached-adapter', () => ({ cachedAPI }));\nvi.mock('./reconnection', () => ({ startReconnection }));\nvi.mock('./connection-state', () => ({\n setConnectionMode,\n setConnectionServerUrl,\n getConnectionState,\n}));\nvi.mock('./interface', () => ({ setAPIInstance }));\nvi.mock('@/lib/preferences', () => ({ preferences }));\nvi.mock('@/lib/tauri-events', () => ({ startTauriEventBridge }));\n\nasync function loadIndexModule(withWindow: boolean) {\n vi.resetModules();\n if (withWindow) {\n const mockWindow: unknown = {};\n vi.stubGlobal('window', mockWindow as Window);\n } else {\n vi.stubGlobal('window', undefined as unknown as Window);\n }\n return await import('./index');\n}\n\ndescribe('api/index initializeAPI', () => {\n beforeEach(() => {\n initializeTauriAPI = vi.fn();\n setConnectionMode.mockClear();\n setConnectionServerUrl.mockClear();\n setAPIInstance.mockClear();\n startReconnection.mockClear();\n startTauriEventBridge.mockClear();\n preferences.initialize.mockClear();\n });\n\n afterEach(() => {\n vi.unstubAllGlobals();\n });\n\n it('returns mock API when tauri is unavailable', async () => {\n initializeTauriAPI.mockRejectedValueOnce(new Error('no tauri'));\n const { initializeAPI } = await loadIndexModule(false);\n\n const api = await initializeAPI();\n\n expect(api).toBe(mockAPI);\n expect(setConnectionMode).toHaveBeenCalledWith('mock');\n expect(setAPIInstance).toHaveBeenCalledWith(mockAPI);\n });\n\n it('connects via tauri when available', async () => {\n const tauriAPI = { connect: vi.fn().mockResolvedValue({ version: '1.0.0' }) };\n initializeTauriAPI.mockResolvedValueOnce(tauriAPI);\n\n const { initializeAPI } = await loadIndexModule(false);\n const api = await initializeAPI();\n\n expect(api).toBe(tauriAPI);\n expect(tauriAPI.connect).toHaveBeenCalled();\n expect(setConnectionMode).toHaveBeenCalledWith('connected');\n expect(preferences.initialize).toHaveBeenCalled();\n expect(startTauriEventBridge).toHaveBeenCalled();\n expect(startReconnection).toHaveBeenCalled();\n });\n\n it('falls back to cached mode when connect fails', async () => {\n const tauriAPI = { connect: vi.fn().mockRejectedValue(new Error('fail')) };\n initializeTauriAPI.mockResolvedValueOnce(tauriAPI);\n\n const { initializeAPI } = await loadIndexModule(false);\n const api = await initializeAPI();\n\n expect(api).toBe(tauriAPI);\n expect(setConnectionMode).toHaveBeenCalledWith('cached', 'fail');\n expect(preferences.initialize).toHaveBeenCalled();\n expect(startReconnection).toHaveBeenCalled();\n });\n\n it('uses a default message when connect fails with non-Error values', async () => {\n const tauriAPI = { connect: vi.fn().mockRejectedValue('boom') };\n initializeTauriAPI.mockResolvedValueOnce(tauriAPI);\n\n const { initializeAPI } = await loadIndexModule(false);\n const api = await initializeAPI();\n\n expect(api).toBe(tauriAPI);\n expect(setConnectionMode).toHaveBeenCalledWith('cached', 'Connection failed');\n });\n\n it('auto-initializes when window is present', async () => {\n initializeTauriAPI.mockRejectedValueOnce(new Error('no tauri'));\n\n const module = await loadIndexModule(true);\n\n await Promise.resolve();\n await Promise.resolve();\n\n expect(setConnectionMode).toHaveBeenCalledWith('cached');\n expect(setAPIInstance).toHaveBeenCalledWith(cachedAPI);\n expect(setConnectionMode).toHaveBeenCalledWith('mock');\n\n const windowApi = (globalThis.window as Window & Record<string, unknown>).__NOTEFLOW_API__;\n expect(windowApi).toBe(mockAPI);\n const connection = (globalThis.window as Window & Record<string, unknown>).__NOTEFLOW_CONNECTION__;\n expect(connection).toBeDefined();\n expect(module).toBeDefined();\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/interface.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-adapter.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":45,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":45,"endColumn":64}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport type { FinalSegment } from './types';\n\nasync function loadMockAPI() {\n vi.resetModules();\n const module = await import('./mock-adapter');\n return module.mockAPI;\n}\n\nasync function flushTimers() {\n await vi.runAllTimersAsync();\n}\n\ndescribe('mockAPI', () => {\n beforeEach(() => {\n vi.useFakeTimers();\n vi.setSystemTime(new Date('2024-01-01T00:00:00Z'));\n localStorage.clear();\n });\n\n afterEach(() => {\n vi.runOnlyPendingTimers();\n vi.useRealTimers();\n vi.clearAllMocks();\n });\n\n it('creates, lists, starts, stops, and deletes meetings', async () => {\n const mockAPI = await loadMockAPI();\n\n const createPromise = mockAPI.createMeeting({ title: 'Team Sync', metadata: { team: 'A' } });\n await flushTimers();\n const meeting = await createPromise;\n expect(meeting.title).toBe('Team Sync');\n\n const listPromise = mockAPI.listMeetings({\n states: ['created'],\n sort_order: 'newest',\n limit: 5,\n offset: 0,\n });\n await flushTimers();\n const list = await listPromise;\n expect(list.meetings.some((m) => m.id === meeting.id)).toBe(true);\n\n const stream = await mockAPI.startTranscription(meeting.id);\n expect(stream).toBeDefined();\n\n const getPromise = mockAPI.getMeeting({\n meeting_id: meeting.id,\n include_segments: false,\n include_summary: false,\n });\n await flushTimers();\n const fetched = await getPromise;\n expect(fetched.state).toBe('recording');\n\n const stopPromise = mockAPI.stopMeeting(meeting.id);\n await flushTimers();\n const stopped = await stopPromise;\n expect(stopped.state).toBe('stopped');\n\n const deletePromise = mockAPI.deleteMeeting(meeting.id);\n await flushTimers();\n const deleted = await deletePromise;\n expect(deleted).toBe(true);\n\n const missingPromise = mockAPI.getMeeting({\n meeting_id: meeting.id,\n include_segments: false,\n include_summary: false,\n });\n const missingExpectation = expect(missingPromise).rejects.toThrow('Meeting not found');\n await flushTimers();\n await missingExpectation;\n });\n\n it('manages annotations, summaries, and exports', async () => {\n const mockAPI = await loadMockAPI();\n\n const createPromise = mockAPI.createMeeting({ title: 'Annotations' });\n await flushTimers();\n const meeting = await createPromise;\n\n const addPromise = mockAPI.addAnnotation({\n meeting_id: meeting.id,\n annotation_type: 'note',\n text: 'Important',\n start_time: 1,\n end_time: 2,\n segment_ids: [1],\n });\n await flushTimers();\n const annotation = await addPromise;\n\n const listPromise = mockAPI.listAnnotations(meeting.id, 0.5, 2.5);\n await flushTimers();\n const list = await listPromise;\n expect(list).toHaveLength(1);\n\n const getPromise = mockAPI.getAnnotation(annotation.id);\n await flushTimers();\n const fetched = await getPromise;\n expect(fetched.text).toBe('Important');\n\n const updatePromise = mockAPI.updateAnnotation({\n annotation_id: annotation.id,\n text: 'Updated',\n annotation_type: 'decision',\n });\n await flushTimers();\n const updated = await updatePromise;\n expect(updated.text).toBe('Updated');\n expect(updated.annotation_type).toBe('decision');\n\n const deletePromise = mockAPI.deleteAnnotation(annotation.id);\n await flushTimers();\n const deleted = await deletePromise;\n expect(deleted).toBe(true);\n\n const missingPromise = mockAPI.getAnnotation('missing');\n const missingExpectation = expect(missingPromise).rejects.toThrow('Annotation not found');\n await flushTimers();\n await missingExpectation;\n\n const summaryPromise = mockAPI.generateSummary(meeting.id);\n await flushTimers();\n const summary = await summaryPromise;\n expect(summary.meeting_id).toBe(meeting.id);\n\n const exportMdPromise = mockAPI.exportTranscript(meeting.id, 'markdown');\n await flushTimers();\n const exportMd = await exportMdPromise;\n expect(exportMd.content).toContain('Summary');\n expect(exportMd.file_extension).toBe('.md');\n\n const exportHtmlPromise = mockAPI.exportTranscript(meeting.id, 'html');\n await flushTimers();\n const exportHtml = await exportHtmlPromise;\n expect(exportHtml.file_extension).toBe('.html');\n expect(exportHtml.content).toContain('<html>');\n });\n\n it('handles playback, consent, diarization, and speaker renames', async () => {\n const mockAPI = await loadMockAPI();\n\n const createPromise = mockAPI.createMeeting({ title: 'Playback' });\n await flushTimers();\n const meeting = await createPromise;\n\n const meetingPromise = mockAPI.getMeeting({\n meeting_id: meeting.id,\n include_segments: false,\n include_summary: false,\n });\n await flushTimers();\n const stored = await meetingPromise;\n\n const segment: FinalSegment = {\n segment_id: 1,\n text: 'Hello world',\n start_time: 0,\n end_time: 1,\n words: [],\n language: 'en',\n language_confidence: 0.99,\n avg_logprob: -0.2,\n no_speech_prob: 0.01,\n speaker_id: 'SPEAKER_00',\n speaker_confidence: 0.9,\n };\n stored.segments.push(segment);\n\n const renamePromise = mockAPI.renameSpeaker(meeting.id, 'SPEAKER_00', 'Alex');\n await flushTimers();\n const renamed = await renamePromise;\n expect(renamed).toBe(true);\n\n await mockAPI.startPlayback(meeting.id, 5);\n await mockAPI.pausePlayback();\n const seeked = await mockAPI.seekPlayback(10);\n expect(seeked.position).toBe(10);\n const playback = await mockAPI.getPlaybackState();\n expect(playback.is_paused).toBe(true);\n await mockAPI.stopPlayback();\n const stopped = await mockAPI.getPlaybackState();\n expect(stopped.meeting_id).toBeUndefined();\n\n const grantPromise = mockAPI.grantCloudConsent();\n await flushTimers();\n await grantPromise;\n const statusPromise = mockAPI.getCloudConsentStatus();\n await flushTimers();\n const status = await statusPromise;\n expect(status.consentGranted).toBe(true);\n\n const revokePromise = mockAPI.revokeCloudConsent();\n await flushTimers();\n await revokePromise;\n const statusAfterPromise = mockAPI.getCloudConsentStatus();\n await flushTimers();\n const statusAfter = await statusAfterPromise;\n expect(statusAfter.consentGranted).toBe(false);\n\n const diarizationPromise = mockAPI.refineSpeakers(meeting.id, 2);\n await flushTimers();\n const diarization = await diarizationPromise;\n expect(diarization.status).toBe('queued');\n\n const jobPromise = mockAPI.getDiarizationJobStatus(diarization.job_id);\n await flushTimers();\n const job = await jobPromise;\n expect(job.status).toBe('completed');\n\n const cancelPromise = mockAPI.cancelDiarization(diarization.job_id);\n await flushTimers();\n const cancel = await cancelPromise;\n expect(cancel.success).toBe(true);\n });\n\n it('returns current user and manages workspace switching', async () => {\n const mockAPI = await loadMockAPI();\n\n const userPromise = mockAPI.getCurrentUser();\n await flushTimers();\n const user = await userPromise;\n expect(user.display_name).toBe('Local User');\n\n const workspacesPromise = mockAPI.listWorkspaces();\n await flushTimers();\n const workspaces = await workspacesPromise;\n expect(workspaces.workspaces.length).toBeGreaterThan(0);\n\n const targetWorkspace = workspaces.workspaces[0];\n const switchPromise = mockAPI.switchWorkspace(targetWorkspace.id);\n await flushTimers();\n const switched = await switchPromise;\n expect(switched.success).toBe(true);\n expect(switched.workspace?.id).toBe(targetWorkspace.id);\n\n const missingPromise = mockAPI.switchWorkspace('missing-workspace');\n await flushTimers();\n const missing = await missingPromise;\n expect(missing.success).toBe(false);\n });\n\n it('handles webhooks, entities, sync, logs, metrics, and calendar flows', async () => {\n const mockAPI = await loadMockAPI();\n\n const registerPromise = mockAPI.registerWebhook({\n workspace_id: 'w1',\n name: 'Webhook',\n url: 'https://example.com',\n events: ['meeting.completed'],\n });\n await flushTimers();\n const webhook = await registerPromise;\n\n const listPromise = mockAPI.listWebhooks();\n await flushTimers();\n const list = await listPromise;\n expect(list.total_count).toBe(1);\n\n const updatePromise = mockAPI.updateWebhook({\n webhook_id: webhook.id,\n enabled: false,\n timeout_ms: 5000,\n });\n await flushTimers();\n const updated = await updatePromise;\n expect(updated.enabled).toBe(false);\n\n const updateRetriesPromise = mockAPI.updateWebhook({\n webhook_id: webhook.id,\n max_retries: 5,\n });\n await flushTimers();\n const updatedRetries = await updateRetriesPromise;\n expect(updatedRetries.max_retries).toBe(5);\n\n const enabledOnlyPromise = mockAPI.listWebhooks(true);\n await flushTimers();\n const enabledOnly = await enabledOnlyPromise;\n expect(enabledOnly.total_count).toBe(0);\n\n const deliveriesPromise = mockAPI.getWebhookDeliveries(webhook.id, 5);\n await flushTimers();\n const deliveries = await deliveriesPromise;\n expect(deliveries.total_count).toBe(0);\n\n const deletePromise = mockAPI.deleteWebhook(webhook.id);\n await flushTimers();\n const deleted = await deletePromise;\n expect(deleted.success).toBe(true);\n\n const updateMissingPromise = mockAPI.updateWebhook({\n webhook_id: 'missing',\n name: 'Missing',\n });\n const updateExpectation = expect(updateMissingPromise).rejects.toThrow('Webhook missing not found');\n await flushTimers();\n await updateExpectation;\n\n const entitiesPromise = mockAPI.extractEntities('meeting');\n await flushTimers();\n const entities = await entitiesPromise;\n expect(entities.cached).toBe(false);\n\n const updateEntityPromise = mockAPI.updateEntity('meeting', 'e1', 'Entity', 'topic');\n await flushTimers();\n const updatedEntity = await updateEntityPromise;\n expect(updatedEntity.text).toBe('Entity');\n\n const updateEntityDefaultPromise = mockAPI.updateEntity('meeting', 'e2');\n await flushTimers();\n const updatedEntityDefault = await updateEntityDefaultPromise;\n expect(updatedEntityDefault.text).toBe('Mock Entity');\n\n const deleteEntityPromise = mockAPI.deleteEntity('meeting', 'e1');\n await flushTimers();\n const deletedEntity = await deleteEntityPromise;\n expect(deletedEntity).toBe(true);\n\n const syncPromise = mockAPI.startIntegrationSync('int-1');\n await flushTimers();\n const sync = await syncPromise;\n expect(sync.status).toBe('running');\n\n const statusPromise = mockAPI.getSyncStatus(sync.sync_run_id);\n await flushTimers();\n const status = await statusPromise;\n expect(status.status).toBe('success');\n\n const historyPromise = mockAPI.listSyncHistory('int-1', 3, 0);\n await flushTimers();\n const history = await historyPromise;\n expect(history.runs.length).toBeGreaterThan(0);\n\n const logsPromise = mockAPI.getRecentLogs({ limit: 5, level: 'error', source: 'api' });\n await flushTimers();\n const logs = await logsPromise;\n expect(logs.logs.length).toBeGreaterThan(0);\n\n const metricsPromise = mockAPI.getPerformanceMetrics({ history_limit: 5 });\n await flushTimers();\n const metrics = await metricsPromise;\n expect(metrics.history).toHaveLength(5);\n\n const triggerEnablePromise = mockAPI.setTriggerEnabled(true);\n await flushTimers();\n await triggerEnablePromise;\n const snoozePromise = mockAPI.snoozeTriggers(5);\n await flushTimers();\n await snoozePromise;\n const resetPromise = mockAPI.resetSnooze();\n await flushTimers();\n await resetPromise;\n const dismissPromise = mockAPI.dismissTrigger();\n await flushTimers();\n await dismissPromise;\n const triggerMeetingPromise = mockAPI.acceptTrigger('Trigger Meeting');\n await flushTimers();\n const triggerMeeting = await triggerMeetingPromise;\n expect(triggerMeeting.title).toContain('Trigger Meeting');\n\n const providersPromise = mockAPI.getCalendarProviders();\n await flushTimers();\n const providers = await providersPromise;\n expect(providers.providers.length).toBe(2);\n\n const authPromise = mockAPI.initiateCalendarAuth('google', 'https://redirect');\n await flushTimers();\n const auth = await authPromise;\n expect(auth.auth_url).toContain('http');\n\n const completePromise = mockAPI.completeCalendarAuth('google', 'code', auth.state);\n await flushTimers();\n const complete = await completePromise;\n expect(complete.success).toBe(true);\n\n const statusAuthPromise = mockAPI.getOAuthConnectionStatus('google');\n await flushTimers();\n const statusAuth = await statusAuthPromise;\n expect(statusAuth.is_connected).toBe(false);\n\n const disconnectPromise = mockAPI.disconnectCalendar('google');\n await flushTimers();\n const disconnect = await disconnectPromise;\n expect(disconnect.success).toBe(true);\n\n const eventsPromise = mockAPI.listCalendarEvents(1, 5, 'google');\n await flushTimers();\n const events = await eventsPromise;\n expect(events.total_count).toBe(0);\n });\n\n it('covers additional mock adapter branches', async () => {\n const mockAPI = await loadMockAPI();\n\n const serverInfoPromise = mockAPI.getServerInfo();\n await flushTimers();\n await serverInfoPromise;\n await mockAPI.isConnected();\n\n const createPromise = mockAPI.createMeeting({ title: 'Branch Coverage' });\n await flushTimers();\n const meeting = await createPromise;\n\n const exportNoSummaryPromise = mockAPI.exportTranscript(meeting.id, 'markdown');\n await flushTimers();\n const exportNoSummary = await exportNoSummaryPromise;\n expect(exportNoSummary.content).not.toContain('Summary');\n\n meeting.segments.push({\n segment_id: 99,\n text: 'Segment text',\n start_time: 0,\n end_time: 1,\n words: [],\n language: 'en',\n language_confidence: 0.9,\n avg_logprob: -0.1,\n no_speech_prob: 0.01,\n speaker_id: 'SPEAKER_00',\n speaker_confidence: 0.8,\n });\n\n const exportHtmlPromise = mockAPI.exportTranscript(meeting.id, 'html');\n await flushTimers();\n await exportHtmlPromise;\n\n const listDefaultPromise = mockAPI.listMeetings({});\n await flushTimers();\n const listDefault = await listDefaultPromise;\n expect(listDefault.meetings.length).toBeGreaterThan(0);\n\n const listOldestPromise = mockAPI.listMeetings({\n sort_order: 'oldest',\n offset: 1,\n limit: 1,\n });\n await flushTimers();\n await listOldestPromise;\n\n const annotationPromise = mockAPI.addAnnotation({\n meeting_id: meeting.id,\n annotation_type: 'note',\n text: 'Branch',\n start_time: 1,\n end_time: 2,\n });\n await flushTimers();\n const annotation = await annotationPromise;\n\n const listNoFilterPromise = mockAPI.listAnnotations(meeting.id);\n await flushTimers();\n const listNoFilter = await listNoFilterPromise;\n expect(listNoFilter.length).toBeGreaterThan(0);\n\n const updatePromise = mockAPI.updateAnnotation({\n annotation_id: annotation.id,\n start_time: 0.5,\n end_time: 3.5,\n segment_ids: [1, 2, 3],\n });\n await flushTimers();\n const updated = await updatePromise;\n expect(updated.segment_ids).toEqual([1, 2, 3]);\n\n const missingDeletePromise = mockAPI.deleteAnnotation('missing');\n await flushTimers();\n const missingDelete = await missingDeletePromise;\n expect(missingDelete).toBe(false);\n\n const renamedMissingPromise = mockAPI.renameSpeaker(meeting.id, 'SPEAKER_99', 'Sam');\n await flushTimers();\n const renamedMissing = await renamedMissingPromise;\n expect(renamedMissing).toBe(false);\n\n await mockAPI.selectAudioDevice('input-1', true);\n await mockAPI.selectAudioDevice('output-1', false);\n await mockAPI.listAudioDevices();\n await mockAPI.getDefaultAudioDevice(true);\n\n await mockAPI.startPlayback(meeting.id);\n const playback = await mockAPI.getPlaybackState();\n expect(playback.position).toBe(0);\n\n await mockAPI.getTriggerStatus();\n\n const deleteMissingWebhookPromise = mockAPI.deleteWebhook('missing');\n await flushTimers();\n const deletedMissing = await deleteMissingWebhookPromise;\n expect(deletedMissing.success).toBe(false);\n\n const webhooksPromise = mockAPI.listWebhooks(false);\n await flushTimers();\n await webhooksPromise;\n\n const deliveriesPromise = mockAPI.getWebhookDeliveries('missing');\n await flushTimers();\n await deliveriesPromise;\n\n const connectPromise = mockAPI.connect('http://localhost');\n await flushTimers();\n await connectPromise;\n const prefsPromise = mockAPI.getPreferences();\n await flushTimers();\n const prefs = await prefsPromise;\n await mockAPI.savePreferences({ ...prefs, simulate_transcription: true });\n await mockAPI.saveExportFile('content', 'Meeting Notes', 'md');\n\n const disconnectPromise = mockAPI.disconnect();\n await flushTimers();\n await disconnectPromise;\n\n const historyDefaultPromise = mockAPI.listSyncHistory('int-1');\n await flushTimers();\n await historyDefaultPromise;\n\n const logsDefaultPromise = mockAPI.getRecentLogs();\n await flushTimers();\n await logsDefaultPromise;\n\n const metricsDefaultPromise = mockAPI.getPerformanceMetrics();\n await flushTimers();\n await metricsDefaultPromise;\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-adapter.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-data.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-data.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-transcription-stream.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/mock-transcription-stream.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/reconnection.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":17,"column":17,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":17,"endColumn":25},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":21,"column":29,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":21,"endColumn":49}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\n\nconst getAPI = vi.fn();\nconst isTauriEnvironment = vi.fn();\nconst getConnectionState = vi.fn();\nconst incrementReconnectAttempts = vi.fn();\nconst resetReconnectAttempts = vi.fn();\nconst setConnectionMode = vi.fn();\nconst setConnectionError = vi.fn();\nconst meetingCache = {\n invalidateAll: vi.fn(),\n updateServerStateVersion: vi.fn(),\n};\nconst preferences = { revalidateIntegrations: vi.fn() };\n\nvi.mock('./interface', () => ({\n getAPI: () => getAPI(),\n}));\n\nvi.mock('./tauri-adapter', () => ({\n isTauriEnvironment: () => isTauriEnvironment(),\n}));\n\nvi.mock('./connection-state', () => ({\n getConnectionState,\n incrementReconnectAttempts,\n resetReconnectAttempts,\n setConnectionMode,\n setConnectionError,\n}));\n\nvi.mock('@/lib/cache/meeting-cache', () => ({\n meetingCache,\n}));\n\nvi.mock('@/lib/preferences', () => ({\n preferences,\n}));\n\nasync function loadReconnection() {\n vi.resetModules();\n return await import('./reconnection');\n}\n\ndescribe('reconnection', () => {\n beforeEach(() => {\n getAPI.mockReset();\n isTauriEnvironment.mockReset();\n getConnectionState.mockReset();\n incrementReconnectAttempts.mockReset();\n resetReconnectAttempts.mockReset();\n setConnectionMode.mockReset();\n setConnectionError.mockReset();\n meetingCache.invalidateAll.mockReset();\n meetingCache.updateServerStateVersion.mockReset();\n preferences.revalidateIntegrations.mockReset();\n });\n\n afterEach(async () => {\n const { stopReconnection } = await loadReconnection();\n stopReconnection();\n vi.unstubAllGlobals();\n });\n\n it('does not attempt reconnect when not in tauri', async () => {\n isTauriEnvironment.mockReturnValue(false);\n getConnectionState.mockReturnValue({ mode: 'cached', reconnectAttempts: 0 });\n\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n\n expect(setConnectionMode).not.toHaveBeenCalled();\n });\n\n it('reconnects successfully and resets attempts', async () => {\n isTauriEnvironment.mockReturnValue(true);\n getConnectionState.mockReturnValue({ mode: 'cached', reconnectAttempts: 1 });\n const getServerInfo = vi.fn().mockResolvedValue({ state_version: 3 });\n getAPI.mockReturnValue({\n connect: vi.fn().mockResolvedValue(undefined),\n getServerInfo,\n });\n preferences.revalidateIntegrations.mockResolvedValue(undefined);\n\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n await Promise.resolve();\n\n expect(resetReconnectAttempts).toHaveBeenCalled();\n expect(setConnectionMode).toHaveBeenCalledWith('connected');\n expect(setConnectionError).toHaveBeenCalledWith(null);\n expect(meetingCache.invalidateAll).toHaveBeenCalled();\n expect(getServerInfo).toHaveBeenCalled();\n expect(meetingCache.updateServerStateVersion).toHaveBeenCalledWith(3);\n expect(preferences.revalidateIntegrations).toHaveBeenCalled();\n });\n\n it('handles reconnect failures and schedules retry', async () => {\n isTauriEnvironment.mockReturnValue(true);\n getConnectionState.mockReturnValue({ mode: 'cached', reconnectAttempts: 0 });\n getAPI.mockReturnValue({ connect: vi.fn().mockRejectedValue(new Error('nope')) });\n\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n\n expect(incrementReconnectAttempts).toHaveBeenCalled();\n expect(setConnectionMode).toHaveBeenCalledWith('cached', 'nope');\n });\n\n it('handles offline network state', async () => {\n isTauriEnvironment.mockReturnValue(true);\n getConnectionState.mockReturnValue({ mode: 'cached', reconnectAttempts: 0 });\n vi.stubGlobal('navigator', { onLine: false });\n\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n\n expect(setConnectionMode).toHaveBeenCalledWith('cached', 'Network offline');\n });\n\n it('does not attempt reconnect when already connected or reconnecting', async () => {\n isTauriEnvironment.mockReturnValue(true);\n getAPI.mockReturnValue({ connect: vi.fn() });\n\n getConnectionState.mockReturnValue({ mode: 'connected', reconnectAttempts: 0 });\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n expect(setConnectionMode).not.toHaveBeenCalledWith('reconnecting');\n\n getConnectionState.mockReturnValue({ mode: 'reconnecting', reconnectAttempts: 0 });\n startReconnection();\n await Promise.resolve();\n expect(setConnectionMode).not.toHaveBeenCalledWith('reconnecting');\n });\n\n it('uses fallback error message on non-Error failures', async () => {\n isTauriEnvironment.mockReturnValue(true);\n getConnectionState.mockReturnValue({ mode: 'cached', reconnectAttempts: 0 });\n getAPI.mockReturnValue({ connect: vi.fn().mockRejectedValue('nope') });\n\n const { startReconnection } = await loadReconnection();\n startReconnection();\n await Promise.resolve();\n\n expect(setConnectionMode).toHaveBeenCalledWith('cached', 'Reconnection failed');\n });\n\n it('syncs state when forceSyncState is called', async () => {\n const serverInfo = { state_version: 5 };\n const getServerInfo = vi.fn().mockResolvedValue(serverInfo);\n getAPI.mockReturnValue({ getServerInfo });\n preferences.revalidateIntegrations.mockResolvedValue(undefined);\n\n const { forceSyncState, onReconnected } = await loadReconnection();\n const callback = vi.fn();\n const unsubscribe = onReconnected(callback);\n\n await forceSyncState();\n\n expect(meetingCache.invalidateAll).toHaveBeenCalled();\n expect(getServerInfo).toHaveBeenCalled();\n expect(meetingCache.updateServerStateVersion).toHaveBeenCalledWith(5);\n expect(preferences.revalidateIntegrations).toHaveBeenCalled();\n expect(callback).toHaveBeenCalled();\n\n unsubscribe();\n });\n\n it('does not invoke unsubscribed reconnection callbacks', async () => {\n getAPI.mockReturnValue({ getServerInfo: vi.fn().mockResolvedValue({ state_version: 1 }) });\n preferences.revalidateIntegrations.mockResolvedValue(undefined);\n\n const { forceSyncState, onReconnected } = await loadReconnection();\n const callback = vi.fn();\n const unsubscribe = onReconnected(callback);\n\n unsubscribe();\n await forceSyncState();\n\n expect(callback).not.toHaveBeenCalled();\n });\n\n it('reports syncing state while integration revalidation is pending', async () => {\n let resolveRevalidate: (() => void) | undefined;\n const revalidatePromise = new Promise<void>((resolve) => {\n resolveRevalidate = resolve;\n });\n preferences.revalidateIntegrations.mockReturnValue(revalidatePromise);\n getAPI.mockReturnValue({ getServerInfo: vi.fn().mockResolvedValue({ state_version: 2 }) });\n\n const { forceSyncState, isSyncingState } = await loadReconnection();\n const syncPromise = forceSyncState();\n\n expect(isSyncingState()).toBe(true);\n\n resolveRevalidate?.();\n await syncPromise;\n\n expect(isSyncingState()).toBe(false);\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/reconnection.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/tauri-adapter.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":163,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":163,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":173,"column":5,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":173,"endColumn":16},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .send on an `error` typed value.","line":173,"column":12,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":173,"endColumn":16},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":190,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":190,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":198,"column":5,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":198,"endColumn":16},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .send on an `error` typed value.","line":198,"column":12,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":198,"endColumn":16},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":225,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":225,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":228,"column":11,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":228,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .onUpdate on an `error` typed value.","line":228,"column":18,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":228,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":273,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":273,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":275,"column":11,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":275,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .onUpdate on an `error` typed value.","line":275,"column":18,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":275,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":350,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":350,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":352,"column":11,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":352,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .onUpdate on an `error` typed value.","line":352,"column":18,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":352,"endColumn":26},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":353,"column":5,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":353,"endColumn":17},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .close on an `error` typed value.","line":353,"column":12,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":353,"endColumn":17},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":364,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":364,"endColumn":54},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":365,"column":5,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":365,"endColumn":17},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .close on an `error` typed value.","line":365,"column":12,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":365,"endColumn":17}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":20,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { beforeEach, describe, expect, it, vi } from 'vitest';\n\nvi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn() }));\nvi.mock('@tauri-apps/api/event', () => ({ listen: vi.fn() }));\n\nimport { invoke } from '@tauri-apps/api/core';\nimport { listen } from '@tauri-apps/api/event';\n\nimport {\n createTauriAPI,\n initializeTauriAPI,\n isTauriEnvironment,\n type TauriInvoke,\n type TauriListen,\n} from './tauri-adapter';\nimport type { AudioChunk, Meeting, Summary, TranscriptUpdate, UserPreferences } from './types';\nimport { meetingCache } from '@/lib/cache/meeting-cache';\n\ntype InvokeMock = (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;\ntype ListenMock = (\n event: string,\n handler: (event: { payload: unknown }) => void\n) => Promise<() => void>;\n\nfunction createMocks() {\n const invoke = vi.fn<Parameters<InvokeMock>, ReturnType<InvokeMock>>();\n const listen = vi\n .fn<Parameters<ListenMock>, ReturnType<ListenMock>>()\n .mockResolvedValue(() => {});\n return { invoke, listen };\n}\n\nfunction buildMeeting(id: string): Meeting {\n return {\n id,\n title: `Meeting ${id}`,\n state: 'created',\n created_at: Date.now() / 1000,\n duration_seconds: 0,\n segments: [],\n metadata: {},\n };\n}\n\nfunction buildSummary(meetingId: string): Summary {\n return {\n meeting_id: meetingId,\n executive_summary: 'Test summary',\n key_points: [],\n action_items: [],\n model_version: 'test-v1',\n generated_at: Date.now() / 1000,\n };\n}\n\nfunction buildPreferences(aiTemplate?: UserPreferences['ai_template']): UserPreferences {\n return {\n simulate_transcription: false,\n default_export_format: 'markdown',\n default_export_location: '',\n completed_tasks: [],\n speaker_names: [],\n tags: [],\n ai_config: { provider: 'anthropic', model_id: 'claude-3-haiku' },\n audio_devices: { input_device_id: '', output_device_id: '' },\n ai_template: aiTemplate ?? {\n tone: 'professional',\n format: 'bullet_points',\n verbosity: 'balanced',\n },\n integrations: [],\n sync_notifications: { enabled: false, on_sync_complete: false, on_sync_error: false },\n sync_scheduler_paused: false,\n sync_history: [],\n };\n}\n\ndescribe('tauri-adapter mapping', () => {\n it('maps listMeetings args to snake_case', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue({ meetings: [], total_count: 0 });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.listMeetings({\n states: ['recording'],\n limit: 5,\n offset: 10,\n sort_order: 'newest',\n });\n\n expect(invoke).toHaveBeenCalledWith('list_meetings', {\n states: [2],\n limit: 5,\n offset: 10,\n sort_order: 1,\n });\n });\n\n it('maps identity commands with expected payloads', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValueOnce({ user_id: 'u1', display_name: 'Local User' });\n invoke.mockResolvedValueOnce({ workspaces: [] });\n invoke.mockResolvedValueOnce({ success: true });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.getCurrentUser();\n await api.listWorkspaces();\n await api.switchWorkspace('w1');\n\n expect(invoke).toHaveBeenCalledWith('get_current_user');\n expect(invoke).toHaveBeenCalledWith('list_workspaces');\n expect(invoke).toHaveBeenCalledWith('switch_workspace', { workspace_id: 'w1' });\n });\n\n it('maps meeting and annotation args to snake_case', async () => {\n const { invoke, listen } = createMocks();\n const meeting = buildMeeting('m1');\n invoke.mockResolvedValueOnce(meeting).mockResolvedValueOnce({ id: 'a1' });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.getMeeting({ meeting_id: 'm1', include_segments: true, include_summary: true });\n await api.addAnnotation({\n meeting_id: 'm1',\n annotation_type: 'decision',\n text: 'Ship it',\n start_time: 1.25,\n end_time: 2.5,\n segment_ids: [1, 2],\n });\n\n expect(invoke).toHaveBeenCalledWith('get_meeting', {\n meeting_id: 'm1',\n include_segments: true,\n include_summary: true,\n });\n expect(invoke).toHaveBeenCalledWith('add_annotation', {\n meeting_id: 'm1',\n annotation_type: 2,\n text: 'Ship it',\n start_time: 1.25,\n end_time: 2.5,\n segment_ids: [1, 2],\n });\n });\n\n it('normalizes delete responses', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValueOnce({ success: true }).mockResolvedValueOnce(true);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await expect(api.deleteMeeting('m1')).resolves.toBe(true);\n await expect(api.deleteAnnotation('a1')).resolves.toBe(true);\n\n expect(invoke).toHaveBeenCalledWith('delete_meeting', { meeting_id: 'm1' });\n expect(invoke).toHaveBeenCalledWith('delete_annotation', { annotation_id: 'a1' });\n });\n\n it('sends audio chunk with snake_case keys', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue(undefined);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m1');\n\n const chunk: AudioChunk = {\n meeting_id: 'm1',\n audio_data: new Float32Array([0.25, -0.25]),\n timestamp: 12.34,\n sample_rate: 48000,\n channels: 2,\n };\n\n stream.send(chunk);\n\n expect(invoke).toHaveBeenCalledWith('start_recording', { meeting_id: 'm1' });\n expect(invoke).toHaveBeenCalledWith('send_audio_chunk', {\n meeting_id: 'm1',\n audio_data: [0.25, -0.25],\n timestamp: 12.34,\n sample_rate: 48000,\n channels: 2,\n });\n });\n\n it('sends audio chunk without optional fields', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue(undefined);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m2');\n\n const chunk: AudioChunk = {\n meeting_id: 'm2',\n audio_data: new Float32Array([0.1]),\n timestamp: 1.23,\n };\n\n stream.send(chunk);\n\n const call = invoke.mock.calls.find((item) => item[0] === 'send_audio_chunk');\n expect(call).toBeDefined();\n const args = call?.[1] as Record<string, unknown>;\n expect(args).toMatchObject({\n meeting_id: 'm2',\n timestamp: 1.23,\n });\n const audioData = args.audio_data as number[] | undefined;\n expect(audioData).toHaveLength(1);\n expect(audioData?.[0]).toBeCloseTo(0.1, 5);\n });\n\n it('forwards transcript updates with full segment payload', async () => {\n let capturedHandler: ((event: { payload: TranscriptUpdate }) => void) | null = null;\n const invoke = vi\n .fn<Parameters<InvokeMock>, ReturnType<InvokeMock>>()\n .mockResolvedValue(undefined);\n const listen = vi\n .fn<Parameters<ListenMock>, ReturnType<ListenMock>>()\n .mockImplementation((_event, handler) => {\n capturedHandler = handler as (event: { payload: TranscriptUpdate }) => void;\n return Promise.resolve(() => {});\n });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m1');\n\n const callback = vi.fn();\n await stream.onUpdate(callback);\n\n const payload: TranscriptUpdate = {\n meeting_id: 'm1',\n update_type: 'final',\n partial_text: undefined,\n segment: {\n segment_id: 12,\n text: 'Hello world',\n start_time: 1.2,\n end_time: 2.3,\n words: [\n { word: 'Hello', start_time: 1.2, end_time: 1.6, probability: 0.9 },\n { word: 'world', start_time: 1.6, end_time: 2.3, probability: 0.92 },\n ],\n language: 'en',\n language_confidence: 0.99,\n avg_logprob: -0.2,\n no_speech_prob: 0.01,\n speaker_id: 'SPEAKER_00',\n speaker_confidence: 0.95,\n },\n server_timestamp: 123.45,\n };\n\n if (!capturedHandler) {\n throw new Error('Transcript update handler not registered');\n }\n\n capturedHandler({ payload });\n\n expect(callback).toHaveBeenCalledWith(payload);\n });\n\n it('ignores transcript updates for other meetings', async () => {\n let capturedHandler: ((event: { payload: TranscriptUpdate }) => void) | null = null;\n const invoke = vi.fn<Parameters<InvokeMock>, ReturnType<InvokeMock>>().mockResolvedValue(undefined);\n const listen = vi\n .fn<Parameters<ListenMock>, ReturnType<ListenMock>>()\n .mockImplementation((_event, handler) => {\n capturedHandler = handler as (event: { payload: TranscriptUpdate }) => void;\n return Promise.resolve(() => {});\n });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m1');\n const callback = vi.fn();\n await stream.onUpdate(callback);\n\n capturedHandler?.({\n payload: {\n meeting_id: 'other',\n update_type: 'partial',\n partial_text: 'nope',\n server_timestamp: 1,\n },\n });\n\n expect(callback).not.toHaveBeenCalled();\n });\n\n it('maps connection and export commands with snake_case args', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue({ version: '1.0.0' });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.connect('localhost:50051');\n await api.saveExportFile('content', 'Meeting Notes', 'md');\n\n expect(invoke).toHaveBeenCalledWith('connect', { server_url: 'localhost:50051' });\n expect(invoke).toHaveBeenCalledWith('save_export_file', {\n content: 'content',\n default_name: 'Meeting Notes',\n extension: 'md',\n });\n });\n\n it('maps audio device selection with snake_case args', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue([]);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.listAudioDevices();\n await api.selectAudioDevice('input:0:Mic', true);\n\n expect(invoke).toHaveBeenCalledWith('list_audio_devices');\n expect(invoke).toHaveBeenCalledWith('select_audio_device', {\n device_id: 'input:0:Mic',\n is_input: true,\n });\n });\n\n it('maps playback commands with snake_case args', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue({\n meeting_id: 'm1',\n position: 0,\n duration: 0,\n is_playing: true,\n is_paused: false,\n });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.startPlayback('m1', 12.5);\n await api.seekPlayback(30);\n await api.getPlaybackState();\n\n expect(invoke).toHaveBeenCalledWith('start_playback', {\n meeting_id: 'm1',\n start_time: 12.5,\n });\n expect(invoke).toHaveBeenCalledWith('seek_playback', { position: 30 });\n expect(invoke).toHaveBeenCalledWith('get_playback_state');\n });\n\n it('stops transcription stream on close', async () => {\n const { invoke, listen } = createMocks();\n const unlisten = vi.fn();\n listen.mockResolvedValueOnce(unlisten);\n invoke.mockResolvedValue(undefined);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m1');\n\n await stream.onUpdate(() => {});\n stream.close();\n\n expect(unlisten).toHaveBeenCalled();\n expect(invoke).toHaveBeenCalledWith('stop_recording', { meeting_id: 'm1' });\n });\n\n it('stops transcription stream even without listeners', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValue(undefined);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const stream = await api.startTranscription('m1');\n stream.close();\n\n expect(invoke).toHaveBeenCalledWith('stop_recording', { meeting_id: 'm1' });\n });\n\n it('only caches meetings when list includes items', async () => {\n const { invoke, listen } = createMocks();\n const cacheSpy = vi.spyOn(meetingCache, 'cacheMeetings');\n\n invoke.mockResolvedValueOnce({ meetings: [], total_count: 0 });\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n await api.listMeetings({});\n expect(cacheSpy).not.toHaveBeenCalled();\n\n invoke.mockResolvedValueOnce({ meetings: [buildMeeting('m1')], total_count: 1 });\n await api.listMeetings({});\n expect(cacheSpy).toHaveBeenCalled();\n });\n\n it('returns false when delete meeting fails', async () => {\n const { invoke, listen } = createMocks();\n invoke.mockResolvedValueOnce({ success: false });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const result = await api.deleteMeeting('m1');\n\n expect(result).toBe(false);\n });\n\n it('generates summary with template options when available', async () => {\n const { invoke, listen } = createMocks();\n const summary = buildSummary('m1');\n\n invoke\n .mockResolvedValueOnce(\n buildPreferences({ tone: 'casual', format: 'narrative', verbosity: 'concise' })\n )\n .mockResolvedValueOnce(summary);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const result = await api.generateSummary('m1', true);\n\n expect(result).toEqual(summary);\n expect(invoke).toHaveBeenCalledWith('generate_summary', {\n meeting_id: 'm1',\n force_regenerate: true,\n options: { tone: 'casual', format: 'narrative', verbosity: 'concise' },\n });\n });\n\n it('generates summary even if preferences lookup fails', async () => {\n const { invoke, listen } = createMocks();\n const summary = buildSummary('m2');\n\n invoke.mockRejectedValueOnce(new Error('no prefs')).mockResolvedValueOnce(summary);\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n const result = await api.generateSummary('m2');\n\n expect(result).toEqual(summary);\n expect(invoke).toHaveBeenCalledWith('generate_summary', {\n meeting_id: 'm2',\n force_regenerate: false,\n options: undefined,\n });\n });\n\n it('covers additional adapter commands', async () => {\n const { invoke, listen } = createMocks();\n\n const annotation = {\n id: 'a1',\n meeting_id: 'm1',\n annotation_type: 'note',\n text: 'Note',\n start_time: 0,\n end_time: 1,\n segment_ids: [],\n created_at: 1,\n };\n\n const annotationResponses: Array<typeof annotation[] | { annotations: typeof annotation[] }> = [\n { annotations: [annotation] },\n [annotation],\n ];\n\n invoke.mockImplementation(async (cmd) => {\n switch (cmd) {\n case 'list_annotations':\n return annotationResponses.shift();\n case 'get_annotation':\n return annotation;\n case 'update_annotation':\n return annotation;\n case 'export_transcript':\n return { content: 'data', format_name: 'Markdown', file_extension: '.md' };\n case 'save_export_file':\n return true;\n case 'list_audio_devices':\n return [];\n case 'get_default_audio_device':\n return null;\n case 'get_preferences':\n return buildPreferences();\n case 'get_cloud_consent_status':\n return { consent_granted: true };\n case 'get_trigger_status':\n return {\n enabled: false,\n is_snoozed: false,\n snooze_remaining_secs: 0,\n pending_trigger: null,\n };\n case 'accept_trigger':\n return buildMeeting('m9');\n case 'extract_entities':\n return { entities: [], total_count: 0, cached: false };\n case 'update_entity':\n return { id: 'e1', text: 'Entity', category: 'other', segment_ids: [], confidence: 1 };\n case 'delete_entity':\n return true;\n case 'list_calendar_events':\n return { events: [], total_count: 0 };\n case 'get_calendar_providers':\n return { providers: [] };\n case 'initiate_oauth':\n return { auth_url: 'https://auth', state: 'state' };\n case 'complete_oauth':\n return { success: true, error_message: '' };\n case 'get_oauth_connection_status':\n return { is_connected: false, email: '', expires_at: 0 };\n case 'disconnect_oauth':\n return { success: true };\n case 'register_webhook':\n return {\n id: 'w1',\n workspace_id: 'w1',\n name: 'Webhook',\n url: 'https://example.com',\n events: ['meeting.completed'],\n enabled: true,\n timeout_ms: 1000,\n max_retries: 3,\n created_at: 1,\n updated_at: 1,\n };\n case 'list_webhooks':\n return { webhooks: [], total_count: 0 };\n case 'update_webhook':\n return {\n id: 'w1',\n workspace_id: 'w1',\n name: 'Webhook',\n url: 'https://example.com',\n events: ['meeting.completed'],\n enabled: false,\n timeout_ms: 1000,\n max_retries: 3,\n created_at: 1,\n updated_at: 2,\n };\n case 'delete_webhook':\n return { success: true };\n case 'get_webhook_deliveries':\n return { deliveries: [], total_count: 0 };\n case 'start_integration_sync':\n return { sync_run_id: 's1', status: 'running' };\n case 'get_sync_status':\n return { status: 'success', items_synced: 1, items_total: 1, error_message: '' };\n case 'list_sync_history':\n return { runs: [], total_count: 0 };\n case 'get_recent_logs':\n return { logs: [], total_count: 0 };\n case 'get_performance_metrics':\n return {\n current: {\n timestamp: 1,\n cpu_percent: 0,\n memory_percent: 0,\n memory_mb: 0,\n disk_percent: 0,\n network_bytes_sent: 0,\n network_bytes_recv: 0,\n process_memory_mb: 0,\n active_connections: 0,\n },\n history: [],\n };\n case 'refine_speakers':\n return { job_id: 'job', status: 'queued', segments_updated: 0, speaker_ids: [] };\n case 'get_diarization_status':\n return { job_id: 'job', status: 'completed', segments_updated: 1, speaker_ids: [] };\n case 'rename_speaker':\n return { success: true };\n case 'cancel_diarization':\n return { success: true, error_message: '', status: 'cancelled' };\n default:\n return undefined;\n }\n });\n\n const api = createTauriAPI(invoke as TauriInvoke, listen as TauriListen);\n\n const list1 = await api.listAnnotations('m1');\n const list2 = await api.listAnnotations('m1');\n expect(list1).toHaveLength(1);\n expect(list2).toHaveLength(1);\n\n await api.getAnnotation('a1');\n await api.updateAnnotation({ annotation_id: 'a1', text: 'Updated' });\n await api.exportTranscript('m1', 'markdown');\n await api.saveExportFile('content', 'Meeting', 'md');\n await api.listAudioDevices();\n await api.getDefaultAudioDevice(true);\n await api.selectAudioDevice('mic', true);\n await api.getPreferences();\n await api.savePreferences(buildPreferences());\n await api.grantCloudConsent();\n await api.revokeCloudConsent();\n await api.getCloudConsentStatus();\n await api.pausePlayback();\n await api.stopPlayback();\n await api.setTriggerEnabled(true);\n await api.snoozeTriggers(5);\n await api.resetSnooze();\n await api.getTriggerStatus();\n await api.dismissTrigger();\n await api.acceptTrigger('Title');\n await api.extractEntities('m1', true);\n await api.updateEntity('m1', 'e1', 'Entity', 'other');\n await api.deleteEntity('m1', 'e1');\n await api.listCalendarEvents(2, 5, 'google');\n await api.getCalendarProviders();\n await api.initiateCalendarAuth('google', 'redirect');\n await api.completeCalendarAuth('google', 'code', 'state');\n await api.getOAuthConnectionStatus('google');\n await api.disconnectCalendar('google');\n await api.registerWebhook({\n workspace_id: 'w1',\n name: 'Webhook',\n url: 'https://example.com',\n events: ['meeting.completed'],\n });\n await api.listWebhooks();\n await api.updateWebhook({ webhook_id: 'w1', name: 'Webhook' });\n await api.deleteWebhook('w1');\n await api.getWebhookDeliveries('w1', 10);\n await api.startIntegrationSync('int-1');\n await api.getSyncStatus('sync');\n await api.listSyncHistory('int-1', 10, 0);\n await api.getRecentLogs({ limit: 10 });\n await api.getPerformanceMetrics({ history_limit: 5 });\n await api.refineSpeakers('m1', 2);\n await api.getDiarizationJobStatus('job');\n await api.renameSpeaker('m1', 'old', 'new');\n await api.cancelDiarization('job');\n });\n});\n\ndescribe('tauri-adapter environment', () => {\n const invokeMock = vi.mocked(invoke);\n const listenMock = vi.mocked(listen);\n\n beforeEach(() => {\n invokeMock.mockReset();\n listenMock.mockReset();\n });\n\n it('detects tauri environment flags', () => {\n // @ts-expect-error intentionally unset\n vi.stubGlobal('window', undefined);\n expect(isTauriEnvironment()).toBe(false);\n vi.unstubAllGlobals();\n expect(isTauriEnvironment()).toBe(false);\n\n // @ts-expect-error set tauri flag\n (window as Record<string, unknown>).__TAURI__ = {};\n expect(isTauriEnvironment()).toBe(true);\n delete (window as Record<string, unknown>).__TAURI__;\n\n // @ts-expect-error set tauri internals flag\n (window as Record<string, unknown>).__TAURI_INTERNALS__ = {};\n expect(isTauriEnvironment()).toBe(true);\n delete (window as Record<string, unknown>).__TAURI_INTERNALS__;\n\n // @ts-expect-error set legacy flag\n (window as Record<string, unknown>).isTauri = true;\n expect(isTauriEnvironment()).toBe(true);\n delete (window as Record<string, unknown>).isTauri;\n });\n\n it('initializes tauri api when available', async () => {\n invokeMock.mockResolvedValueOnce(true);\n listenMock.mockResolvedValue(() => {});\n\n const api = await initializeTauriAPI();\n expect(api).toBeDefined();\n expect(invokeMock).toHaveBeenCalledWith('is_connected');\n });\n\n it('throws when tauri api is unavailable', async () => {\n invokeMock.mockRejectedValueOnce(new Error('no tauri'));\n\n await expect(initializeTauriAPI()).rejects.toThrow('Not running in Tauri environment');\n });\n\n it('throws a helpful error when invoke rejects with non-Error', async () => {\n invokeMock.mockRejectedValueOnce('no tauri');\n await expect(initializeTauriAPI()).rejects.toThrow('Not running in Tauri environment');\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/tauri-adapter.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/tauri-constants.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/tauri-helpers.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/tauri-helpers.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/tauri-transcription-stream.test.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":36,"column":11,"nodeType":"Property","messageId":"anyAssignment","endLine":36,"endColumn":87},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":91,"column":9,"nodeType":"Property","messageId":"anyAssignment","endLine":91,"endColumn":60},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":164,"column":11,"nodeType":"Property","messageId":"anyAssignment","endLine":164,"endColumn":61}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { beforeEach, describe, expect, it, vi } from 'vitest';\nimport {\n CONSECUTIVE_FAILURE_THRESHOLD,\n TauriTranscriptionStream,\n type TauriInvoke,\n type TauriListen,\n} from './tauri-adapter';\nimport { TauriCommands } from './tauri-constants';\n\ndescribe('TauriTranscriptionStream', () => {\n let mockInvoke: TauriInvoke;\n let mockListen: TauriListen;\n let stream: TauriTranscriptionStream;\n\n beforeEach(() => {\n mockInvoke = vi.fn().mockResolvedValue(undefined);\n mockListen = vi.fn().mockResolvedValue(() => {});\n stream = new TauriTranscriptionStream('meeting-123', mockInvoke, mockListen);\n });\n\n describe('send()', () => {\n it('calls invoke with correct command and args', async () => {\n const chunk = {\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.5, 1.0]),\n timestamp: 1.5,\n sample_rate: 48000,\n channels: 2,\n };\n\n stream.send(chunk);\n\n await vi.waitFor(() => {\n expect(mockInvoke).toHaveBeenCalledWith(TauriCommands.SEND_AUDIO_CHUNK, {\n meeting_id: 'meeting-123',\n audio_data: expect.arrayContaining([expect.any(Number), expect.any(Number)]),\n timestamp: 1.5,\n sample_rate: 48000,\n channels: 2,\n });\n });\n });\n\n it('resets consecutive failures on successful send', async () => {\n const errorCallback = vi.fn();\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Network error'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n failingStream.onError(errorCallback);\n\n // Send twice (below threshold of 3)\n failingStream.send({\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.1]),\n timestamp: 1,\n });\n failingStream.send({\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.1]),\n timestamp: 2,\n });\n\n await vi.waitFor(() => {\n expect(failingInvoke).toHaveBeenCalledTimes(2);\n });\n\n // Error should NOT be emitted yet (only 2 failures)\n expect(errorCallback).not.toHaveBeenCalled();\n });\n\n it('emits error after threshold consecutive failures', async () => {\n const errorCallback = vi.fn();\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Connection lost'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n failingStream.onError(errorCallback);\n\n // Send enough chunks to exceed threshold\n for (let i = 0; i < CONSECUTIVE_FAILURE_THRESHOLD + 1; i++) {\n failingStream.send({\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.1]),\n timestamp: i,\n });\n }\n\n await vi.waitFor(() => {\n expect(errorCallback).toHaveBeenCalledTimes(1);\n });\n\n expect(errorCallback).toHaveBeenCalledWith({\n code: 'stream_send_failed',\n message: expect.stringContaining('Connection lost'),\n });\n });\n\n it('only emits error once even with more failures', async () => {\n const errorCallback = vi.fn();\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Network error'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n failingStream.onError(errorCallback);\n\n // Send many chunks\n for (let i = 0; i < 10; i++) {\n failingStream.send({\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.1]),\n timestamp: i,\n });\n }\n\n await vi.waitFor(() => {\n expect(failingInvoke).toHaveBeenCalledTimes(10);\n });\n\n // Wait a bit more for all promises to settle\n await new Promise((r) => setTimeout(r, 100));\n\n // Error should only be emitted once\n expect(errorCallback).toHaveBeenCalledTimes(1);\n });\n\n it('logs errors to console', async () => {\n const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Test error'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n\n failingStream.send({\n meeting_id: 'meeting-123',\n audio_data: new Float32Array([0.1]),\n timestamp: 1,\n });\n\n await vi.waitFor(() => {\n expect(consoleSpy).toHaveBeenCalledWith(\n expect.stringContaining('[TauriTranscriptionStream] send_audio_chunk failed:')\n );\n });\n\n consoleSpy.mockRestore();\n });\n });\n\n describe('close()', () => {\n it('calls stop_recording command', async () => {\n stream.close();\n\n await vi.waitFor(() => {\n expect(mockInvoke).toHaveBeenCalledWith(TauriCommands.STOP_RECORDING, {\n meeting_id: 'meeting-123',\n });\n });\n });\n\n it('emits error on close failure', async () => {\n const errorCallback = vi.fn();\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Failed to stop'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n failingStream.onError(errorCallback);\n\n failingStream.close();\n\n await vi.waitFor(() => {\n expect(errorCallback).toHaveBeenCalledWith({\n code: 'stream_close_failed',\n message: expect.stringContaining('Failed to stop'),\n });\n });\n });\n\n it('logs close errors to console', async () => {\n const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});\n const failingInvoke = vi.fn().mockRejectedValue(new Error('Stop failed'));\n const failingStream = new TauriTranscriptionStream('meeting-123', failingInvoke, mockListen);\n\n failingStream.close();\n\n await vi.waitFor(() => {\n expect(consoleSpy).toHaveBeenCalledWith(\n expect.stringContaining('[TauriTranscriptionStream] stop_recording failed:')\n );\n });\n\n consoleSpy.mockRestore();\n });\n });\n\n describe('onUpdate()', () => {\n it('registers listener for transcript updates', async () => {\n const callback = vi.fn();\n await stream.onUpdate(callback);\n\n expect(mockListen).toHaveBeenCalledWith('transcript_update', expect.any(Function));\n });\n });\n\n describe('onError()', () => {\n it('registers error callback', () => {\n const callback = vi.fn();\n stream.onError(callback);\n\n // No immediate call\n expect(callback).not.toHaveBeenCalled();\n });\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/transcription-stream.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/core.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/enums.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/errors.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/errors.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/features.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/projects.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/api/types/requests.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/NavLink.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/analytics/logs-tab.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/analytics/logs-tab.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/analytics/performance-tab.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/analytics/performance-tab.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/analytics/speech-analysis-tab.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/annotation-type-badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/app-layout.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/app-sidebar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/calendar-connection-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/calendar-events-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/connection-status.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/empty-state.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/entity-highlight.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/entity-highlight.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/entity-management-panel.test.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'layout' is defined but never used. Allowed unused args must match /^_/u.","line":9,"column":23,"nodeType":null,"messageId":"unusedVar","endLine":9,"endColumn":29},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":51,"column":47,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":51,"endColumn":74},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":52,"column":52,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":52,"endColumn":84},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":53,"column":52,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":53,"endColumn":84},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":55,"column":22,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":55,"endColumn":35},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":60,"column":34,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":60,"endColumn":48}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":6,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { act, fireEvent, render, screen } from '@testing-library/react';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport { EntityManagementPanel } from './entity-management-panel';\nimport type { Entity } from '@/types/entity';\n\nvi.mock('framer-motion', () => ({\n AnimatePresence: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n motion: {\n div: ({ children, layout, ...rest }: { children: React.ReactNode; layout?: unknown }) => (\n <div {...rest}>{children}</div>\n ),\n },\n}));\n\nvi.mock('@/components/ui/scroll-area', () => ({\n ScrollArea: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n}));\n\nvi.mock('@/components/ui/sheet', () => ({\n Sheet: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n SheetTrigger: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n SheetContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n SheetHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n SheetTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n}));\n\nvi.mock('@/components/ui/dialog', () => ({\n Dialog: ({ open, children }: { open: boolean; children: React.ReactNode }) =>\n open ? <div>{children}</div> : null,\n DialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n DialogHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n DialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n DialogFooter: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n}));\n\nvi.mock('@/components/ui/select', () => ({\n Select: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n SelectTrigger: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n SelectValue: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n SelectContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n SelectItem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n}));\n\nconst addEntityAndNotify = vi.fn();\nconst updateEntityWithPersist = vi.fn();\nconst deleteEntityWithPersist = vi.fn();\nconst subscribeToEntities = vi.fn(() => () => {});\nconst getEntities = vi.fn();\n\nvi.mock('@/lib/entity-store', () => ({\n addEntityAndNotify: (...args: unknown[]) => addEntityAndNotify(...args),\n updateEntityWithPersist: (...args: unknown[]) => updateEntityWithPersist(...args),\n deleteEntityWithPersist: (...args: unknown[]) => deleteEntityWithPersist(...args),\n subscribeToEntities: (...args: unknown[]) => subscribeToEntities(...args),\n getEntities: () => getEntities(),\n}));\n\nconst toast = vi.fn();\nvi.mock('@/hooks/use-toast', () => ({\n toast: (...args: unknown[]) => toast(...args),\n}));\n\nconst baseEntities: Entity[] = [\n {\n id: 'e1',\n text: 'API',\n aliases: ['api'],\n category: 'technical',\n description: 'Core API platform',\n source: 'Docs',\n extractedAt: new Date(),\n },\n {\n id: 'e2',\n text: 'Roadmap',\n aliases: [],\n category: 'product',\n description: 'Product roadmap',\n source: 'Plan',\n extractedAt: new Date(),\n },\n];\n\ndescribe('EntityManagementPanel', () => {\n beforeEach(() => {\n getEntities.mockReturnValue([...baseEntities]);\n });\n\n afterEach(() => {\n vi.clearAllMocks();\n });\n\n it('filters entities by search query', () => {\n render(<EntityManagementPanel />);\n\n expect(screen.getByText('API')).toBeInTheDocument();\n expect(screen.getByText('Roadmap')).toBeInTheDocument();\n\n const searchInput = screen.getByPlaceholderText('Search entities...');\n fireEvent.change(searchInput, { target: { value: 'api' } });\n\n expect(screen.getByText('API')).toBeInTheDocument();\n expect(screen.queryByText('Roadmap')).not.toBeInTheDocument();\n\n fireEvent.change(searchInput, { target: { value: 'nomatch' } });\n expect(screen.getByText('No matching entities found')).toBeInTheDocument();\n });\n\n it('adds, edits, and deletes entities when persisted', async () => {\n updateEntityWithPersist.mockResolvedValue(undefined);\n deleteEntityWithPersist.mockResolvedValue(undefined);\n\n render(<EntityManagementPanel meetingId=\"m1\" />);\n\n const addEntityButtons = screen.getAllByRole('button', { name: 'Add Entity' });\n await act(async () => {\n fireEvent.click(addEntityButtons[0]);\n });\n\n fireEvent.change(screen.getByLabelText('Text *'), { target: { value: 'New' } });\n fireEvent.change(screen.getByLabelText('Aliases (comma-separated)'), {\n target: { value: 'new, alias' },\n });\n fireEvent.change(screen.getByLabelText('Description *'), {\n target: { value: 'New description' },\n });\n\n const submitButtons = screen.getAllByRole('button', { name: 'Add Entity' });\n await act(async () => {\n fireEvent.click(submitButtons[1]);\n });\n expect(addEntityAndNotify).toHaveBeenCalledWith({\n text: 'New',\n aliases: ['new', 'alias'],\n category: 'other',\n description: 'New description',\n source: undefined,\n });\n\n const editButtons = screen.getAllByRole('button', { name: 'Edit entity' });\n await act(async () => {\n fireEvent.click(editButtons[0]);\n });\n\n fireEvent.change(screen.getByLabelText('Text *'), { target: { value: 'API v2' } });\n fireEvent.change(screen.getByLabelText('Description *'), {\n target: { value: 'Updated' },\n });\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Save Changes' }));\n });\n\n expect(updateEntityWithPersist).toHaveBeenCalledWith('m1', 'e1', {\n text: 'API v2',\n category: 'technical',\n });\n\n const deleteButtons = screen.getAllByRole('button', { name: 'Delete entity' });\n await act(async () => {\n fireEvent.click(deleteButtons[0]);\n });\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Delete' }));\n });\n\n expect(deleteEntityWithPersist).toHaveBeenCalledWith('m1', 'e1');\n expect(toast).toHaveBeenCalled();\n });\n\n it('handles update errors and non-persisted edits', async () => {\n updateEntityWithPersist.mockRejectedValueOnce(new Error('nope'));\n\n render(<EntityManagementPanel />);\n\n const editButtons = screen.getAllByRole('button', { name: 'Edit entity' });\n await act(async () => {\n fireEvent.click(editButtons[0]);\n });\n\n fireEvent.change(screen.getByLabelText('Text *'), { target: { value: 'API v3' } });\n fireEvent.change(screen.getByLabelText('Description *'), {\n target: { value: 'Updated' },\n });\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Save Changes' }));\n });\n\n expect(updateEntityWithPersist).not.toHaveBeenCalled();\n expect(toast).toHaveBeenCalled();\n });\n\n it('shows delete error toast on failure', async () => {\n deleteEntityWithPersist.mockRejectedValueOnce(new Error('fail'));\n\n render(<EntityManagementPanel meetingId=\"m1\" />);\n\n const deleteButtons = screen.getAllByRole('button', { name: 'Delete entity' });\n await act(async () => {\n fireEvent.click(deleteButtons[0]);\n });\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Delete' }));\n });\n\n expect(deleteEntityWithPersist).toHaveBeenCalledWith('m1', 'e1');\n expect(toast).toHaveBeenCalled();\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/entity-management-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/error-boundary.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/integration-config-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/meeting-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/meeting-state-badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/offline-banner.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/offline-banner.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/preferences-sync-bridge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/preferences-sync-status.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/preferences-sync-status.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/priority-badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/projects/ProjectList.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/projects/ProjectMembersPanel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/projects/ProjectSettingsPanel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/projects/ProjectSidebar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/projects/ProjectSwitcher.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/audio-device-selector.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/audio-device-selector.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/audio-level-meter.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/audio-level-meter.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/buffering-indicator.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/buffering-indicator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/confidence-indicator.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/confidence-indicator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/idle-state.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/idle-state.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/index.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/listening-state.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/partial-text-display.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/recording-components.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/recording-header.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/recording-header.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/speaker-distribution.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/speaker-distribution.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/stat-card.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/stat-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/stats-content.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/transcript-segment-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/vad-indicator.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/recording/vad-indicator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/ai-config-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/audio-devices-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/developer-options-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/export-ai-section.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/export-ai-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/integrations-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/provider-config-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/quick-actions-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/settings/server-connection-section.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/simulation-indicator.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/simulation-indicator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/speaker-badge.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/speaker-badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/stats-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/sync-control-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/sync-history-log.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/sync-status-indicator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/tauri-event-listener.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/timestamped-notes-editor.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/timestamped-notes-editor.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/top-bar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/accordion.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/alert-dialog.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/alert.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/aspect-ratio.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/avatar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/badge.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":51,"column":17,"nodeType":"Identifier","messageId":"namedExport","endLine":51,"endColumn":30,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/breadcrumb.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/button.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":60,"column":18,"nodeType":"Identifier","messageId":"namedExport","endLine":60,"endColumn":32,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/calendar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/carousel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/chart.tsx","messages":[],"suppressedMessages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":175,"column":19,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":175,"endColumn":76,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .fill on an `any` value.","line":175,"column":58,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":175,"endColumn":62,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type `any` assigned to a parameter of type `Payload<ValueType, NameType>[]`.","line":186,"column":65,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":186,"endColumn":77,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":206,"column":31,"nodeType":"Property","messageId":"anyAssignment","endLine":206,"endColumn":59,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":207,"column":31,"nodeType":"Property","messageId":"anyAssignment","endLine":207,"endColumn":63,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":274,"column":18,"nodeType":"MemberExpression","messageId":"anyAssignment","endLine":274,"endColumn":28,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/checkbox.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/collapsible.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/command.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/context-menu.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/dialog.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/drawer.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/dropdown-menu.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/dropdown-menu.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/form.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":164,"column":3,"nodeType":"Identifier","messageId":"namedExport","endLine":164,"endColumn":15,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/hover-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/input-otp.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/input.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/label.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/menubar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/navigation-menu.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":113,"column":3,"nodeType":"Identifier","messageId":"namedExport","endLine":113,"endColumn":29,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/pagination.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/popover.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/progress.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/radio-group.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/resizable.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/resizable.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/scroll-area.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/select.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/separator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/sheet.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/sidebar.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":735,"column":3,"nodeType":"Identifier","messageId":"namedExport","endLine":735,"endColumn":13,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/skeleton.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/slider.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/sonner.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":28,"column":19,"nodeType":"Identifier","messageId":"namedExport","endLine":28,"endColumn":24,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/status-badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/switch.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/table.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/tabs.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/textarea.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/toast.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/toaster.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/toggle-group.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/toggle.tsx","messages":[],"suppressedMessages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":43,"column":18,"nodeType":"Identifier","messageId":"namedExport","endLine":43,"endColumn":32,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/tooltip.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/ui-components.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/ui/use-toast.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/upcoming-meetings.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/webhook-settings-panel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/workspace-switcher.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/components/workspace-switcher.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/contexts/connection-context.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/contexts/connection-context.tsx","messages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":55,"column":17,"nodeType":"Identifier","messageId":"namedExport","endLine":55,"endColumn":35}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Connection context for offline/cached read-only mode\n\nimport { createContext, useContext, useEffect, useMemo, useState } from 'react';\nimport {\n type ConnectionState,\n getConnectionState,\n setConnectionMode,\n setConnectionServerUrl,\n subscribeConnectionState,\n} from '@/api/connection-state';\nimport { useTauriEvent } from '@/lib/tauri-events';\n\ninterface ConnectionHelpers {\n state: ConnectionState;\n isConnected: boolean;\n isReadOnly: boolean;\n isReconnecting: boolean;\n}\n\nconst ConnectionContext = createContext<ConnectionHelpers | null>(null);\n\nexport function ConnectionProvider({ children }: { children: React.ReactNode }) {\n const [state, setState] = useState<ConnectionState>(() => getConnectionState());\n\n useEffect(() => subscribeConnectionState(setState), []);\n\n useTauriEvent(\n 'connection_change',\n (payload) => {\n if (payload.is_connected) {\n setConnectionMode('connected');\n setConnectionServerUrl(payload.server_url);\n return;\n }\n setConnectionMode('cached', payload.error ?? null);\n setConnectionServerUrl(payload.server_url);\n },\n []\n );\n\n const value = useMemo<ConnectionHelpers>(() => {\n const isConnected = state.mode === 'connected';\n const isReconnecting = state.mode === 'reconnecting';\n const isReadOnly =\n state.mode === 'cached' ||\n state.mode === 'disconnected' ||\n state.mode === 'mock' ||\n state.mode === 'reconnecting';\n return { state, isConnected, isReadOnly, isReconnecting };\n }, [state]);\n\n return <ConnectionContext.Provider value={value}>{children}</ConnectionContext.Provider>;\n}\n\nexport function useConnectionState(): ConnectionHelpers {\n const context = useContext(ConnectionContext);\n if (!context) {\n return {\n state: getConnectionState(),\n isConnected: false,\n isReadOnly: true,\n isReconnecting: false,\n };\n }\n return context;\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/contexts/project-context.tsx","messages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":251,"column":17,"nodeType":"Identifier","messageId":"namedExport","endLine":251,"endColumn":28}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Project context for managing active project selection and project data\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';\nimport { IdentityDefaults } from '@/api/constants';\nimport { getAPI } from '@/api/interface';\nimport type { CreateProjectRequest, Project, UpdateProjectRequest } from '@/api/types';\nimport { useWorkspace } from '@/contexts/workspace-context';\n\ninterface ProjectContextValue {\n projects: Project[];\n activeProject: Project | null;\n switchProject: (projectId: string) => void;\n refreshProjects: () => Promise<void>;\n createProject: (request: Omit<CreateProjectRequest, 'workspace_id'> & { workspace_id?: string }) => Promise<Project>;\n updateProject: (request: UpdateProjectRequest) => Promise<Project>;\n archiveProject: (projectId: string) => Promise<Project>;\n restoreProject: (projectId: string) => Promise<Project>;\n deleteProject: (projectId: string) => Promise<boolean>;\n isLoading: boolean;\n error: string | null;\n}\n\nconst STORAGE_KEY_PREFIX = 'noteflow_active_project_id';\n\nconst ProjectContext = createContext<ProjectContextValue | null>(null);\n\nfunction storageKey(workspaceId: string): string {\n return `${STORAGE_KEY_PREFIX}:${workspaceId}`;\n}\n\nfunction readStoredProjectId(workspaceId: string): string | null {\n try {\n return localStorage.getItem(storageKey(workspaceId));\n } catch {\n return null;\n }\n}\n\nfunction persistProjectId(workspaceId: string, projectId: string): void {\n try {\n localStorage.setItem(storageKey(workspaceId), projectId);\n } catch {\n // Ignore storage failures\n }\n}\n\nfunction resolveActiveProject(projects: Project[], preferredId: string | null): Project | null {\n if (!projects.length) {\n return null;\n }\n const activeCandidates = projects.filter((project) => !project.is_archived);\n if (preferredId) {\n const match = activeCandidates.find((project) => project.id === preferredId);\n if (match) {\n return match;\n }\n }\n const defaultProject = activeCandidates.find((project) => project.is_default);\n return defaultProject ?? activeCandidates[0] ?? null;\n}\n\nfunction fallbackProject(workspaceId: string): Project {\n return {\n id: IdentityDefaults.DEFAULT_PROJECT_ID,\n workspace_id: workspaceId,\n name: IdentityDefaults.DEFAULT_PROJECT_NAME,\n slug: 'general',\n description: 'Default project',\n is_default: true,\n is_archived: false,\n settings: {},\n created_at: 0,\n updated_at: 0,\n };\n}\n\nexport function ProjectProvider({ children }: { children: React.ReactNode }) {\n const { currentWorkspace } = useWorkspace();\n const [projects, setProjects] = useState<Project[]>([]);\n const [activeProjectId, setActiveProjectId] = useState<string | null>(null);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const loadProjects = useCallback(async () => {\n if (!currentWorkspace) {\n return;\n }\n setIsLoading(true);\n setError(null);\n try {\n const response = await getAPI().listProjects({\n workspace_id: currentWorkspace.id,\n include_archived: true,\n limit: 200,\n offset: 0,\n });\n let preferredId = readStoredProjectId(currentWorkspace.id);\n try {\n const activeResponse = await getAPI().getActiveProject({\n workspace_id: currentWorkspace.id,\n });\n const activeId = activeResponse.project_id ?? activeResponse.project?.id;\n if (activeId) {\n preferredId = activeId;\n }\n } catch {\n // Ignore active project lookup failures (offline or unsupported)\n }\n const available = response.projects.length\n ? response.projects\n : [fallbackProject(currentWorkspace.id)];\n setProjects(available);\n const resolved = resolveActiveProject(available, preferredId);\n setActiveProjectId(resolved?.id ?? null);\n if (resolved) {\n persistProjectId(currentWorkspace.id, resolved.id);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load projects');\n const fallback = fallbackProject(currentWorkspace.id);\n setProjects([fallback]);\n setActiveProjectId(fallback.id);\n persistProjectId(currentWorkspace.id, fallback.id);\n } finally {\n setIsLoading(false);\n }\n }, [currentWorkspace]);\n\n useEffect(() => {\n void loadProjects();\n }, [loadProjects]);\n\n const switchProject = useCallback(\n (projectId: string) => {\n if (!currentWorkspace) {\n return;\n }\n setActiveProjectId(projectId);\n persistProjectId(currentWorkspace.id, projectId);\n void getAPI()\n .setActiveProject({ workspace_id: currentWorkspace.id, project_id: projectId })\n .catch(() => {\n // Failed to persist active project - context state already updated\n });\n },\n [currentWorkspace]\n );\n\n const createProject = useCallback(\n async (\n request: Omit<CreateProjectRequest, 'workspace_id'> & { workspace_id?: string }\n ): Promise<Project> => {\n const workspaceId = request.workspace_id ?? currentWorkspace?.id;\n if (!workspaceId) {\n throw new Error('Workspace is required to create a project');\n }\n const project = await getAPI().createProject({ ...request, workspace_id: workspaceId });\n setProjects((prev) => [project, ...prev]);\n switchProject(project.id);\n return project;\n },\n [currentWorkspace, switchProject]\n );\n\n const updateProject = useCallback(async (request: UpdateProjectRequest): Promise<Project> => {\n const updated = await getAPI().updateProject(request);\n setProjects((prev) => prev.map((project) => (project.id === updated.id ? updated : project)));\n return updated;\n }, []);\n\n const archiveProject = useCallback(\n async (projectId: string): Promise<Project> => {\n const updated = await getAPI().archiveProject(projectId);\n const nextProjects = projects.map((project) =>\n project.id === updated.id ? updated : project\n );\n setProjects(nextProjects);\n if (activeProjectId === projectId && currentWorkspace) {\n const nextActive = resolveActiveProject(nextProjects, null);\n if (nextActive) {\n switchProject(nextActive.id);\n }\n }\n return updated;\n },\n [activeProjectId, currentWorkspace, projects, switchProject]\n );\n\n const restoreProject = useCallback(async (projectId: string): Promise<Project> => {\n const updated = await getAPI().restoreProject(projectId);\n setProjects((prev) => prev.map((project) => (project.id === updated.id ? updated : project)));\n return updated;\n }, []);\n\n const deleteProject = useCallback(async (projectId: string): Promise<boolean> => {\n const deleted = await getAPI().deleteProject(projectId);\n if (deleted) {\n setProjects((prev) => prev.filter((project) => project.id !== projectId));\n if (activeProjectId === projectId && currentWorkspace) {\n const next = resolveActiveProject(\n projects.filter((project) => project.id !== projectId),\n null\n );\n if (next) {\n switchProject(next.id);\n }\n }\n }\n return deleted;\n }, [activeProjectId, currentWorkspace, projects, switchProject]);\n\n const activeProject = useMemo(() => {\n if (!activeProjectId) {\n return null;\n }\n return projects.find((project) => project.id === activeProjectId) ?? null;\n }, [activeProjectId, projects]);\n\n const value = useMemo<ProjectContextValue>(\n () => ({\n projects,\n activeProject,\n switchProject,\n refreshProjects: loadProjects,\n createProject,\n updateProject,\n archiveProject,\n restoreProject,\n deleteProject,\n isLoading,\n error,\n }),\n [\n projects,\n activeProject,\n switchProject,\n loadProjects,\n createProject,\n updateProject,\n archiveProject,\n restoreProject,\n deleteProject,\n isLoading,\n error,\n ]\n );\n\n return <ProjectContext.Provider value={value}>{children}</ProjectContext.Provider>;\n}\n\nexport function useProjects(): ProjectContextValue {\n const context = useContext(ProjectContext);\n if (!context) {\n throw new Error('useProjects must be used within ProjectProvider');\n }\n return context;\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/contexts/workspace-context.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/contexts/workspace-context.tsx","messages":[{"ruleId":"react-refresh/only-export-components","severity":1,"message":"Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.","line":150,"column":17,"nodeType":"Identifier","messageId":"namedExport","endLine":150,"endColumn":29}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Workspace context for managing current user/workspace identity\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';\nimport { IdentityDefaults } from '@/api/constants';\nimport { getAPI } from '@/api/interface';\nimport type { GetCurrentUserResponse, Workspace } from '@/api/types';\n\ninterface WorkspaceContextValue {\n currentWorkspace: Workspace | null;\n workspaces: Workspace[];\n currentUser: GetCurrentUserResponse | null;\n switchWorkspace: (workspaceId: string) => Promise<void>;\n isLoading: boolean;\n error: string | null;\n}\n\nconst STORAGE_KEY = 'noteflow_current_workspace_id';\nconst fallbackUser: GetCurrentUserResponse = {\n user_id: IdentityDefaults.DEFAULT_USER_ID,\n display_name: IdentityDefaults.DEFAULT_USER_NAME,\n};\nconst fallbackWorkspace: Workspace = {\n id: IdentityDefaults.DEFAULT_WORKSPACE_ID,\n name: IdentityDefaults.DEFAULT_WORKSPACE_NAME,\n role: 'owner',\n is_default: true,\n};\n\nconst WorkspaceContext = createContext<WorkspaceContextValue | null>(null);\n\nfunction readStoredWorkspaceId(): string | null {\n try {\n return localStorage.getItem(STORAGE_KEY);\n } catch {\n return null;\n }\n}\n\nfunction persistWorkspaceId(workspaceId: string): void {\n try {\n localStorage.setItem(STORAGE_KEY, workspaceId);\n } catch {\n // Ignore storage failures (private mode or blocked)\n }\n}\n\nfunction resolveWorkspace(\n workspaces: Workspace[],\n preferredId: string | null\n): Workspace | null {\n if (!workspaces.length) {\n return null;\n }\n if (preferredId) {\n const byId = workspaces.find((workspace) => workspace.id === preferredId);\n if (byId) {\n return byId;\n }\n }\n const defaultWorkspace = workspaces.find((workspace) => workspace.is_default);\n return defaultWorkspace ?? workspaces[0] ?? null;\n}\n\nexport function WorkspaceProvider({ children }: { children: React.ReactNode }) {\n const [currentWorkspace, setCurrentWorkspace] = useState<Workspace | null>(null);\n const [workspaces, setWorkspaces] = useState<Workspace[]>([]);\n const [currentUser, setCurrentUser] = useState<GetCurrentUserResponse | null>(null);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const loadContext = useCallback(async () => {\n setIsLoading(true);\n setError(null);\n try {\n const api = getAPI();\n const [user, workspaceResponse] = await Promise.all([\n api.getCurrentUser(),\n api.listWorkspaces(),\n ]);\n\n const availableWorkspaces =\n workspaceResponse.workspaces.length > 0 ? workspaceResponse.workspaces : [fallbackWorkspace];\n\n setCurrentUser(user ?? fallbackUser);\n setWorkspaces(availableWorkspaces);\n\n const storedId = readStoredWorkspaceId();\n const selected = resolveWorkspace(availableWorkspaces, storedId);\n setCurrentWorkspace(selected);\n if (selected) {\n persistWorkspaceId(selected.id);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load workspace context');\n setCurrentUser(fallbackUser);\n setWorkspaces([fallbackWorkspace]);\n setCurrentWorkspace(fallbackWorkspace);\n persistWorkspaceId(fallbackWorkspace.id);\n } finally {\n setIsLoading(false);\n }\n }, []);\n\n useEffect(() => {\n void loadContext();\n }, [loadContext]);\n\n const switchWorkspace = useCallback(\n async (workspaceId: string) => {\n if (!workspaceId) {\n return;\n }\n setIsLoading(true);\n setError(null);\n try {\n const api = getAPI();\n const response = await api.switchWorkspace(workspaceId);\n const selected =\n response.workspace ?? workspaces.find((workspace) => workspace.id === workspaceId);\n if (!response.success || !selected) {\n throw new Error('Workspace not found');\n }\n setCurrentWorkspace(selected);\n persistWorkspaceId(selected.id);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to switch workspace');\n throw err;\n } finally {\n setIsLoading(false);\n }\n },\n [workspaces]\n );\n\n const value = useMemo<WorkspaceContextValue>(\n () => ({\n currentWorkspace,\n workspaces,\n currentUser,\n switchWorkspace,\n isLoading,\n error,\n }),\n [currentWorkspace, workspaces, currentUser, switchWorkspace, isLoading, error]\n );\n\n return <WorkspaceContext.Provider value={value}>{children}</WorkspaceContext.Provider>;\n}\n\nexport function useWorkspace(): WorkspaceContextValue {\n const context = useContext(WorkspaceContext);\n if (!context) {\n throw new Error('useWorkspace must be used within WorkspaceProvider');\n }\n return context;\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-audio-devices.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-audio-devices.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type error typed assigned to a parameter of type `string`.","line":211,"column":22,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":211,"endColumn":52},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type error typed assigned to a parameter of type `string`.","line":279,"column":22,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":279,"endColumn":51},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type error typed assigned to a parameter of type `string`.","line":312,"column":22,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":312,"endColumn":53}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Shared Audio Device Management Hook\n *\n * Provides audio device enumeration, selection, and testing functionality.\n * Used by both Settings page and Recording page.\n *\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { initializeAPI } from '@/api';\nimport { TauriCommands, Timing } from '@/api/constants';\nimport { isTauriEnvironment, TauriEvents } from '@/api/tauri-adapter';\nimport { toast } from '@/hooks/use-toast';\nimport { preferences } from '@/lib/preferences';\nimport { type AudioTestLevelEvent, useTauriEvent } from '@/lib/tauri-events';\n\nexport interface AudioDevice {\n deviceId: string;\n label: string;\n kind: 'audioinput' | 'audiooutput';\n}\n\ninterface UseAudioDevicesOptions {\n /** Auto-load devices on mount */\n autoLoad?: boolean;\n /** Show toast notifications */\n showToasts?: boolean;\n}\n\ninterface UseAudioDevicesReturn {\n // Device lists\n inputDevices: AudioDevice[];\n outputDevices: AudioDevice[];\n\n // Selected devices\n selectedInputDevice: string;\n selectedOutputDevice: string;\n\n // State\n isLoading: boolean;\n hasPermission: boolean | null;\n\n // Actions\n loadDevices: () => Promise<void>;\n setInputDevice: (deviceId: string) => void;\n setOutputDevice: (deviceId: string) => void;\n\n // Testing\n isTestingInput: boolean;\n isTestingOutput: boolean;\n inputLevel: number;\n startInputTest: () => Promise<void>;\n stopInputTest: () => Promise<void>;\n testOutputDevice: () => Promise<void>;\n}\n\n/**\n * Hook for managing audio device selection and testing\n */\nexport function useAudioDevices(options: UseAudioDevicesOptions = {}): UseAudioDevicesReturn {\n const { autoLoad = false, showToasts = true } = options;\n\n // Device lists\n const [inputDevices, setInputDevices] = useState<AudioDevice[]>([]);\n const [outputDevices, setOutputDevices] = useState<AudioDevice[]>([]);\n\n // Selected devices (from preferences)\n const [selectedInputDevice, setSelectedInputDevice] = useState<string>(\n preferences.get().audio_devices.input_device_id\n );\n const [selectedOutputDevice, setSelectedOutputDevice] = useState<string>(\n preferences.get().audio_devices.output_device_id\n );\n\n // State\n const [isLoading, setIsLoading] = useState(false);\n const [hasPermission, setHasPermission] = useState<boolean | null>(null);\n\n // Testing state\n const [isTestingInput, setIsTestingInput] = useState(false);\n const [isTestingOutput, setIsTestingOutput] = useState(false);\n const [inputLevel, setInputLevel] = useState(0);\n\n // Refs for audio context\n const audioContextRef = useRef<AudioContext | null>(null);\n const analyserRef = useRef<AnalyserNode | null>(null);\n const mediaStreamRef = useRef<MediaStream | null>(null);\n const animationFrameRef = useRef<number | null>(null);\n\n /**\n * Load available audio devices\n * Uses Web Audio API for browser, Tauri command for desktop\n */\n const loadDevices = useCallback(async () => {\n setIsLoading(true);\n\n try {\n if (isTauriEnvironment()) {\n const api = await initializeAPI();\n const devices = await api.listAudioDevices();\n const inputs = devices\n .filter((device) => device.is_input)\n .map((device) => ({\n deviceId: device.id,\n label: device.name,\n kind: 'audioinput' as const,\n }));\n const outputs = devices\n .filter((device) => !device.is_input)\n .map((device) => ({\n deviceId: device.id,\n label: device.name,\n kind: 'audiooutput' as const,\n }));\n\n setHasPermission(true);\n setInputDevices(inputs);\n setOutputDevices(outputs);\n\n if (inputs.length > 0 && !selectedInputDevice) {\n setSelectedInputDevice(inputs[0].deviceId);\n preferences.setAudioDevice('input', inputs[0].deviceId);\n await api.selectAudioDevice(inputs[0].deviceId, true);\n }\n if (outputs.length > 0 && !selectedOutputDevice) {\n setSelectedOutputDevice(outputs[0].deviceId);\n preferences.setAudioDevice('output', outputs[0].deviceId);\n await api.selectAudioDevice(outputs[0].deviceId, false);\n }\n return;\n }\n\n // Request permission first\n await navigator.mediaDevices.getUserMedia({ audio: true });\n setHasPermission(true);\n\n const devices = await navigator.mediaDevices.enumerateDevices();\n\n const inputs = devices\n .filter((d) => d.kind === 'audioinput')\n .map((d, i) => ({\n deviceId: d.deviceId,\n label: d.label || `Microphone ${i + 1}`,\n kind: 'audioinput' as const,\n }));\n\n const outputs = devices\n .filter((d) => d.kind === 'audiooutput')\n .map((d, i) => ({\n deviceId: d.deviceId,\n label: d.label || `Speaker ${i + 1}`,\n kind: 'audiooutput' as const,\n }));\n\n setInputDevices(inputs);\n setOutputDevices(outputs);\n\n // Auto-select first device if none selected\n if (inputs.length > 0 && !selectedInputDevice) {\n setSelectedInputDevice(inputs[0].deviceId);\n preferences.setAudioDevice('input', inputs[0].deviceId);\n }\n if (outputs.length > 0 && !selectedOutputDevice) {\n setSelectedOutputDevice(outputs[0].deviceId);\n preferences.setAudioDevice('output', outputs[0].deviceId);\n }\n } catch (_error) {\n setHasPermission(false);\n if (showToasts) {\n toast({\n title: 'Audio access denied',\n description: 'Please allow audio access to detect devices',\n variant: 'destructive',\n });\n }\n } finally {\n setIsLoading(false);\n }\n }, [selectedInputDevice, selectedOutputDevice, showToasts]);\n\n /**\n * Set the selected input device and persist to preferences\n */\n const setInputDevice = useCallback((deviceId: string) => {\n setSelectedInputDevice(deviceId);\n preferences.setAudioDevice('input', deviceId);\n if (isTauriEnvironment()) {\n void initializeAPI().then((api) => api.selectAudioDevice(deviceId, true));\n }\n }, []);\n\n /**\n * Set the selected output device and persist to preferences\n */\n const setOutputDevice = useCallback((deviceId: string) => {\n setSelectedOutputDevice(deviceId);\n preferences.setAudioDevice('output', deviceId);\n if (isTauriEnvironment()) {\n void initializeAPI().then((api) => api.selectAudioDevice(deviceId, false));\n }\n }, []);\n\n /**\n * Start testing the selected input device (microphone level visualization)\n */\n const startInputTest = useCallback(async () => {\n if (isTauriEnvironment()) {\n try {\n const { invoke } = await import('@tauri-apps/api/core');\n setIsTestingInput(true);\n await invoke(TauriCommands.START_INPUT_TEST, {\n device_id: selectedInputDevice || null,\n });\n if (showToasts) {\n toast({ title: 'Input test started', description: 'Speak into your microphone' });\n }\n } catch (err) {\n if (showToasts) {\n toast({\n title: 'Failed to test input',\n description: String(err),\n variant: 'destructive',\n });\n }\n setIsTestingInput(false);\n }\n return;\n }\n // Browser implementation\n try {\n setIsTestingInput(true);\n\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: { deviceId: selectedInputDevice ? { exact: selectedInputDevice } : undefined },\n });\n mediaStreamRef.current = stream;\n\n audioContextRef.current = new AudioContext();\n analyserRef.current = audioContextRef.current.createAnalyser();\n const source = audioContextRef.current.createMediaStreamSource(stream);\n source.connect(analyserRef.current);\n analyserRef.current.fftSize = 256;\n\n const dataArray = new Uint8Array(analyserRef.current.frequencyBinCount);\n\n const updateLevel = () => {\n if (!analyserRef.current) {\n return;\n }\n analyserRef.current.getByteFrequencyData(dataArray);\n const avg = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;\n setInputLevel(avg / 255);\n animationFrameRef.current = requestAnimationFrame(updateLevel);\n };\n updateLevel();\n\n if (showToasts) {\n toast({ title: 'Input test started', description: 'Speak into your microphone' });\n }\n } catch {\n if (showToasts) {\n toast({\n title: 'Failed to test input',\n description: 'Could not access microphone',\n variant: 'destructive',\n });\n }\n setIsTestingInput(false);\n }\n }, [selectedInputDevice, showToasts]);\n\n /**\n * Stop the input device test\n */\n const stopInputTest = useCallback(async () => {\n if (isTauriEnvironment()) {\n try {\n const { invoke } = await import('@tauri-apps/api/core');\n await invoke(TauriCommands.STOP_INPUT_TEST);\n } catch {\n // Tauri invoke failed - stop test command is non-critical cleanup\n }\n }\n\n setIsTestingInput(false);\n setInputLevel(0);\n\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n if (mediaStreamRef.current) {\n for (const track of mediaStreamRef.current.getTracks()) {\n track.stop();\n }\n mediaStreamRef.current = null;\n }\n if (audioContextRef.current) {\n audioContextRef.current.close();\n audioContextRef.current = null;\n }\n }, []);\n\n /**\n * Test the output device by playing a tone\n */\n const testOutputDevice = useCallback(async () => {\n if (isTauriEnvironment()) {\n try {\n const { invoke } = await import('@tauri-apps/api/core');\n setIsTestingOutput(true);\n await invoke(TauriCommands.START_OUTPUT_TEST, {\n device_id: selectedOutputDevice || null,\n });\n if (showToasts) {\n toast({ title: 'Output test', description: 'Playing test tone' });\n }\n // Output test auto-stops after 2 seconds\n setTimeout(() => setIsTestingOutput(false), Timing.TWO_SECONDS_MS);\n } catch (err) {\n if (showToasts) {\n toast({\n title: 'Failed to test output',\n description: String(err),\n variant: 'destructive',\n });\n }\n setIsTestingOutput(false);\n }\n return;\n }\n // Browser implementation\n setIsTestingOutput(true);\n try {\n const audioContext = new AudioContext();\n const oscillator = audioContext.createOscillator();\n const gainNode = audioContext.createGain();\n\n oscillator.connect(gainNode);\n gainNode.connect(audioContext.destination);\n\n oscillator.frequency.setValueAtTime(440, audioContext.currentTime);\n gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);\n gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);\n\n oscillator.start(audioContext.currentTime);\n oscillator.stop(audioContext.currentTime + 0.5);\n\n if (showToasts) {\n toast({ title: 'Output test', description: 'Playing test tone' });\n }\n\n setTimeout(() => {\n setIsTestingOutput(false);\n audioContext.close();\n }, 500);\n } catch {\n if (showToasts) {\n toast({\n title: 'Failed to test output',\n description: 'Could not play audio',\n variant: 'destructive',\n });\n }\n setIsTestingOutput(false);\n }\n }, [selectedOutputDevice, showToasts]);\n\n // Listen for audio test level events from Tauri backend\n useTauriEvent(\n TauriEvents.AUDIO_TEST_LEVEL,\n useCallback(\n (event: AudioTestLevelEvent) => {\n if (isTestingInput) {\n setInputLevel(event.level);\n }\n },\n [isTestingInput]\n ),\n [isTestingInput]\n );\n\n // Auto-load devices on mount if requested\n useEffect(() => {\n if (autoLoad) {\n loadDevices();\n }\n }, [autoLoad, loadDevices]);\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n void stopInputTest();\n };\n }, [stopInputTest]);\n\n return {\n // Device lists\n inputDevices,\n outputDevices,\n\n // Selected devices\n selectedInputDevice,\n selectedOutputDevice,\n\n // State\n isLoading,\n hasPermission,\n\n // Actions\n loadDevices,\n setInputDevice,\n setOutputDevice,\n\n // Testing\n isTestingInput,\n isTestingOutput,\n inputLevel,\n startInputTest,\n stopInputTest,\n testOutputDevice,\n };\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-calendar-sync.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-cloud-consent.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-cloud-consent.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-diarization.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-diarization.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-entity-extraction.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-guarded-mutation.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-guarded-mutation.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-integration-sync.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-integration-sync.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-integration-validation.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-meeting-reminders.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-mobile.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-oauth-flow.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-oauth-flow.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an `any` value.","line":175,"column":17,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":175,"endColumn":65},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `any` typed value.","line":176,"column":17,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":176,"endColumn":27},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .open on an `any` value.","line":176,"column":23,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":176,"endColumn":27}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// OAuth flow state management hook for calendar integrations\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { isIntegrationNotFoundError } from '@/api/helpers';\nimport { getAPI } from '@/api/interface';\nimport { isTauriEnvironment } from '@/api/tauri-adapter';\nimport type { OAuthConnection } from '@/api/types';\nimport { toast } from '@/hooks/use-toast';\n\nexport type OAuthFlowStatus =\n | 'idle'\n | 'initiating'\n | 'awaiting_callback'\n | 'completing'\n | 'connected'\n | 'error';\n\nexport interface OAuthFlowState {\n status: OAuthFlowStatus;\n provider: string | null;\n authUrl: string | null;\n error: string | null;\n connection: OAuthConnection | null;\n}\n\ninterface UseOAuthFlowReturn {\n state: OAuthFlowState;\n initiateAuth: (provider: string, redirectUri?: string) => Promise<void>;\n completeAuth: (provider: string, code: string, state: string) => Promise<boolean>;\n checkConnection: (provider: string) => Promise<OAuthConnection | null>;\n disconnect: (provider: string) => Promise<boolean>;\n reset: () => void;\n}\n\nconst initialState: OAuthFlowState = {\n status: 'idle',\n provider: null,\n authUrl: null,\n error: null,\n connection: null,\n};\n\n/** Parse OAuth callback URL to extract code and state. */\nfunction parseOAuthCallback(url: string): { code: string; state: string } | null {\n if (!url.startsWith('noteflow://oauth/callback')) {\n return null;\n }\n try {\n const parsed = new URL(url);\n const code = parsed.searchParams.get('code');\n const oauthState = parsed.searchParams.get('state');\n if (code && oauthState) {\n return { code, state: oauthState };\n }\n } catch {\n // Invalid URL\n }\n return null;\n}\n\nexport function useOAuthFlow(): UseOAuthFlowReturn {\n const [state, setState] = useState<OAuthFlowState>(initialState);\n const pendingStateRef = useRef<string | null>(null);\n const stateRef = useRef<OAuthFlowState>(initialState);\n stateRef.current = state;\n\n // Listen for OAuth callback via deep link (Tauri v2)\n useEffect(() => {\n if (!isTauriEnvironment()) {\n return;\n }\n\n let cleanup: (() => void) | undefined;\n\n const setupDeepLinkListener = async () => {\n try {\n // Dynamic import to avoid bundling issues in browser\n // Type assertion needed for dynamic module import\n type DeepLinkModule = { onOpenUrl: (cb: (urls: string[]) => void) => Promise<() => void> };\n const deepLink = (await import('@tauri-apps/plugin-deep-link')) as DeepLinkModule;\n cleanup = await deepLink.onOpenUrl((urls: string[]) => {\n void handleDeepLinkCallback(urls);\n });\n } catch {\n // Deep link plugin not available - OAuth callback won't be handled automatically\n }\n };\n\n const handleDeepLinkCallback = async (urls: string[]) => {\n const currentState = stateRef.current;\n for (const url of urls) {\n const params = parseOAuthCallback(url);\n if (params && currentState.status === 'awaiting_callback' && currentState.provider) {\n const {provider} = currentState;\n // Validate state matches pending state (CSRF protection)\n if (pendingStateRef.current && params.state !== pendingStateRef.current) {\n toast({\n title: 'OAuth Error',\n description: 'State mismatch - possible CSRF attack',\n variant: 'destructive',\n });\n continue;\n }\n\n // Complete the OAuth flow\n const api = getAPI();\n setState((prev) => ({ ...prev, status: 'completing' }));\n\n try {\n const response = await api.completeCalendarAuth(provider, params.code, params.state);\n if (response.success) {\n const connectionStatus = await api.getOAuthConnectionStatus(provider);\n setState((prev) => ({\n ...prev,\n status: 'connected',\n connection: connectionStatus.connection,\n }));\n toast({\n title: 'Connected',\n description: `Successfully connected to ${provider} calendar`,\n });\n } else {\n throw new Error(response.error_message || 'OAuth completion failed');\n }\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : 'Failed to complete OAuth';\n setState((prev) => ({ ...prev, status: 'error', error: errorMessage }));\n toast({\n title: 'Connection Failed',\n description: errorMessage,\n variant: 'destructive',\n });\n } finally {\n pendingStateRef.current = null;\n }\n }\n }\n };\n\n void setupDeepLinkListener();\n\n return () => {\n if (cleanup) {\n cleanup();\n }\n };\n }, []);\n\n const initiateAuth = useCallback(async (provider: string, redirectUri?: string) => {\n setState((prev) => ({\n ...prev,\n status: 'initiating',\n provider,\n error: null,\n }));\n\n try {\n const api = getAPI();\n const response = await api.initiateCalendarAuth(provider, redirectUri);\n\n if (response.auth_url) {\n // Store state token for CSRF validation when callback arrives\n pendingStateRef.current = response.state;\n\n setState((prev) => ({\n ...prev,\n status: 'awaiting_callback',\n authUrl: response.auth_url,\n }));\n\n // Open auth URL in default browser\n if (isTauriEnvironment()) {\n // Use Tauri shell plugin to open in system browser\n const shell = await import('@tauri-apps/plugin-shell');\n await shell.open(response.auth_url);\n } else {\n window.open(response.auth_url, '_blank');\n }\n } else {\n throw new Error('No auth URL returned from server');\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Failed to initiate OAuth';\n setState((prev) => ({\n ...prev,\n status: 'error',\n error: errorMessage,\n }));\n toast({\n title: 'OAuth Error',\n description: errorMessage,\n variant: 'destructive',\n });\n }\n }, []);\n\n const completeAuth = useCallback(\n async (provider: string, code: string, oauthState: string): Promise<boolean> => {\n setState((prev) => ({\n ...prev,\n status: 'completing',\n error: null,\n }));\n\n try {\n const api = getAPI();\n const response = await api.completeCalendarAuth(provider, code, oauthState);\n\n if (response.success) {\n // Fetch connection status to get full details\n const connectionStatus = await api.getOAuthConnectionStatus(provider);\n setState((prev) => ({\n ...prev,\n status: 'connected',\n connection: connectionStatus.connection,\n }));\n toast({\n title: 'Connected',\n description: `Successfully connected to ${provider} calendar`,\n });\n return true;\n } else {\n throw new Error(response.error_message || 'OAuth completion failed');\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Failed to complete OAuth';\n setState((prev) => ({\n ...prev,\n status: 'error',\n error: errorMessage,\n }));\n toast({\n title: 'Connection Failed',\n description: errorMessage,\n variant: 'destructive',\n });\n return false;\n }\n },\n []\n );\n\n const checkConnection = useCallback(async (provider: string): Promise<OAuthConnection | null> => {\n try {\n const api = getAPI();\n const response = await api.getOAuthConnectionStatus(provider);\n const {connection} = response;\n\n setState((prev) => ({\n ...prev,\n connection,\n status: connection?.status === 'connected' ? 'connected' : 'idle',\n provider: connection ? provider : prev.provider,\n }));\n\n return connection;\n } catch (error) {\n // Reset to idle state if integration no longer exists\n if (isIntegrationNotFoundError(error)) {\n setState(initialState);\n }\n return null;\n }\n }, []);\n\n const disconnect = useCallback(async (provider: string): Promise<boolean> => {\n try {\n const api = getAPI();\n const response = await api.disconnectCalendar(provider);\n\n if (response.success) {\n setState(initialState);\n toast({\n title: 'Disconnected',\n description: `${provider} calendar has been disconnected`,\n });\n return true;\n }\n return false;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Failed to disconnect';\n toast({\n title: 'Disconnect Failed',\n description: errorMessage,\n variant: 'destructive',\n });\n return false;\n }\n }, []);\n\n const reset = useCallback(() => {\n setState(initialState);\n }, []);\n\n return {\n state,\n initiateAuth,\n completeAuth,\n checkConnection,\n disconnect,\n reset,\n };\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-panel-preferences.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-panel-preferences.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-preferences-sync.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-project-members.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-project.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-secure-integration-secrets.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-toast.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-toast.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/hooks/use-webhooks.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/ai-models.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/ai-providers.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/cache/meeting-cache.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/cache/meeting-cache.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/app-config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/config.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/defaults.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/provider-endpoints.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/config/server.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/crypto.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/crypto.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/cva.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/cva.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/default-integrations.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/entity-store.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/entity-store.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/format.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/format.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/integration-utils.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/integration-utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/object-utils.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/object-utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/preferences-sync.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/preferences-sync.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/preferences-validation.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/preferences.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/preferences.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/speaker-utils.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/speaker-utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/status-constants.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/styles.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/tauri-events.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/tauri-events.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/timing-constants.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/utils.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/lib/utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/main.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Analytics.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Home.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Index.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/MeetingDetail.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/MeetingDetailRedirect.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Meetings.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/MeetingsRedirect.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/NotFound.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/People.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/ProjectSettings.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Projects.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Recording.logic.test.tsx","messages":[{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type `any`.","line":101,"column":34,"nodeType":"CallExpression","messageId":"unsafeReturn","endLine":101,"endColumn":48}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport type { TranscriptUpdate } from '@/api/types';\n\nlet isTauri = false;\nlet simulateTranscription = false;\nlet isConnected = true;\nlet params: { id?: string } = { id: 'new' };\n\nconst navigate = vi.fn();\nconst guard = vi.fn(async (fn: () => Promise<void>) => fn());\n\nconst apiInstance = {\n createMeeting: vi.fn(),\n getMeeting: vi.fn(),\n startTranscription: vi.fn(),\n stopMeeting: vi.fn(),\n};\n\nconst mockApiInstance = {\n createMeeting: vi.fn(),\n startTranscription: vi.fn(),\n stopMeeting: vi.fn(),\n};\n\nconst stream = {\n onUpdate: vi.fn(),\n close: vi.fn(),\n};\n\nconst mockStreamOnUpdate = vi.fn();\nconst mockStreamClose = vi.fn();\n\nlet panelPrefs = {\n showNotesPanel: true,\n showStatsPanel: true,\n notesPanelSize: 25,\n statsPanelSize: 25,\n transcriptPanelSize: 50,\n};\n\nconst setShowNotesPanel = vi.fn();\nconst setShowStatsPanel = vi.fn();\nconst setNotesPanelSize = vi.fn();\nconst setStatsPanelSize = vi.fn();\nconst setTranscriptPanelSize = vi.fn();\n\nconst tauriHandlers: Record<string, (payload: unknown) => void> = {};\n\nvi.mock('react-router-dom', async () => {\n const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');\n return {\n ...actual,\n useNavigate: () => navigate,\n useParams: () => params,\n };\n});\n\nvi.mock('@/api', () => ({\n getAPI: () => apiInstance,\n mockAPI: mockApiInstance,\n isTauriEnvironment: () => isTauri,\n}));\n\nvi.mock('@/api/mock-transcription-stream', () => ({\n MockTranscriptionStream: class MockTranscriptionStream {\n meetingId: string;\n constructor(meetingId: string) {\n this.meetingId = meetingId;\n }\n onUpdate = mockStreamOnUpdate;\n close = mockStreamClose;\n },\n}));\n\nvi.mock('@/contexts/connection-context', () => ({\n useConnectionState: () => ({ isConnected }),\n}));\n\nvi.mock('@/contexts/project-context', () => ({\n useProjects: () => ({ activeProject: { id: 'p1' } }),\n}));\n\nvi.mock('@/hooks/use-panel-preferences', () => ({\n usePanelPreferences: () => ({\n ...panelPrefs,\n setShowNotesPanel,\n setShowStatsPanel,\n setNotesPanelSize,\n setStatsPanelSize,\n setTranscriptPanelSize,\n }),\n}));\n\nvi.mock('@/hooks/use-guarded-mutation', () => ({\n useGuardedMutation: () => ({ guard }),\n}));\n\nconst toast = vi.fn();\nvi.mock('@/hooks/use-toast', () => ({\n toast: (...args: unknown[]) => toast(...args),\n}));\n\nvi.mock('@/lib/preferences', () => ({\n preferences: {\n get: () => ({ simulate_transcription: simulateTranscription }),\n },\n}));\n\nvi.mock('@/lib/tauri-events', () => ({\n useTauriEvent: (_event: string, handler: (payload: unknown) => void) => {\n tauriHandlers[_event] = handler;\n },\n}));\n\nvi.mock('framer-motion', () => ({\n AnimatePresence: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n}));\n\nvi.mock('@/components/recording', () => ({\n RecordingHeader: ({\n recordingState,\n meetingTitle,\n setMeetingTitle,\n onStartRecording,\n onStopRecording,\n elapsedTime,\n }: {\n recordingState: string;\n meetingTitle: string;\n setMeetingTitle: (title: string) => void;\n onStartRecording: () => void;\n onStopRecording: () => void;\n elapsedTime: number;\n }) => (\n <div>\n <div data-testid=\"recording-state\">{recordingState}</div>\n <div data-testid=\"meeting-title\">{meetingTitle}</div>\n <div data-testid=\"elapsed-time\">{elapsedTime}</div>\n <button type=\"button\" onClick={() => setMeetingTitle('Updated')}>Set Title</button>\n <button type=\"button\" onClick={onStartRecording}>Start Recording</button>\n <button type=\"button\" onClick={onStopRecording}>Stop Recording</button>\n </div>\n ),\n IdleState: () => <div>Idle</div>,\n ListeningState: () => <div>Listening</div>,\n PartialTextDisplay: ({ text, onTogglePin }: { text: string; onTogglePin: (id: string) => void }) => (\n <div>\n <div data-testid=\"partial-text\">{text}</div>\n <button type=\"button\" onClick={() => onTogglePin('entity-1')}>Toggle Pin</button>\n </div>\n ),\n TranscriptSegmentCard: ({\n segment,\n onTogglePin,\n }: {\n segment: { text: string };\n onTogglePin: (id: string) => void;\n }) => (\n <div>\n <div data-testid=\"segment-text\">{segment.text}</div>\n <button type=\"button\" onClick={() => onTogglePin('entity-2')}>Toggle Pin 2</button>\n </div>\n ),\n StatsContent: ({ isRecording, audioLevel }: { isRecording: boolean; audioLevel: number }) => (\n <div data-testid=\"stats\">{isRecording ? 'recording' : 'idle'}:{audioLevel}</div>\n ),\n VADIndicator: ({ isActive }: { isActive: boolean }) => (\n <div data-testid=\"vad\">{isActive ? 'on' : 'off'}</div>\n ),\n}));\n\nvi.mock('@/components/timestamped-notes-editor', () => ({\n TimestampedNotesEditor: () => <div data-testid=\"notes-editor\" />,\n}));\n\nvi.mock('@/components/ui/resizable', () => ({\n ResizablePanelGroup: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n ResizablePanel: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,\n ResizableHandle: () => <div data-testid=\"resize-handle\" />,\n}));\n\nconst buildMeeting = (id: string, state: string = 'created', title = 'Meeting') => ({\n id,\n project_id: 'p1',\n title,\n state,\n created_at: Date.now() / 1000,\n duration_seconds: 0,\n segments: [],\n metadata: {},\n});\n\ndescribe('RecordingPage logic', () => {\n beforeEach(() => {\n isTauri = false;\n simulateTranscription = false;\n isConnected = true;\n params = { id: 'new' };\n panelPrefs = {\n showNotesPanel: true,\n showStatsPanel: true,\n notesPanelSize: 25,\n statsPanelSize: 25,\n transcriptPanelSize: 50,\n };\n\n apiInstance.createMeeting.mockReset();\n apiInstance.getMeeting.mockReset();\n apiInstance.startTranscription.mockReset();\n apiInstance.stopMeeting.mockReset();\n mockApiInstance.createMeeting.mockReset();\n mockApiInstance.startTranscription.mockReset();\n mockApiInstance.stopMeeting.mockReset();\n stream.onUpdate.mockReset();\n stream.close.mockReset();\n mockStreamOnUpdate.mockReset();\n mockStreamClose.mockReset();\n guard.mockClear();\n navigate.mockClear();\n toast.mockClear();\n });\n\n afterEach(() => {\n Object.keys(tauriHandlers).forEach((key) => {\n delete tauriHandlers[key];\n });\n });\n\n it('shows desktop-only message when not running in tauri without simulation', async () => {\n isTauri = false;\n simulateTranscription = false;\n\n const { default: RecordingPage } = await import('./Recording');\n render(<RecordingPage />);\n\n expect(screen.getByText('Desktop recording only')).toBeInTheDocument();\n });\n\n it('starts and stops recording via guard', async () => {\n isTauri = true;\n simulateTranscription = false;\n isConnected = true;\n\n apiInstance.createMeeting.mockResolvedValue(buildMeeting('m1'));\n apiInstance.startTranscription.mockResolvedValue(stream);\n apiInstance.stopMeeting.mockResolvedValue(buildMeeting('m1', 'stopped'));\n\n const { default: RecordingPage } = await import('./Recording');\n render(<RecordingPage />);\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Start Recording' }));\n });\n\n expect(guard).toHaveBeenCalled();\n expect(apiInstance.createMeeting).toHaveBeenCalled();\n await waitFor(() => expect(apiInstance.startTranscription).toHaveBeenCalledWith('m1'));\n await waitFor(() => expect(stream.onUpdate).toHaveBeenCalled());\n\n const updateCallback = stream.onUpdate.mock.calls[0]?.[0] as (update: TranscriptUpdate) => void;\n await act(async () => {\n updateCallback({\n meeting_id: 'm1',\n update_type: 'partial',\n partial_text: 'Hello',\n server_timestamp: 1,\n });\n });\n await waitFor(() => expect(screen.getByTestId('partial-text')).toHaveTextContent('Hello'));\n\n await act(async () => {\n updateCallback({\n meeting_id: 'm1',\n update_type: 'final',\n segment: {\n segment_id: 1,\n text: 'Final',\n start_time: 0,\n end_time: 1,\n words: [],\n language: 'en',\n language_confidence: 1,\n avg_logprob: -0.1,\n no_speech_prob: 0,\n speaker_id: 'SPEAKER_00',\n speaker_confidence: 0.9,\n },\n server_timestamp: 2,\n });\n });\n await waitFor(() => expect(screen.getByTestId('segment-text')).toHaveTextContent('Final'));\n\n await act(async () => {\n updateCallback({ meeting_id: 'm1', update_type: 'vad_start', server_timestamp: 3 });\n });\n await waitFor(() => expect(screen.getByTestId('vad')).toHaveTextContent('on'));\n await act(async () => {\n updateCallback({ meeting_id: 'm1', update_type: 'vad_end', server_timestamp: 4 });\n });\n await waitFor(() => expect(screen.getByTestId('vad')).toHaveTextContent('off'));\n\n await act(async () => {\n tauriHandlers.recording_timer?.({ meeting_id: 'm1', elapsed_seconds: 12 });\n });\n await waitFor(() => expect(screen.getByTestId('elapsed-time')).toHaveTextContent('12'));\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Stop Recording' }));\n });\n\n expect(stream.close).toHaveBeenCalled();\n expect(apiInstance.stopMeeting).toHaveBeenCalledWith('m1');\n expect(navigate).toHaveBeenCalledWith('/projects/p1/meetings/m1');\n });\n\n it('uses mock API when simulating offline', async () => {\n isTauri = false;\n simulateTranscription = true;\n isConnected = false;\n\n mockApiInstance.createMeeting.mockResolvedValue(buildMeeting('m2'));\n mockApiInstance.startTranscription.mockResolvedValue(stream);\n\n const { default: RecordingPage } = await import('./Recording');\n render(<RecordingPage />);\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Start Recording' }));\n });\n\n expect(mockApiInstance.createMeeting).toHaveBeenCalled();\n expect(apiInstance.createMeeting).not.toHaveBeenCalled();\n });\n\n it('uses mock transcription stream when simulating while connected', async () => {\n isTauri = true;\n simulateTranscription = true;\n isConnected = true;\n\n apiInstance.createMeeting.mockResolvedValue(buildMeeting('m3'));\n\n const { default: RecordingPage } = await import('./Recording');\n render(<RecordingPage />);\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Start Recording' }));\n });\n\n expect(apiInstance.createMeeting).toHaveBeenCalled();\n expect(apiInstance.startTranscription).not.toHaveBeenCalled();\n await waitFor(() => expect(mockStreamOnUpdate).toHaveBeenCalled());\n });\n\n it('auto-starts existing meeting and respects terminal state', async () => {\n isTauri = true;\n simulateTranscription = false;\n isConnected = true;\n params = { id: 'm4' };\n\n apiInstance.getMeeting.mockResolvedValue(buildMeeting('m4', 'completed', 'Existing'));\n\n const { default: RecordingPage } = await import('./Recording');\n render(<RecordingPage />);\n\n await waitFor(() => expect(apiInstance.getMeeting).toHaveBeenCalled());\n await waitFor(() => expect(apiInstance.startTranscription).not.toHaveBeenCalled());\n await waitFor(() =>\n expect(screen.getByTestId('recording-state')).toHaveTextContent('idle')\n );\n });\n\n it('auto-starts existing meeting when state allows', async () => {\n isTauri = true;\n simulateTranscription = false;\n isConnected = true;\n params = { id: 'm5' };\n\n apiInstance.getMeeting.mockResolvedValue(buildMeeting('m5', 'created', 'Existing'));\n apiInstance.startTranscription.mockResolvedValue(stream);\n\n const { default: RecordingPage } = await import('./Recording');\n render(<RecordingPage />);\n\n await waitFor(() => expect(apiInstance.startTranscription).toHaveBeenCalledWith('m5'));\n await waitFor(() => expect(screen.getByTestId('meeting-title')).toHaveTextContent('Existing'));\n });\n\n it('renders collapsed panels when hidden', async () => {\n isTauri = true;\n simulateTranscription = true;\n isConnected = false;\n panelPrefs.showNotesPanel = false;\n panelPrefs.showStatsPanel = false;\n\n mockApiInstance.createMeeting.mockResolvedValue(buildMeeting('m6'));\n mockApiInstance.startTranscription.mockResolvedValue(stream);\n\n const { default: RecordingPage } = await import('./Recording');\n render(<RecordingPage />);\n\n await act(async () => {\n fireEvent.click(screen.getByRole('button', { name: 'Start Recording' }));\n });\n\n await act(async () => {\n fireEvent.click(screen.getByTitle('Expand notes panel'));\n fireEvent.click(screen.getByTitle('Expand stats panel'));\n });\n\n expect(setShowNotesPanel).toHaveBeenCalledWith(true);\n expect(setShowStatsPanel).toHaveBeenCalledWith(true);\n });\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Recording.test.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Recording.tsx","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":195,"column":11,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":195,"endColumn":63},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":1,"message":"Unsafe assignment of an error typed value.","line":248,"column":11,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":248,"endColumn":68}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Live Recording Page\n\nimport { AnimatePresence } from 'framer-motion';\nimport { BarChart3, PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen } from 'lucide-react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useNavigate, useParams } from 'react-router-dom';\nimport { getAPI, isTauriEnvironment, mockAPI, type TranscriptionStream } from '@/api';\nimport type { FinalSegment, Meeting, TranscriptUpdate } from '@/api/types';\nimport {\n IdleState,\n ListeningState,\n PartialTextDisplay,\n RecordingHeader,\n StatsContent,\n TranscriptSegmentCard,\n VADIndicator,\n} from '@/components/recording';\nimport { type NoteEdit, TimestampedNotesEditor } from '@/components/timestamped-notes-editor';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent } from '@/components/ui/card';\nimport { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';\nimport { useConnectionState } from '@/contexts/connection-context';\nimport { useProjects } from '@/contexts/project-context';\nimport { usePanelPreferences } from '@/hooks/use-panel-preferences';\nimport { useGuardedMutation } from '@/hooks/use-guarded-mutation';\nimport { toast } from '@/hooks/use-toast';\nimport { preferences } from '@/lib/preferences';\nimport { useTauriEvent } from '@/lib/tauri-events';\n\ntype RecordingState = 'idle' | 'starting' | 'recording' | 'paused' | 'stopping';\n\nexport default function RecordingPage() {\n const navigate = useNavigate();\n const { id } = useParams<{ id: string }>();\n const isNewRecording = !id || id === 'new';\n const { activeProject } = useProjects();\n\n // Recording state\n const [recordingState, setRecordingState] = useState<RecordingState>('idle');\n const [meeting, setMeeting] = useState<Meeting | null>(null);\n const [meetingTitle, setMeetingTitle] = useState('');\n\n // Transcription state\n const [segments, setSegments] = useState<FinalSegment[]>([]);\n const [partialText, setPartialText] = useState('');\n const [isVadActive, setIsVadActive] = useState(false);\n const [audioLevel, setAudioLevel] = useState<number | null>(null);\n\n // Notes state\n const [notes, setNotes] = useState<NoteEdit[]>([]);\n\n // Panel preferences (persisted to localStorage)\n const {\n showNotesPanel,\n showStatsPanel,\n notesPanelSize,\n statsPanelSize,\n transcriptPanelSize,\n setShowNotesPanel,\n setShowStatsPanel,\n setNotesPanelSize,\n setStatsPanelSize,\n setTranscriptPanelSize,\n } = usePanelPreferences();\n\n // Entity highlighting state\n const [pinnedEntities, setPinnedEntities] = useState<Set<string>>(new Set());\n\n const handleTogglePinEntity = (entityId: string) => {\n setPinnedEntities((prev) => {\n const next = new Set(prev);\n if (next.has(entityId)) {\n next.delete(entityId);\n } else {\n next.add(entityId);\n }\n return next;\n });\n };\n\n // Timer\n const [elapsedTime, setElapsedTime] = useState(0);\n const [hasTauriTimer, setHasTauriTimer] = useState(false);\n const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);\n const isTauri = isTauriEnvironment();\n const { isConnected } = useConnectionState();\n const { guard } = useGuardedMutation();\n const simulateTranscription = preferences.get().simulate_transcription;\n\n // Transcription stream\n const streamRef = useRef<TranscriptionStream | null>(null);\n const transcriptEndRef = useRef<HTMLDivElement>(null);\n\n // Auto-scroll to bottom\n useEffect(() => {\n transcriptEndRef.current?.scrollIntoView({ behavior: 'smooth' });\n }, []);\n\n // Timer effect\n useEffect(() => {\n if (recordingState === 'idle') {\n setHasTauriTimer(false);\n }\n const clearTimer = () => {\n if (timerRef.current) {\n clearInterval(timerRef.current);\n timerRef.current = null;\n }\n };\n if (isTauri && hasTauriTimer) {\n clearTimer();\n return;\n }\n if (recordingState === 'recording') {\n timerRef.current = setInterval(() => setElapsedTime((prev) => prev + 1), 1000);\n } else {\n clearTimer();\n }\n return clearTimer;\n }, [recordingState, hasTauriTimer, isTauri]);\n\n useEffect(() => {\n if (recordingState !== 'recording') {\n setAudioLevel(null);\n }\n }, [recordingState]);\n\n useTauriEvent(\n 'audio_level',\n (payload) => {\n if (payload.meeting_id !== meeting?.id) {\n return;\n }\n setAudioLevel(payload.level);\n },\n [meeting?.id]\n );\n\n useTauriEvent(\n 'recording_timer',\n (payload) => {\n if (payload.meeting_id !== meeting?.id) {\n return;\n }\n setHasTauriTimer(true);\n setElapsedTime(payload.elapsed_seconds);\n },\n [meeting?.id]\n );\n\n // Handle transcript updates\n // Toast helpers\n const toastSuccess = useCallback(\n (title: string, description: string) => toast({ title, description }),\n []\n );\n const toastError = useCallback(\n (title: string) => toast({ title, description: 'Please try again', variant: 'destructive' }),\n []\n );\n\n const handleTranscriptUpdate = useCallback((update: TranscriptUpdate) => {\n if (update.update_type === 'partial') {\n setPartialText(update.partial_text || '');\n } else if (update.update_type === 'final' && update.segment) {\n const seg = update.segment;\n setSegments((prev) => [...prev, seg]);\n setPartialText('');\n } else if (update.update_type === 'vad_start') {\n setIsVadActive(true);\n } else if (update.update_type === 'vad_end') {\n setIsVadActive(false);\n }\n }, []);\n\n // Start recording\n const startRecording = async () => {\n const shouldSimulate = preferences.get().simulate_transcription;\n const runStart = async () => {\n setRecordingState('starting');\n\n try {\n const api = shouldSimulate && !isConnected ? mockAPI : getAPI();\n const newMeeting = await api.createMeeting({\n title: meetingTitle || `Recording ${new Date().toLocaleString()}`,\n project_id: activeProject?.id,\n });\n setMeeting(newMeeting);\n\n let stream: TranscriptionStream;\n if (shouldSimulate && isConnected) {\n const { MockTranscriptionStream } = await import('@/api/mock-transcription-stream');\n stream = new MockTranscriptionStream(newMeeting.id);\n } else {\n stream = await api.startTranscription(newMeeting.id);\n }\n\n streamRef.current = stream;\n stream.onUpdate(handleTranscriptUpdate);\n\n setRecordingState('recording');\n toastSuccess(\n 'Recording started',\n shouldSimulate ? 'Simulation is active' : 'Transcription is now active'\n );\n } catch (_error) {\n setRecordingState('idle');\n toastError('Failed to start recording');\n }\n };\n\n if (shouldSimulate) {\n await runStart();\n } else {\n await guard(runStart, {\n title: 'Offline mode',\n message: 'Recording requires an active server connection.',\n });\n }\n };\n\n // Auto-start recording for existing meeting (trigger accept flow)\n useEffect(() => {\n if (!isTauri || isNewRecording || !id || recordingState !== 'idle') {\n return;\n }\n const startExistingRecording = async () => {\n const shouldSimulate = preferences.get().simulate_transcription;\n setRecordingState('starting');\n try {\n const api = shouldSimulate && !isConnected ? mockAPI : getAPI();\n const existingMeeting = await api.getMeeting({\n meeting_id: id,\n include_segments: false,\n include_summary: false,\n });\n setMeeting(existingMeeting);\n setMeetingTitle(existingMeeting.title);\n if (!['created', 'recording'].includes(existingMeeting.state)) {\n setRecordingState('idle');\n return;\n }\n let stream: TranscriptionStream;\n if (shouldSimulate && isConnected) {\n const { MockTranscriptionStream } = await import('@/api/mock-transcription-stream');\n stream = new MockTranscriptionStream(existingMeeting.id);\n } else {\n stream = await api.startTranscription(existingMeeting.id);\n }\n streamRef.current = stream;\n stream.onUpdate(handleTranscriptUpdate);\n setRecordingState('recording');\n toastSuccess(\n 'Recording started',\n shouldSimulate ? 'Simulation is active' : 'Transcription is now active'\n );\n } catch (_error) {\n setRecordingState('idle');\n toastError('Failed to start recording');\n }\n };\n void startExistingRecording();\n }, [\n handleTranscriptUpdate,\n id,\n isNewRecording,\n isTauri,\n isConnected,\n recordingState,\n toastError,\n toastSuccess,\n ]);\n\n // Stop recording\n const stopRecording = async () => {\n if (!meeting) {\n return;\n }\n const shouldSimulate = preferences.get().simulate_transcription;\n const runStop = async () => {\n setRecordingState('stopping');\n try {\n streamRef.current?.close();\n streamRef.current = null;\n const api = shouldSimulate && !isConnected ? mockAPI : getAPI();\n const stoppedMeeting = await api.stopMeeting(meeting.id);\n setMeeting(stoppedMeeting);\n toastSuccess(\n 'Recording stopped',\n shouldSimulate ? 'Simulation finished' : 'Your meeting has been saved'\n );\n const projectId = meeting.project_id ?? activeProject?.id;\n navigate(projectId ? `/projects/${projectId}/meetings/${meeting.id}` : `/meetings/${meeting.id}`);\n } catch (_error) {\n setRecordingState('recording');\n toastError('Failed to stop recording');\n }\n };\n\n if (shouldSimulate) {\n await runStop();\n } else {\n await guard(runStop, {\n title: 'Offline mode',\n message: 'Stopping a recording requires an active server connection.',\n });\n }\n };\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n streamRef.current?.close();\n };\n }, []);\n\n if (!isTauri && !simulateTranscription) {\n return (\n <div className=\"h-full flex items-center justify-center p-6 bg-background\">\n <Card className=\"max-w-lg w-full\">\n <CardContent className=\"p-6 space-y-2\">\n <h2 className=\"text-lg font-semibold text-foreground\">Desktop recording only</h2>\n <p className=\"text-sm text-muted-foreground\">\n Recording and live transcription are available in the desktop app. Use the web app for\n administration, configuration, and reporting.\n </p>\n </CardContent>\n </Card>\n </div>\n );\n }\n\n return (\n <div className=\"h-full flex flex-col bg-background\">\n <RecordingHeader\n recordingState={recordingState}\n meeting={meeting}\n meetingTitle={meetingTitle}\n setMeetingTitle={setMeetingTitle}\n simulateTranscription={simulateTranscription}\n elapsedTime={elapsedTime}\n onStartRecording={startRecording}\n onStopRecording={stopRecording}\n />\n\n {/* Content */}\n <ResizablePanelGroup direction=\"horizontal\" className=\"flex-1\">\n {/* Transcript Panel */}\n <ResizablePanel\n defaultSize={transcriptPanelSize}\n minSize={30}\n onResize={setTranscriptPanelSize}\n >\n <div className=\"h-full overflow-auto p-6\">\n {recordingState === 'idle' ? (\n <IdleState />\n ) : (\n <div className=\"max-w-3xl mx-auto space-y-4\">\n {/* VAD Indicator */}\n <VADIndicator isActive={isVadActive} isRecording={recordingState === 'recording'} />\n\n {/* Transcript */}\n <div className=\"space-y-3\">\n <AnimatePresence mode=\"popLayout\">\n {segments.map((segment) => (\n <TranscriptSegmentCard\n key={segment.segment_id}\n segment={segment}\n meetingId={meeting?.id}\n pinnedEntities={pinnedEntities}\n onTogglePin={handleTogglePinEntity}\n />\n ))}\n </AnimatePresence>\n <PartialTextDisplay\n text={partialText}\n pinnedEntities={pinnedEntities}\n onTogglePin={handleTogglePinEntity}\n />\n <div ref={transcriptEndRef} />\n </div>\n\n {/* Empty State */}\n {segments.length === 0 && !partialText && recordingState === 'recording' && (\n <ListeningState />\n )}\n </div>\n )}\n </div>\n </ResizablePanel>\n\n {/* Notes Panel */}\n {recordingState !== 'idle' && showNotesPanel && (\n <>\n <ResizableHandle withHandle />\n <ResizablePanel\n defaultSize={notesPanelSize}\n minSize={15}\n maxSize={40}\n onResize={setNotesPanelSize}\n >\n <div className=\"h-full flex flex-col border-l border-border bg-card/50\">\n <div className=\"flex-1 flex flex-col p-4 min-h-0\">\n <div className=\"flex items-center justify-between mb-3\">\n <h3 className=\"font-medium text-foreground text-sm\">Notes</h3>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setShowNotesPanel(false)}\n className=\"h-7 w-7 p-0\"\n title=\"Collapse notes panel\"\n >\n <PanelRightClose className=\"h-4 w-4\" />\n </Button>\n </div>\n <div className=\"flex-1 min-h-0\">\n <TimestampedNotesEditor\n elapsedTime={elapsedTime}\n isRecording={recordingState === 'recording'}\n notes={notes}\n onNotesChange={setNotes}\n />\n </div>\n </div>\n </div>\n </ResizablePanel>\n </>\n )}\n\n {/* Collapsed Notes Panel */}\n {recordingState !== 'idle' && !showNotesPanel && (\n <div className=\"shrink-0 w-12 border-l border-border bg-card/50 flex flex-col items-center pt-4\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setShowNotesPanel(true)}\n className=\"h-8 w-8 p-0\"\n title=\"Expand notes panel\"\n >\n <PanelRightOpen className=\"h-4 w-4\" />\n </Button>\n <span className=\"text-[10px] text-muted-foreground mt-2 [writing-mode:vertical-rl] rotate-180\">\n Notes\n </span>\n </div>\n )}\n\n {/* Stats Panel */}\n {recordingState !== 'idle' && showStatsPanel && (\n <>\n <ResizableHandle withHandle />\n <ResizablePanel\n defaultSize={statsPanelSize}\n minSize={15}\n maxSize={35}\n onResize={setStatsPanelSize}\n >\n <div className=\"h-full flex flex-col border-l border-border bg-card/50 overflow-auto\">\n <div className=\"p-4 space-y-4\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"font-medium text-foreground\">Recording Stats</h3>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setShowStatsPanel(false)}\n className=\"h-7 w-7 p-0\"\n title=\"Collapse stats panel\"\n >\n <PanelLeftClose className=\"h-4 w-4\" />\n </Button>\n </div>\n <StatsContent\n elapsedTime={elapsedTime}\n segments={segments}\n meetingId={meeting?.id}\n isRecording={recordingState === 'recording'}\n isVadActive={isVadActive}\n audioLevel={audioLevel}\n />\n </div>\n </div>\n </ResizablePanel>\n </>\n )}\n\n {/* Collapsed Stats Panel */}\n {recordingState !== 'idle' && !showStatsPanel && (\n <div className=\"shrink-0 w-12 border-l border-border bg-card/50 flex flex-col items-center pt-4\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setShowStatsPanel(true)}\n className=\"h-8 w-8 p-0\"\n title=\"Expand stats panel\"\n >\n <PanelLeftOpen className=\"h-4 w-4\" />\n </Button>\n <span className=\"text-[10px] text-muted-foreground mt-2 [writing-mode:vertical-rl] rotate-180\">\n <BarChart3 className=\"h-3 w-3 mb-1\" />\n Stats\n </span>\n </div>\n )}\n </ResizablePanelGroup>\n </div>\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Settings.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/pages/Tasks.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/test/code-quality.test.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/test/mocks/tauri-plugin-shell.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/test/setup.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/test/vitest.d.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/types/entity.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/types/navigator.d.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/types/task.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/src/vite-env.d.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/tailwind.config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/vite.config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/vitest.config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/trav/repos/noteflow/client/wdio.conf.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type error.","line":101,"column":7,"nodeType":"ReturnStatement","messageId":"unsafeReturn","endLine":101,"endColumn":33},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":1,"message":"Unsafe argument of type error typed assigned to a parameter of type `PathLike`.","line":103,"column":45,"nodeType":"MemberExpression","messageId":"unsafeArgument","endLine":103,"endColumn":63},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":1,"message":"Unsafe return of a value of type error.","line":104,"column":7,"nodeType":"ReturnStatement","messageId":"unsafeReturn","endLine":104,"endColumn":33},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `any` typed value.","line":209,"column":37,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":209,"endColumn":57},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `any` typed value.","line":209,"column":37,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":209,"endColumn":50},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .toString on an `any` value.","line":209,"column":42,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":209,"endColumn":50},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .trim on an `any` value.","line":209,"column":53,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":209,"endColumn":57},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `any` typed value.","line":213,"column":39,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":213,"endColumn":59},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `any` typed value.","line":213,"column":39,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":213,"endColumn":52},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .toString on an `any` value.","line":213,"column":44,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":213,"endColumn":52},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .trim on an `any` value.","line":213,"column":55,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":213,"endColumn":59},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":1,"message":"Unsafe call of a(n) `error` type typed value.","line":266,"column":13,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":266,"endColumn":35},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":1,"message":"Unsafe member access .saveScreenshot on an `error` typed value.","line":266,"column":21,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":266,"endColumn":35}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":13,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * WebdriverIO Configuration for Native Tauri Testing\n *\n * This config runs tests against the actual Tauri desktop app using tauri-driver.\n * Requires: cargo install tauri-driver\n *\n * Usage:\n * 1. Build the app: npm run tauri:build\n * 2. Run tests: npm run test:native\n */\n\nimport type { Options } from '@wdio/types';\nimport * as path from 'node:path';\nimport * as fs from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { spawn, type ChildProcess } from 'node:child_process';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n// Track tauri-driver process\nlet tauriDriverProcess: ChildProcess | null = null;\n\n// Detect the built Tauri binary path based on platform\nfunction getTauriBinaryPath(): string {\n const projectRoot = path.resolve(__dirname, 'src-tauri');\n\n if (process.platform === 'win32') {\n // Windows: look for .exe in release or debug\n // Binary name comes from Cargo.toml package name\n const releasePath = path.join(projectRoot, 'target', 'release', 'noteflow-tauri.exe');\n const debugPath = path.join(projectRoot, 'target', 'debug', 'noteflow-tauri.exe');\n\n if (fs.existsSync(releasePath)) {\n return releasePath;\n }\n if (fs.existsSync(debugPath)) {\n return debugPath;\n }\n\n // Fallback to release path (will error if not built)\n return releasePath;\n } else if (process.platform === 'darwin') {\n // macOS: .app bundle\n const releasePath = path.join(\n projectRoot,\n 'target',\n 'release',\n 'bundle',\n 'macos',\n 'NoteFlow.app',\n 'Contents',\n 'MacOS',\n 'noteflow-tauri'\n );\n const debugPath = path.join(projectRoot, 'target', 'debug', 'noteflow-tauri');\n\n if (fs.existsSync(releasePath)) {\n return releasePath;\n }\n if (fs.existsSync(debugPath)) {\n return debugPath;\n }\n return releasePath;\n } else {\n // Linux: AppImage or direct binary\n const releasePath = path.join(projectRoot, 'target', 'release', 'noteflow-tauri');\n const debugPath = path.join(projectRoot, 'target', 'debug', 'noteflow-tauri');\n\n if (fs.existsSync(releasePath)) {\n return releasePath;\n }\n if (fs.existsSync(debugPath)) {\n return debugPath;\n }\n return releasePath;\n }\n}\n\n// Get tauri-driver path\nfunction getTauriDriverPath(): string {\n if (process.platform === 'win32') {\n // On Windows, tauri-driver is in cargo bin\n const cargoHome = process.env.CARGO_HOME || path.join(process.env.USERPROFILE || '', '.cargo');\n return path.join(cargoHome, 'bin', 'tauri-driver.exe');\n }\n return 'tauri-driver';\n}\n\n// Get msedgedriver path (Windows only)\nasync function getMsEdgeDriverPath(): Promise<string | null> {\n if (process.platform !== 'win32') {\n return null;\n }\n\n // Try edgedriver npm package first\n try {\n const edgedriver = await import('edgedriver');\n // The package provides a function to get/download the binary\n if (typeof edgedriver.default === 'string') {\n return edgedriver.default;\n }\n if (edgedriver.binPath && fs.existsSync(edgedriver.binPath)) {\n return edgedriver.binPath;\n }\n } catch {\n // Package not available or failed\n }\n\n // Check common locations\n const possiblePaths = [\n // Custom env var\n process.env.MSEDGEDRIVER_PATH,\n // Common install locations\n 'C:\\\\Program Files\\\\Microsoft\\\\Edge\\\\msedgedriver.exe',\n 'C:\\\\Program Files (x86)\\\\Microsoft\\\\Edge\\\\msedgedriver.exe',\n path.join(process.env.USERPROFILE || '', 'msedgedriver.exe'),\n path.join(process.env.USERPROFILE || '', '.webdrivers', 'msedgedriver.exe'),\n ];\n\n for (const p of possiblePaths) {\n if (p && fs.existsSync(p)) {\n return p;\n }\n }\n\n return null;\n}\n\nexport const config: Options.Testrunner = {\n // Test specs\n specs: ['./e2e-native/**/*.spec.ts'],\n exclude: [],\n\n // Capabilities\n maxInstances: 1, // Tauri apps should run one at a time\n capabilities: [\n {\n // Use tauri-driver as the WebDriver server\n 'tauri:options': {\n application: getTauriBinaryPath(),\n },\n },\n ],\n\n // Test framework\n framework: 'mocha',\n mochaOpts: {\n ui: 'bdd',\n timeout: 60000,\n },\n\n // Reporters\n reporters: ['spec'],\n\n // Log level\n logLevel: 'info',\n\n // Connection settings for tauri-driver\n hostname: '127.0.0.1',\n port: 4444,\n\n // No built-in service - tauri-driver started via onPrepare hook\n services: [],\n\n // Timeouts\n connectionRetryTimeout: 120000,\n connectionRetryCount: 3,\n\n // Hooks\n onPrepare: async () => {\n const driverPath = getTauriDriverPath();\n console.log(`Starting tauri-driver: ${driverPath}`);\n\n // Check if tauri-driver exists\n if (!fs.existsSync(driverPath)) {\n throw new Error(\n `tauri-driver not found at: ${driverPath}\\n` +\n 'Install it with: cargo install tauri-driver'\n );\n }\n\n // On Windows, check for msedgedriver\n const edgeDriverPath = await getMsEdgeDriverPath();\n if (process.platform === 'win32' && !edgeDriverPath) {\n console.warn(\n '\\n⚠️ msedgedriver.exe not found in common locations.\\n' +\n ' Download from: https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/\\n' +\n ' Then either:\\n' +\n ' - Add to PATH\\n' +\n ' - Set MSEDGEDRIVER_PATH environment variable\\n' +\n ' - Place in your home directory\\n'\n );\n }\n\n // Build args\n const args = ['--port', '4444'];\n if (edgeDriverPath) {\n args.push('--native-driver', edgeDriverPath);\n console.log(`Using msedgedriver: ${edgeDriverPath}`);\n }\n\n // Start tauri-driver\n tauriDriverProcess = spawn(driverPath, args, {\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n tauriDriverProcess.stdout?.on('data', (data) => {\n console.log(`[tauri-driver] ${data.toString().trim()}`);\n });\n\n tauriDriverProcess.stderr?.on('data', (data) => {\n console.error(`[tauri-driver] ${data.toString().trim()}`);\n });\n\n // Wait for tauri-driver to be ready\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error('tauri-driver failed to start within 10s'));\n }, 10000);\n\n const checkReady = async () => {\n try {\n const response = await fetch('http://127.0.0.1:4444/status');\n if (response.ok) {\n clearTimeout(timeout);\n console.log('tauri-driver is ready');\n resolve();\n }\n } catch {\n // Not ready yet, retry\n setTimeout(checkReady, 200);\n }\n };\n\n // Start checking after a brief delay\n setTimeout(checkReady, 500);\n });\n },\n\n onComplete: async () => {\n // Stop tauri-driver\n if (tauriDriverProcess) {\n console.log('Stopping tauri-driver');\n tauriDriverProcess.kill();\n tauriDriverProcess = null;\n }\n },\n\n beforeSession: async () => {\n const binaryPath = getTauriBinaryPath();\n if (!fs.existsSync(binaryPath)) {\n throw new Error(\n `Tauri binary not found at: ${binaryPath}\\n` +\n 'Please build the app first with: npm run tauri:build'\n );\n }\n console.log(`Using Tauri binary: ${binaryPath}`);\n },\n\n afterTest: async (test, _context, { error }) => {\n if (error) {\n // Take screenshot on failure\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n const screenshotPath = `./e2e-native/screenshots/${test.title}-${timestamp}.png`;\n await browser.saveScreenshot(screenshotPath);\n console.log(`Screenshot saved: ${screenshotPath}`);\n }\n },\n};\n","usedDeprecatedRules":[]}] |