mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 16:19:12 +00:00
Merge pull request #1424 from kaitranntt/codex/fix-logs-filtering-to-show-dashboard-audits
fix(ui): show dashboard audit logs by default
This commit is contained in:
@@ -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
|
||||
</Label>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Suppress <code className="rounded bg-background px-1">web-server:*</code>{' '}
|
||||
self-polling.
|
||||
Optional noise reduction. Audit entries are visible by default.
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
@@ -338,7 +337,7 @@ export function LogsFilters({
|
||||
checked={hideDashboardInternals}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -116,10 +116,10 @@ export function useLogsWorkspace() {
|
||||
const [limit, setLimit] = useState(DEFAULT_LIMIT);
|
||||
const [selectedEntryId, setSelectedEntryId] = useState<string | null>(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<Set<string>>(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 {
|
||||
|
||||
@@ -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<string, unknown>, 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 }) => (
|
||||
<AllProviders>{children}</AllProviders>
|
||||
);
|
||||
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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user