Adds type definitions + forwarding for status, reconnect_attempt, reconnect_failed, reconnect events. Frontend build now runs tsc --noEmit before vite build so undefined socket events cause failures. Fixes pre-existing type errors exposed by strict mode in the frontend.
126 lines
4.1 KiB
TypeScript
126 lines
4.1 KiB
TypeScript
import { useRef, useEffect, useState } from 'react';
|
|
|
|
interface LogEntry {
|
|
id: string;
|
|
timestamp: Date;
|
|
level: string;
|
|
component: string;
|
|
message: string;
|
|
metadata?: unknown;
|
|
}
|
|
|
|
interface LogPanelProps {
|
|
logs: LogEntry[];
|
|
expanded: boolean;
|
|
onToggleExpand: () => void;
|
|
}
|
|
|
|
const levelColors: Record<string, string> = {
|
|
debug: 'text-gray-500',
|
|
info: 'text-green-400',
|
|
warn: 'text-yellow-400',
|
|
alert: 'text-red-400',
|
|
error: 'bg-red-800 text-white',
|
|
crit: 'bg-red-800 text-yellow-300',
|
|
fatal: 'bg-red-800 text-gray-400',
|
|
};
|
|
|
|
function formatTimestamp(date: Date): string {
|
|
const y = date.getFullYear();
|
|
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
const d = String(date.getDate()).padStart(2, '0');
|
|
const hh = String(date.getHours()).padStart(2, '0');
|
|
const mm = String(date.getMinutes()).padStart(2, '0');
|
|
const ss = String(date.getSeconds()).padStart(2, '0');
|
|
const ms = String(date.getMilliseconds()).padStart(3, '0');
|
|
return `${y}-${m}-${d} ${hh}:${mm}:${ss}.${ms}`;
|
|
}
|
|
|
|
export default function LogPanel({ logs, expanded, onToggleExpand }: LogPanelProps) {
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
const [expandedMetadata, setExpandedMetadata] = useState<Set<string>>(new Set());
|
|
|
|
useEffect(() => {
|
|
if (scrollRef.current) {
|
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
}
|
|
}, [logs]);
|
|
|
|
const toggleMetadata = (id: string) => {
|
|
setExpandedMetadata(prev => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) {
|
|
next.delete(id);
|
|
} else {
|
|
next.add(id);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={`border-t border-border-subtle bg-bg-secondary flex flex-col ${
|
|
expanded ? 'flex-1' : 'h-48'
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between px-4 py-2 bg-bg-tertiary shrink-0">
|
|
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider">
|
|
Log
|
|
</h3>
|
|
<button
|
|
onClick={onToggleExpand}
|
|
className="w-6 h-6 flex items-center justify-center text-text-muted hover:text-text-secondary transition-colors"
|
|
title={expanded ? 'Collapse' : 'Expand'}
|
|
>
|
|
{expanded ? '▾' : '▴'}
|
|
</button>
|
|
</div>
|
|
<div
|
|
ref={scrollRef}
|
|
className="flex-1 overflow-y-auto p-2 font-mono text-xs leading-relaxed"
|
|
>
|
|
{logs.length === 0 ? (
|
|
<div className="text-text-muted">No log entries</div>
|
|
) : (
|
|
logs.map((entry) => {
|
|
const levelClass = levelColors[entry.level] || 'text-text-secondary';
|
|
return (
|
|
<div key={entry.id} className="flex gap-2 py-0.5 hover:bg-bg-elevated/50">
|
|
<span className="text-gray-600 shrink-0 select-none">
|
|
{formatTimestamp(entry.timestamp)}
|
|
</span>
|
|
<span className={`shrink-0 font-bold ${levelClass}`}>
|
|
{entry.level.toUpperCase().padEnd(5)}
|
|
</span>
|
|
<span className="text-cyan-400 shrink-0">
|
|
{entry.component}
|
|
</span>
|
|
<span className="text-text-secondary break-all min-w-0">
|
|
{entry.message}
|
|
</span>
|
|
{entry.metadata != null && (
|
|
<button
|
|
onClick={() => toggleMetadata(entry.id)}
|
|
className="shrink-0 text-text-muted hover:text-text-secondary transition-colors"
|
|
title={expandedMetadata.has(entry.id) ? 'Hide metadata' : 'Show metadata'}
|
|
>
|
|
{expandedMetadata.has(entry.id) ? '⊟' : '⊞'}
|
|
</button>
|
|
)}
|
|
{entry.metadata != null && expandedMetadata.has(entry.id) && (
|
|
<div className="w-full text-text-muted pl-[1em] border-l border-border-subtle mt-0.5 mb-1">
|
|
<pre className="whitespace-pre-wrap break-all">
|
|
{JSON.stringify(entry.metadata, null, 2)}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|