diff --git a/ui/src/components/logs/logs-config-card.tsx b/ui/src/components/logs/logs-config-card.tsx index b9551b72..7232fdff 100644 --- a/ui/src/components/logs/logs-config-card.tsx +++ b/ui/src/components/logs/logs-config-card.tsx @@ -41,7 +41,7 @@ export function LogsConfigCard({ const isDirty = useMemo(() => JSON.stringify(draft) !== JSON.stringify(config), [config, draft]); return ( - +
@@ -52,10 +52,12 @@ export function LogsConfigCard({ -
+
-

Current posture

+

+ Current posture +

{config.enabled ? 'Logging is enabled' : 'Logging is disabled'} at{' '} {config.level.toUpperCase()} and above. @@ -68,7 +70,7 @@ export function LogsConfigCard({

-
+

@@ -84,7 +86,7 @@ export function LogsConfigCard({ />

-
+

@@ -109,7 +111,11 @@ export function LogsConfigCard({ setDraft((current) => ({ ...current, level: value as LogsConfig['level'] })) } > - + @@ -128,6 +134,7 @@ export function LogsConfigCard({ id="logs-rotate-mb" type="number" min={1} + className="border-border/70 bg-background/70" value={draft.rotate_mb} onChange={(event) => setDraft((current) => ({ @@ -143,6 +150,7 @@ export function LogsConfigCard({ id="logs-retain-days" type="number" min={1} + className="border-border/70 bg-background/70" value={draft.retain_days} onChange={(event) => setDraft((current) => ({ @@ -160,6 +168,7 @@ export function LogsConfigCard({ id="logs-buffer-size" type="number" min={1} + className="border-border/70 bg-background/70" value={draft.live_buffer_size} onChange={(event) => setDraft((current) => ({ @@ -170,7 +179,7 @@ export function LogsConfigCard({ />

-
+
) : ( - -
+ +
{entries.map((entry) => { const isSelected = entry.id === selectedEntryId; @@ -63,33 +65,44 @@ export function LogsEntryList({ type="button" onClick={() => onSelect(entry.id)} className={cn( - 'w-full rounded-xl border px-4 py-3 text-left transition-colors', + 'w-full rounded-2xl border px-4 py-3 text-left transition-colors', isSelected - ? 'border-primary bg-primary/5 shadow-sm' - : 'border-border bg-background hover:bg-muted/40' + ? 'border-primary/40 bg-primary/5 shadow-sm' + : 'border-border/70 bg-background/70 hover:border-primary/20 hover:bg-muted/30' )} > -
+
+
+

+ {formatRelativeLogTime(entry.timestamp)} +

+

+ {formatLogTimestamp(entry.timestamp)} +

+
{sourceLabels[entry.source] ?? entry.source} + {entry.event}
-

+

{entry.message}

+
+ PID {entry.processId ?? 'n/a'} + Run {entry.runId ?? 'n/a'} +
- {entry.level === 'error' ? ( - - ) : null} -
-
- {entry.event} - {formatRelativeLogTime(entry.timestamp)} - {formatLogTimestamp(entry.timestamp)}
+ {entry.level === 'error' ? ( +
+ + Elevated severity +
+ ) : null} ); })} diff --git a/ui/src/components/logs/logs-filters.tsx b/ui/src/components/logs/logs-filters.tsx index f23cb01e..ad6ac500 100644 --- a/ui/src/components/logs/logs-filters.tsx +++ b/ui/src/components/logs/logs-filters.tsx @@ -1,16 +1,10 @@ -import { Search, RefreshCw } from 'lucide-react'; +import { Search, RefreshCw, SlidersHorizontal } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; import type { LogsSource } from '@/lib/api-client'; +import { cn } from '@/lib/utils'; import type { LogsLevelFilter, LogsSourceFilter } from '@/hooks/use-logs'; import { getLogLevelOptions, getSelectedSourceLabel } from '@/hooks/use-logs'; @@ -40,74 +34,24 @@ export function LogsFilters({ isRefreshing: boolean; }) { const sourceLabel = getSelectedSourceLabel(selectedSource, sources); + const levels = getLogLevelOptions(); + const limits = [50, 100, 150, 250]; return ( - - + + +
+ + Explorer filters +
- Log explorer - Slice the unified CCS log stream by source, severity, or message content. + Narrow the stream the same way you would in a management console: start with source, + then severity, then message text.
-
- -
- - -
- -
- - -
- -
- - -
- +
@@ -117,10 +61,99 @@ export function LogsFilters({ value={search} onChange={(event) => onSearchChange(event.target.value)} placeholder="Search message, event, process, or run ID" - className="pl-9" + className="h-11 border-border/70 bg-background/70 pl-9" />
+ +
+
+ + {sourceLabel} +
+
+ + {sources.map((source) => ( + + ))} +
+
+ +
+ +
+ {levels.map((option) => ( + + ))} +
+
+ +
+
+

+ View window +

+

+ Keep the rendered slice tight when you are chasing a live issue. +

+
+
+ {limits.map((option) => ( + + ))} +
+ +
); diff --git a/ui/src/components/logs/logs-overview-cards.tsx b/ui/src/components/logs/logs-overview-cards.tsx index 527a2315..678f8548 100644 --- a/ui/src/components/logs/logs-overview-cards.tsx +++ b/ui/src/components/logs/logs-overview-cards.tsx @@ -70,7 +70,7 @@ export function LogsOverviewCards({ 0 ? ` • ${legacySources} legacy` : ''}`} icon={Database} accent="bg-sky-500/10 text-sky-700 dark:text-sky-300" /> @@ -81,7 +81,7 @@ export function LogsOverviewCards({ icon={Activity} accent="bg-violet-500/10 text-violet-700 dark:text-violet-300" /> -
+
Last ingested event:{' '} {formatLogTimestamp(latestTimestamp)}
diff --git a/ui/src/pages/logs.tsx b/ui/src/pages/logs.tsx index 943a89f0..4bf09ef3 100644 --- a/ui/src/pages/logs.tsx +++ b/ui/src/pages/logs.tsx @@ -1,7 +1,17 @@ -import { AlertCircle, ArrowRight, RefreshCw } from 'lucide-react'; +import { + AlertCircle, + ArrowRight, + RefreshCw, + ScrollText, + ShieldAlert, + TimerReset, +} from 'lucide-react'; import { Link } from 'react-router-dom'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { ErrorLogsMonitor } from '@/components/error-logs-monitor'; import { LogsConfigCard } from '@/components/logs/logs-config-card'; import { LogsDetailPanel } from '@/components/logs/logs-detail-panel'; import { LogsEntryList } from '@/components/logs/logs-entry-list'; @@ -34,36 +44,82 @@ export function LogsPage() { } return ( -
-
-
+
+
+
+ LOGS +
+
-

System logs

-
-

Unified CCS logging

-

- Watch current CCS activity across native and legacy emitters, inspect structured - context, and keep retention policy aligned with what the host actually stores. +

+ + System logs +
+
+

+ Operational log workspace +

+

+ Work through the CCS event stream like a control center: source-first filtering, + dense entry inspection, and retention controls beside the live activity feed.

-
- - + +
+ + +
+ + Redaction +
+

+ {config.redact ? 'Active' : 'Disabled'} +

+

+ Sensitive fields are masked before persistence. +

+
+
+ + +
+ + Retention +
+

{config.retain_days} days

+

+ Rotation threshold {config.rotate_mb} MB. +

+
+
+
+ + +
@@ -83,25 +139,42 @@ export function LogsPage() { latestTimestamp={workspace.latestTimestamp} /> -
-
- - void Promise.all([workspace.sourcesQuery.refetch(), workspace.entriesQuery.refetch()]) - } - isRefreshing={workspace.entriesQuery.isFetching || workspace.sourcesQuery.isFetching} - /> + + + Activity stream + CLIProxy errors + + + +
+
+ + void Promise.all([ + workspace.sourcesQuery.refetch(), + workspace.entriesQuery.refetch(), + ]) + } + isRefreshing={ + workspace.entriesQuery.isFetching || workspace.sourcesQuery.isFetching + } + /> + updateConfig.mutate(payload)} + isPending={updateConfig.isPending} + /> +
-
-
+
- updateConfig.mutate(payload)} - isPending={updateConfig.isPending} - /> -
+ + + +

+ Legacy diagnostics +

+

+ CLIProxy request-failure viewer +

+

+ Keep the existing request-failure drilldown nearby while the structured CCS stream + matures. This tab preserves the old operational workflow instead of burying it + behind Home-only UI. +

+
+
+ +
+
); } diff --git a/ui/tests/unit/ui/pages/logs-page.test.tsx b/ui/tests/unit/ui/pages/logs-page.test.tsx index d4227e8c..1e119ff8 100644 --- a/ui/tests/unit/ui/pages/logs-page.test.tsx +++ b/ui/tests/unit/ui/pages/logs-page.test.tsx @@ -41,7 +41,7 @@ function buildEntries(source: string) { source: 'agent-runner', event: 'task.retry', message: 'Worker retry scheduled', - processId: 'worker-1', + processId: 4121, runId: 'run-2', context: { attempt: 2, reason: 'network jitter' }, }, @@ -56,7 +56,7 @@ function buildEntries(source: string) { source: 'dashboard', event: 'logs.bootstrap', message: 'Boot sequence failed for dashboard logging', - processId: 'api-1', + processId: 25582, runId: 'run-1', context: { component: 'dashboard', stage: 'bootstrap' }, }, @@ -67,7 +67,7 @@ function buildEntries(source: string) { source: 'agent-runner', event: 'task.retry', message: 'Worker retry scheduled', - processId: 'worker-1', + processId: 4121, runId: 'run-2', context: { attempt: 2, reason: 'network jitter' }, }, @@ -150,8 +150,7 @@ describe('LogsPage', () => { (await screen.findAllByText('Boot sequence failed for dashboard logging')).length ).toBeGreaterThan(0); - await userEvent.click(screen.getByRole('combobox', { name: 'Source filter' })); - await userEvent.click(await screen.findByRole('option', { name: 'Agent Runner' })); + await userEvent.click(screen.getByRole('button', { name: 'Agent Runner' })); expect((await screen.findAllByText('Worker retry scheduled')).length).toBeGreaterThan(0); await waitFor(() => { @@ -185,7 +184,7 @@ describe('LogsPage', () => { await userEvent.click(screen.getByRole('button', { name: /Worker retry scheduled/i })); expect((await screen.findAllByText('task.retry')).length).toBeGreaterThan(0); - expect(screen.getByText('worker-1')).toBeInTheDocument(); + expect(screen.getByText('4121')).toBeInTheDocument(); await userEvent.click(screen.getByRole('tab', { name: /Raw context/i }));