diff --git a/ui/src/components/logs/logs-filters.tsx b/ui/src/components/logs/logs-filters.tsx index 4d00d830..7080a1a9 100644 --- a/ui/src/components/logs/logs-filters.tsx +++ b/ui/src/components/logs/logs-filters.tsx @@ -44,7 +44,7 @@ export interface LogsFiltersProps { onRequestIdChange?: (v: string) => void; timeWindow?: LogsTimeWindow; onTimeWindowChange?: (v: LogsTimeWindow) => void; - /** When true, hides entries from `web-server:*` sources. Default ON. */ + /** When true, hides entries from `web-server:*` sources. Default OFF. */ hideDashboardInternals?: boolean; onHideDashboardInternalsChange?: (next: boolean) => void; onClearAll?: () => void; @@ -91,7 +91,7 @@ export function LogsFilters({ onRequestIdChange, timeWindow = 'all', onTimeWindowChange, - hideDashboardInternals = true, + hideDashboardInternals = false, onHideDashboardInternalsChange, onClearAll, }: LogsFiltersProps) { @@ -324,11 +324,10 @@ export function LogsFilters({ htmlFor="logs-hide-internals" className="block text-[12px] font-medium text-foreground" > - Hide dashboard internals + Hide dashboard web-server logs

- Suppress web-server:*{' '} - self-polling. + Optional noise reduction. Audit entries are visible by default.

onHideDashboardInternalsChange(e.target.checked)} className={cn('mt-0.5 h-4 w-4 cursor-pointer accent-foreground', FOCUS_RING)} - aria-label="Hide dashboard internals" + aria-label="Hide dashboard web-server logs" /> ) : null} diff --git a/ui/src/hooks/use-logs.ts b/ui/src/hooks/use-logs.ts index 702db1f6..b27dc0ae 100644 --- a/ui/src/hooks/use-logs.ts +++ b/ui/src/hooks/use-logs.ts @@ -116,10 +116,10 @@ export function useLogsWorkspace() { const [limit, setLimit] = useState(DEFAULT_LIMIT); const [selectedEntryId, setSelectedEntryId] = useState(null); const [isPaused, setIsPaused] = useState(false); - // Default ON: dashboard self-polling generates 100s of identical entries - // per refresh which drown out real provider activity. Users can opt in to - // see internals via the advanced filter toggle. - const [hideDashboardInternals, setHideDashboardInternals] = useState(true); + // Default OFF: web-server:* entries include dashboard access and WebSocket + // audit evidence, so keep them visible unless the operator opts into noise + // reduction from the advanced filter toggle. + const [hideDashboardInternals, setHideDashboardInternals] = useState(false); const frozenIdsRef = useRef>(new Set()); const deferredSearch = useDeferredValue(search.trim()); @@ -196,8 +196,9 @@ export function useLogsWorkspace() { const ts = Date.parse(entry.timestamp); if (Number.isFinite(ts) && now - ts > cutoffMs) return false; } - // Hide dashboard self-polling unless user opted in. Preserves the - // signal-to-noise ratio for fresh-load investigations. + // Optional noise reduction only: web-server:* entries can contain + // security-relevant dashboard access and WebSocket audit evidence, so + // they remain visible by default. if (hideDashboardInternals && /^web-server:/i.test(entry.source)) return false; return true; }); @@ -273,7 +274,7 @@ export function useLogsWorkspace() { setStageFilter(''); setRequestIdFilter(''); setTimeWindow('all'); - setHideDashboardInternals(true); + setHideDashboardInternals(false); }, []); return { diff --git a/ui/tests/unit/hooks/use-logs.test.tsx b/ui/tests/unit/hooks/use-logs.test.tsx new file mode 100644 index 00000000..f34c6b7a --- /dev/null +++ b/ui/tests/unit/hooks/use-logs.test.tsx @@ -0,0 +1,120 @@ +import { act, renderHook, waitFor } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import type { ReactNode } from 'react'; +import { AllProviders } from '../../setup/test-utils'; +import { useLogsWorkspace } from '@/hooks/use-logs'; +import type { LogsEntry } from '@/lib/api-client'; + +function createJsonResponse(body: Record, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }); +} + +const entries: LogsEntry[] = [ + { + id: 'http-audit-1', + timestamp: '2026-05-29T00:00:00.000Z', + level: 'error', + source: 'web-server:http', + event: 'request.completed', + message: 'Dashboard request completed', + processId: 1, + runId: null, + requestId: 'dashboard-audit', + }, + { + id: 'ws-audit-1', + timestamp: '2026-05-29T00:00:01.000Z', + level: 'warn', + source: 'web-server:websocket', + event: 'message.invalid', + message: 'WebSocket client sent invalid JSON', + processId: 1, + runId: null, + requestId: 'dashboard-audit', + }, + { + id: 'provider-1', + timestamp: '2026-05-29T00:00:02.000Z', + level: 'info', + source: 'provider:codex', + event: 'request.completed', + message: 'Provider request completed', + processId: 2, + runId: null, + requestId: 'provider-trace', + }, +]; + +function mockLogsApi(): void { + vi.stubGlobal( + 'fetch', + vi.fn(async (input: RequestInfo | URL) => { + const url = String(input); + if (url.startsWith('/api/logs/config')) { + return createJsonResponse({ + logging: { + enabled: true, + level: 'info', + rotate_mb: 10, + retain_days: 7, + redact: true, + live_buffer_size: 150, + }, + }); + } + if (url.startsWith('/api/logs/sources')) { + return createJsonResponse({ + sources: entries.map((entry) => ({ + source: entry.source, + label: entry.source, + kind: 'native', + count: 1, + lastTimestamp: entry.timestamp, + })), + }); + } + if (url.startsWith('/api/logs/entries')) { + return createJsonResponse({ entries }); + } + return createJsonResponse({ error: `Unexpected URL: ${url}` }, 404); + }) + ); +} + +describe('useLogsWorkspace', () => { + it('keeps dashboard audit events visible by default and only hides them on opt-in', async () => { + mockLogsApi(); + + const wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useLogsWorkspace(), { wrapper }); + + await waitFor(() => { + expect(result.current.entriesQuery.data?.map((entry) => entry.id)).toEqual([ + 'http-audit-1', + 'ws-audit-1', + 'provider-1', + ]); + }); + + act(() => result.current.setHideDashboardInternals(true)); + + await waitFor(() => { + expect(result.current.entriesQuery.data?.map((entry) => entry.id)).toEqual(['provider-1']); + }); + + act(() => result.current.clearAdvancedFilters()); + + await waitFor(() => { + expect(result.current.entriesQuery.data?.map((entry) => entry.id)).toEqual([ + 'http-audit-1', + 'ws-audit-1', + 'provider-1', + ]); + }); + }); +});