mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-06 02:22:45 +00:00
fix(ui): show dashboard audit logs by default
This commit is contained in:
@@ -44,7 +44,7 @@ export interface LogsFiltersProps {
|
|||||||
onRequestIdChange?: (v: string) => void;
|
onRequestIdChange?: (v: string) => void;
|
||||||
timeWindow?: LogsTimeWindow;
|
timeWindow?: LogsTimeWindow;
|
||||||
onTimeWindowChange?: (v: LogsTimeWindow) => void;
|
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;
|
hideDashboardInternals?: boolean;
|
||||||
onHideDashboardInternalsChange?: (next: boolean) => void;
|
onHideDashboardInternalsChange?: (next: boolean) => void;
|
||||||
onClearAll?: () => void;
|
onClearAll?: () => void;
|
||||||
@@ -91,7 +91,7 @@ export function LogsFilters({
|
|||||||
onRequestIdChange,
|
onRequestIdChange,
|
||||||
timeWindow = 'all',
|
timeWindow = 'all',
|
||||||
onTimeWindowChange,
|
onTimeWindowChange,
|
||||||
hideDashboardInternals = true,
|
hideDashboardInternals = false,
|
||||||
onHideDashboardInternalsChange,
|
onHideDashboardInternalsChange,
|
||||||
onClearAll,
|
onClearAll,
|
||||||
}: LogsFiltersProps) {
|
}: LogsFiltersProps) {
|
||||||
@@ -324,11 +324,10 @@ export function LogsFilters({
|
|||||||
htmlFor="logs-hide-internals"
|
htmlFor="logs-hide-internals"
|
||||||
className="block text-[12px] font-medium text-foreground"
|
className="block text-[12px] font-medium text-foreground"
|
||||||
>
|
>
|
||||||
Hide dashboard internals
|
Hide dashboard web-server logs
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-[11px] text-muted-foreground">
|
<p className="text-[11px] text-muted-foreground">
|
||||||
Suppress <code className="rounded bg-background px-1">web-server:*</code>{' '}
|
Optional noise reduction. Audit entries are visible by default.
|
||||||
self-polling.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@@ -338,7 +337,7 @@ export function LogsFilters({
|
|||||||
checked={hideDashboardInternals}
|
checked={hideDashboardInternals}
|
||||||
onChange={(e) => onHideDashboardInternalsChange(e.target.checked)}
|
onChange={(e) => onHideDashboardInternalsChange(e.target.checked)}
|
||||||
className={cn('mt-0.5 h-4 w-4 cursor-pointer accent-foreground', FOCUS_RING)}
|
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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -116,10 +116,10 @@ export function useLogsWorkspace() {
|
|||||||
const [limit, setLimit] = useState(DEFAULT_LIMIT);
|
const [limit, setLimit] = useState(DEFAULT_LIMIT);
|
||||||
const [selectedEntryId, setSelectedEntryId] = useState<string | null>(null);
|
const [selectedEntryId, setSelectedEntryId] = useState<string | null>(null);
|
||||||
const [isPaused, setIsPaused] = useState(false);
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
// Default ON: dashboard self-polling generates 100s of identical entries
|
// Default OFF: web-server:* entries include dashboard access and WebSocket
|
||||||
// per refresh which drown out real provider activity. Users can opt in to
|
// audit evidence, so keep them visible unless the operator opts into noise
|
||||||
// see internals via the advanced filter toggle.
|
// reduction from the advanced filter toggle.
|
||||||
const [hideDashboardInternals, setHideDashboardInternals] = useState(true);
|
const [hideDashboardInternals, setHideDashboardInternals] = useState(false);
|
||||||
const frozenIdsRef = useRef<Set<string>>(new Set());
|
const frozenIdsRef = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
const deferredSearch = useDeferredValue(search.trim());
|
const deferredSearch = useDeferredValue(search.trim());
|
||||||
@@ -196,8 +196,9 @@ export function useLogsWorkspace() {
|
|||||||
const ts = Date.parse(entry.timestamp);
|
const ts = Date.parse(entry.timestamp);
|
||||||
if (Number.isFinite(ts) && now - ts > cutoffMs) return false;
|
if (Number.isFinite(ts) && now - ts > cutoffMs) return false;
|
||||||
}
|
}
|
||||||
// Hide dashboard self-polling unless user opted in. Preserves the
|
// Optional noise reduction only: web-server:* entries can contain
|
||||||
// signal-to-noise ratio for fresh-load investigations.
|
// security-relevant dashboard access and WebSocket audit evidence, so
|
||||||
|
// they remain visible by default.
|
||||||
if (hideDashboardInternals && /^web-server:/i.test(entry.source)) return false;
|
if (hideDashboardInternals && /^web-server:/i.test(entry.source)) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -273,7 +274,7 @@ export function useLogsWorkspace() {
|
|||||||
setStageFilter('');
|
setStageFilter('');
|
||||||
setRequestIdFilter('');
|
setRequestIdFilter('');
|
||||||
setTimeWindow('all');
|
setTimeWindow('all');
|
||||||
setHideDashboardInternals(true);
|
setHideDashboardInternals(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
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