diff --git a/ui/src/components/logs/log-level-badge.tsx b/ui/src/components/logs/log-level-badge.tsx index adf85604..e4de4bee 100644 --- a/ui/src/components/logs/log-level-badge.tsx +++ b/ui/src/components/logs/log-level-badge.tsx @@ -13,7 +13,7 @@ export function LogLevelBadge({ level, className }: { level: LogsLevel; classNam return ( JSON.stringify(draft) !== JSON.stringify(config), [config, draft]); return ( - - -
- - Logging policy +
+
+
+
+ +
+
+

+ Logging Policy +

+

+ Retention and privacy +

+
- - Keep retention, file rotation, redaction, and live tail depth aligned with the host. - - - -
-
-
-

- Current posture -

-

- {config.enabled ? 'Logging is enabled' : 'Logging is disabled'} at{' '} - {config.level.toUpperCase()} and above. -

+
+
+ +
+
+
+ + Active Status + +
+ + {config.enabled ? 'Live' : 'Off'} +
- - {config.redact ? 'Redacted' : 'Plain'} +
+
+ + Redaction + + + {config.redact ? 'Enforced' : 'Plain'}
-
-
-
- -

- Turn the unified logging pipeline on or off. +

+
+
+ +

+ Enable structured logging

setDraft((current) => ({ ...current, enabled: checked })) } + className="data-[state=checked]:bg-primary" />
-
-
- -

- Mask sensitive content before it lands in the log archive. +

+
+ +

+ Sanitize payload data

setDraft((current) => ({ ...current, redact: checked })) } + className="data-[state=checked]:bg-primary" />
-
- - + setDraft((current) => ({ ...current, level: value as LogsConfig['level'] })) + } > - - - - Error only - Warn and above - Info and above - Debug and above - - -
- -
-
- - - setDraft((current) => ({ - ...current, - rotate_mb: parseInteger(event.target.value, current.rotate_mb), - })) - } - /> + + + + + + Error Only + + + Warn + Above + + + Info + Above + + + Full Debug + + +
-
- - - setDraft((current) => ({ - ...current, - retain_days: parseInteger(event.target.value, current.retain_days), - })) - } - /> + +
+
+ + + setDraft((current) => ({ + ...current, + rotate_mb: parseInteger(event.target.value, current.rotate_mb), + })) + } + /> +
+
+ + + setDraft((current) => ({ + ...current, + retain_days: parseInteger(event.target.value, current.retain_days), + })) + } + /> +
-
- - - setDraft((current) => ({ - ...current, - live_buffer_size: parseInteger(event.target.value, current.live_buffer_size), - })) - } - /> -
- -
- - {isDirty ? Unsaved changes : null}
- - +
+ +
+
+ + + Operational Logic v3.4 + +
+ {isDirty && ( +
+
+ + Pending + +
+ )} +
+
); } diff --git a/ui/src/components/logs/logs-detail-panel.tsx b/ui/src/components/logs/logs-detail-panel.tsx index df3fb459..f2b219c5 100644 --- a/ui/src/components/logs/logs-detail-panel.tsx +++ b/ui/src/components/logs/logs-detail-panel.tsx @@ -1,16 +1,46 @@ -import { FileJson, Info } from 'lucide-react'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { + FileJson, + Info, + ShieldCheck, + Terminal, + Fingerprint, + Database, + Cpu, + Activity, + type LucideIcon, +} from 'lucide-react'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import type { LogsEntry } from '@/lib/api-client'; +import { cn } from '@/lib/utils'; import { LogLevelBadge } from './log-level-badge'; -import { formatJson, formatLogTimestamp } from './utils'; +import { formatJson } from './utils'; -function MetaRow({ label, value }: { label: string; value: string | number }) { +function MetaRow({ + label, + value, + icon: Icon, +}: { + label: string; + value: string | number; + icon?: LucideIcon; +}) { return ( -
-

{label}

-

{value}

+
+
+
+ {Icon && ( + + )} +

+ {label} +

+
+
+
+

+ {value} +

); } @@ -24,92 +54,213 @@ export function LogsDetailPanel({ }) { if (!entry) { return ( - - - Inspector - - Select an activity row to inspect its operational metadata. - - - - Nothing selected yet. - - +
+
+
+
+ +
+
+
+

+ Inspector Standby +

+

+ Select a telemetry node from the active data queue to perform deep analysis of its + operational context. +

+
+
); } return ( - - -
- - - {sourceLabel ?? entry.source} - - - {formatLogTimestamp(entry.timestamp)} - -
-
- {entry.event} - - {entry.message} - -
-
- - - - - - Details - - - - Raw context - - +
+ {/* Tactical Inspector Header */} +
+ {/* Pattern Overlay */} +
- -
- - - - +
+
+
+ +
+
+ + + {sourceLabel ?? entry.source} + +
-
-

- Context summary -

-

- This entry belongs to{' '} - {sourceLabel ?? entry.source}{' '} - and was emitted at the{' '} - {entry.level} threshold. Use - the raw tab for the exact structured payload. -

-
- - - - -
-                {formatJson({
-                  id: entry.id,
-                  timestamp: entry.timestamp,
-                  level: entry.level,
-                  source: entry.source,
-                  event: entry.event,
-                  message: entry.message,
-                  processId: entry.processId,
-                  runId: entry.runId,
-                  context: entry.context ?? {},
+            
+ + + {new Date(entry.timestamp).toLocaleTimeString(undefined, { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', })} -
-
-
- - - + +
+
+ +
+
+
+

+ Event +

+
+

+ {entry.event} +

+
+

+ {entry.message} +

+
+
+
+
+ + +
+ + + + + Details + + + + Raw Context + + + + +
+ + + + +
+ +
+ {/* Background Scanline */} +
+ +
+
+
+ +
+
+

+ Automated Summary +

+

+ Quick interpretation +

+
+
+

+ This telemetry node was captured from{' '} + + {sourceLabel ?? entry.source} + + operating at the{' '} + + {entry.level} + {' '} + threshold. The operational payload indicates an event state of{' '} + {entry.event}. +

+
+
+ + + +
+ {/* Copy HUD */} +
+
+ JSON.RAW.MODE +
+
+ + +
+                    {formatJson({
+                      id: entry.id,
+                      timestamp: entry.timestamp,
+                      level: entry.level,
+                      source: entry.source,
+                      event: entry.event,
+                      message: entry.message,
+                      processId: entry.processId,
+                      runId: entry.runId,
+                      context: entry.context ?? {},
+                    })}
+                  
+
+
+
+ +
+ + +
+
+
+
+ + Node Verified + +
+
+ + {entry.id.slice(0, 8)} + +
+
+ + CCS-TEC-v3 + +
+
+
); } diff --git a/ui/src/components/logs/logs-entry-list.tsx b/ui/src/components/logs/logs-entry-list.tsx index 3998303f..dc205f6a 100644 --- a/ui/src/components/logs/logs-entry-list.tsx +++ b/ui/src/components/logs/logs-entry-list.tsx @@ -1,10 +1,8 @@ -import { AlertCircle, Inbox, Loader2 } from 'lucide-react'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Activity, ArrowRight, Inbox, Loader2 } from 'lucide-react'; import { ScrollArea } from '@/components/ui/scroll-area'; import type { LogsEntry } from '@/lib/api-client'; import { cn } from '@/lib/utils'; import { LogLevelBadge } from './log-level-badge'; -import { formatLogTimestamp, formatRelativeLogTime } from './utils'; export function LogsEntryList({ entries, @@ -22,40 +20,79 @@ export function LogsEntryList({ isFetching: boolean; }) { return ( - - -
-
- Activity queue - - Dense operational feed for the active source and severity mix. - +
+
+
+
+
+

+ Live Entry Stream +

+
+
+
+ + + Live telemetry +
- {isFetching ? : null}
- - +
+ {isFetching && ( +
+ + + Syncing + +
+ )} + + NODE.01 + +
+
+ +
+
Time
+
Lvl
+
Source
+
Message
+
Proc
+
Run
+
Open
+
+ +
{isLoading ? ( -
- {[1, 2, 3, 4, 5].map((item) => ( -
-
-
-
-
+
+ {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].map((item) => ( +
))}
) : entries.length === 0 ? ( -
- -
-

No entries matched these filters.

-

Try broadening the source, severity, or search terms.

+
+
+
+
+ +
+
+
+

+ No matching entries +

+

+ Your current source, level, or search filters are hiding the stream. Adjust them to + bring entries back into view. +

) : ( - -
+ +
{entries.map((entry) => { const isSelected = entry.id === selectedEntryId; @@ -65,51 +102,114 @@ export function LogsEntryList({ type="button" onClick={() => onSelect(entry.id)} className={cn( - 'w-full rounded-2xl border px-4 py-3 text-left transition-colors', + 'group relative flex w-full items-center border-b border-border/5 px-0 py-2.5 text-left transition-all duration-150', isSelected - ? 'border-primary/40 bg-primary/5 shadow-sm' - : 'border-border/70 bg-background/70 hover:border-primary/20 hover:bg-muted/30' + ? 'z-10 bg-primary/[0.08] shadow-[inset_4px_0_0_rgba(var(--primary),1)]' + : 'bg-transparent hover:bg-muted/30' )} > -
-
-

- {formatRelativeLogTime(entry.timestamp)} -

-

- {formatLogTimestamp(entry.timestamp)} +

+ +
+
+

+ {new Date(entry.timestamp).toLocaleTimeString(undefined, { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + })}

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

- {entry.message} -

-
- PID {entry.processId ?? 'n/a'} - Run {entry.runId ?? 'n/a'} + +
+

+ {entry.message} +

+
+ +
+ {entry.processId ?? '????'} +
+
+ {entry.runId?.slice(0, 4).toUpperCase() ?? 'NONE'} +
+
+
+ +
- {entry.level === 'error' ? ( -
- - Elevated severity -
- ) : null} ); })}
)} - - +
+ +
+
+ + Node: CCS-CORE + +
+ + Status: Operational + +
+
+ + Entries: {entries.length} + +
+
+
); } diff --git a/ui/src/components/logs/logs-filters.tsx b/ui/src/components/logs/logs-filters.tsx index ad6ac500..f212890d 100644 --- a/ui/src/components/logs/logs-filters.tsx +++ b/ui/src/components/logs/logs-filters.tsx @@ -1,12 +1,11 @@ -import { Search, RefreshCw, SlidersHorizontal } from 'lucide-react'; +import { Search, RefreshCw, Filter, Shield, Zap } 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 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'; +import { getLogLevelOptions } from '@/hooks/use-logs'; export function LogsFilters({ sources, @@ -33,67 +32,76 @@ export function LogsFilters({ onRefresh: () => void; isRefreshing: boolean; }) { - const sourceLabel = getSelectedSourceLabel(selectedSource, sources); const levels = getLogLevelOptions(); const limits = [50, 100, 150, 250]; return ( - - -
- - Explorer filters -
-
- - Narrow the stream the same way you would in a management console: start with source, - then severity, then message text. - -
-
- -
- -
- - onSearchChange(event.target.value)} - placeholder="Search message, event, process, or run ID" - className="h-11 border-border/70 bg-background/70 pl-9" - /> -
-
- -
-
- - {sourceLabel} -
-
- + Payload Search + +
+ +
+
+
+ +
+ onSearchChange(event.target.value)} + placeholder="Scan for patterns..." + className="h-11 rounded-xl border-2 border-border/40 bg-background/50 pl-10 text-[13px] font-medium text-foreground placeholder:text-foreground/35 focus-visible:border-primary/40 focus-visible:ring-0 transition-all shadow-inner" + /> +
+
+ +
+
+ + +
+
+ + +
{sources.map((source) => (
+
-
- -
- {levels.map((option) => ( - - ))} -
+ {/* Threshold Control */} +
+
+ +
+
+ {levels.map((option) => ( + + ))} +
+
-
-
-

- View window -

-

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

+ {/* Operational Deck */} +
+
+
+
+

+ Operational Window +

+

+ Tail Capacity +

+
+
+ +
-
+ +
{limits.map((option) => ( ))}
-
- - +
+
); } diff --git a/ui/src/index.css b/ui/src/index.css index b15fdb8e..88723524 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -374,3 +374,50 @@ .scrollbar-editor::-webkit-scrollbar-corner { background: transparent; } + +@keyframes scan { + 0% { + transform: translateY(0); + } + 100% { + transform: translateY(100%); + } +} + +.animate-scan { + animation: scan 8s linear infinite; +} + +@keyframes slide-in-from-right { + from { + transform: translateX(10px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slide-in-from-left { + from { + transform: translateX(-10px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.slide-in-from-right-4 { + animation: slide-in-from-right 0.5s ease-out; +} + +.slide-in-from-left-2 { + animation: slide-in-from-left 0.4s ease-out; +} + +.slide-in-from-bottom-2 { + animation: enter 0.3s ease-out; +} diff --git a/ui/src/pages/logs.tsx b/ui/src/pages/logs.tsx index 4bf09ef3..e96d1ab9 100644 --- a/ui/src/pages/logs.tsx +++ b/ui/src/pages/logs.tsx @@ -1,41 +1,93 @@ +import { useEffect, useState } from 'react'; import { - AlertCircle, ArrowRight, + ChevronLeft, + ChevronRight, 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 { ScrollArea } from '@/components/ui/scroll-area'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { cn } from '@/lib/utils'; 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'; import { LogsFilters } from '@/components/logs/logs-filters'; -import { LogsOverviewCards } from '@/components/logs/logs-overview-cards'; import { LogsPageSkeleton } from '@/components/logs/logs-page-skeleton'; import { getSourceLabelMap, useLogsWorkspace, useUpdateLogsConfig } from '@/hooks/use-logs'; +const DESKTOP_LOGS_BREAKPOINT = 1200; +const LEFT_PANEL_WIDTH = 336; +const RIGHT_PANEL_WIDTH = 368; +const COLLAPSED_PANEL_WIDTH = 52; + +function CollapsedPaneToggle({ + side, + label, + onExpand, +}: { + side: 'left' | 'right'; + label: string; + onExpand: () => void; +}) { + return ( +
+ + + {label} + +
+ ); +} + export function LogsPage() { const workspace = useLogsWorkspace(); const updateConfig = useUpdateLogsConfig(); const sourceLabels = getSourceLabelMap(workspace.sourcesQuery.data ?? []); - const errors = [ - workspace.configQuery.error, - workspace.sourcesQuery.error, - workspace.entriesQuery.error, - ].filter(Boolean) as Error[]; + const [isDesktopLayout, setIsDesktopLayout] = useState(() => + typeof window !== 'undefined' ? window.innerWidth >= DESKTOP_LOGS_BREAKPOINT : false + ); + const [isFiltersCollapsed, setIsFiltersCollapsed] = useState(false); + const [isDetailsCollapsed, setIsDetailsCollapsed] = useState(false); + + useEffect(() => { + const mediaQuery = window.matchMedia(`(min-width: ${DESKTOP_LOGS_BREAKPOINT}px)`); + const syncLayout = () => { + setIsDesktopLayout(window.innerWidth >= DESKTOP_LOGS_BREAKPOINT); + }; + + syncLayout(); + mediaQuery.addEventListener('change', syncLayout); + return () => mediaQuery.removeEventListener('change', syncLayout); + }, []); if (workspace.isInitialLoading) { - return ( -
- -
- ); + return ; } const config = workspace.configQuery.data; @@ -44,173 +96,389 @@ export function LogsPage() { } return ( -
-
-
- LOGS -
-
-
-
- - 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. +

+
+ + Operational + +

+ Log Operations Center +

+
+

+ CCS.TOC.LOGS.STREAM.v3

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

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

-

- Sensitive fields are masked before persistence. -

- - - - -
- + + + {config.redact ? 'Enforced' : 'Standby'} + +
+
+
+
+ +
+
+ Retention -
-

{config.retain_days} days

-

- Rotation threshold {config.rotate_mb} MB. -

- - -
- - + + + {config.retain_days}D / {config.rotate_mb}MB + +
+ +
+ +
+ +
- {errors.length > 0 ? ( - - - Unable to fully load the logs workspace - {errors[0]?.message} - - ) : null} +
+ +
+ + + Telemetry Stream + + + Legacy Errors + + - - - - - 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} - /> +
+
+ + + + + + Connected + +
+ + {workspace.entriesQuery.data?.length ?? 0} captured +
- - -
- - - - -

- 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. -

-
-
- -
- + + {isDesktopLayout ? ( +
+
+ {isFiltersCollapsed ? ( + setIsFiltersCollapsed(false)} + /> + ) : ( +
+
+
+

+ Filters +

+

+ Search, source, and retention controls +

+
+ +
+ + +
+ + void Promise.all([ + workspace.sourcesQuery.refetch(), + workspace.entriesQuery.refetch(), + ]) + } + isRefreshing={ + workspace.entriesQuery.isFetching || workspace.sourcesQuery.isFetching + } + /> +
+ updateConfig.mutate(payload)} + isPending={updateConfig.isPending} + /> +
+
+
+
+ )} +
+ +
+ +
+ +
+ {isDetailsCollapsed ? ( + setIsDetailsCollapsed(false)} + /> + ) : ( +
+
+
+

+ Details +

+

+ Selected entry context and raw payload +

+
+ +
+
+ +
+
+ )} +
+
+ ) : ( +
+
+
+ + void Promise.all([ + workspace.sourcesQuery.refetch(), + workspace.entriesQuery.refetch(), + ]) + } + isRefreshing={ + workspace.entriesQuery.isFetching || workspace.sourcesQuery.isFetching + } + /> + updateConfig.mutate(payload)} + isPending={updateConfig.isPending} + /> +
+
+ +
+ +
+ +
+ +
+
+ )} +
+ + +
+
+
+ + + +
+
+
+ +
+
+

+ Legacy Diagnostic Node +

+

+ CCS-MATRIX-FAILURE-MONITOR +

+
+
+
+ Mode: Historical +
+
+ +
+

+ CLIProxy Failure Analysis +

+

+ Maintain oversight of legacy request failures while the unified stream + consolidates system-wide telemetry. This view provides direct access to the + historical failure matrix for deep-field debugging. +

+
+ +
+ + +
+ +
+
+
+ + Realtime Monitoring Deck + +
+ +
+
+ + +
); } diff --git a/ui/tests/unit/ui/pages/logs-page.test.tsx b/ui/tests/unit/ui/pages/logs-page.test.tsx index 1e119ff8..32521ffa 100644 --- a/ui/tests/unit/ui/pages/logs-page.test.tsx +++ b/ui/tests/unit/ui/pages/logs-page.test.tsx @@ -131,6 +131,11 @@ describe('LogsPage', () => { beforeEach(() => { vi.clearAllMocks(); global.fetch = fetchMock; + Object.defineProperty(window, 'innerWidth', { + configurable: true, + writable: true, + value: 900, + }); }); it('shows the loading skeleton while the initial queries are pending', () => { @@ -184,11 +189,11 @@ 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('4121')).toBeInTheDocument(); + expect(screen.getAllByText('4121').length).toBeGreaterThan(0); await userEvent.click(screen.getByRole('tab', { name: /Raw context/i })); - expect(await screen.findByText(/"reason": "network jitter"/)).toBeInTheDocument(); - expect(screen.getByText(/"runId": "run-2"/)).toBeInTheDocument(); + expect(await screen.findByText(/network jitter/)).toBeInTheDocument(); + expect(screen.getByText(/run-2/)).toBeInTheDocument(); }); });