116 lines
2.9 KiB
TypeScript
116 lines
2.9 KiB
TypeScript
import {
|
|
LogApp,
|
|
LogEntry,
|
|
LogsBody,
|
|
LogsHeader,
|
|
LogsSection,
|
|
LogsTitle,
|
|
LogText,
|
|
LogTimestamp,
|
|
} from '../styles'
|
|
import { update } from '../update'
|
|
|
|
export interface UnifiedLogLine {
|
|
time: number
|
|
app: string
|
|
text: string
|
|
}
|
|
|
|
const MAX_LOGS = 200
|
|
|
|
let _logs: UnifiedLogLine[] = []
|
|
let _source: EventSource | undefined
|
|
|
|
const formatTime = (timestamp: number): string => {
|
|
const d = new Date(timestamp)
|
|
const pad = (n: number) => String(n).padStart(2, '0')
|
|
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
}
|
|
|
|
const getStatusColor = (status?: number): string | undefined => {
|
|
if (!status) return undefined
|
|
if (status >= 200 && status < 300) return '#22c55e'
|
|
if (status >= 400 && status < 500) return '#f59e0b'
|
|
if (status >= 500) return '#ef4444'
|
|
return undefined
|
|
}
|
|
|
|
function parseLogText(text: string): { method?: string, path?: string, status?: number, rest: string } {
|
|
// Match patterns like "GET /api/time 200" or "200 GET http://... (0ms)"
|
|
const httpMatch = text.match(/^(\d{3})\s+(GET|POST|PUT|DELETE|PATCH)\s+\S+/)
|
|
|| text.match(/^(GET|POST|PUT|DELETE|PATCH)\s+(\S+)\s+(\d{3})/)
|
|
|
|
if (httpMatch) {
|
|
if (httpMatch[1]?.match(/^\d{3}$/)) {
|
|
return { method: httpMatch[2], status: parseInt(httpMatch[1], 10), rest: text }
|
|
} else {
|
|
return { method: httpMatch[1], path: httpMatch[2], status: parseInt(httpMatch[3]!, 10), rest: text }
|
|
}
|
|
}
|
|
|
|
return { rest: text }
|
|
}
|
|
|
|
function LogLineEntry({ log }: { log: UnifiedLogLine }) {
|
|
const parsed = parseLogText(log.text)
|
|
const statusColor = getStatusColor(parsed.status)
|
|
|
|
return (
|
|
<LogEntry>
|
|
<LogTimestamp>{formatTime(log.time)}</LogTimestamp>
|
|
<LogApp>{log.app}</LogApp>
|
|
<LogText style={statusColor ? { color: statusColor } : undefined}>
|
|
{log.text}
|
|
</LogText>
|
|
</LogEntry>
|
|
)
|
|
}
|
|
|
|
function LogsBodyContent() {
|
|
return (
|
|
<>
|
|
{_logs.length === 0 ? (
|
|
<LogEntry>
|
|
<LogText style={{ color: 'var(--colors-textFaint)' }}>No activity yet</LogText>
|
|
</LogEntry>
|
|
) : (
|
|
_logs.map((log, i) => <LogLineEntry key={i} log={log} />)
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
function renderLogs() {
|
|
update('#unified-logs-body', <LogsBodyContent />)
|
|
// Auto-scroll after render
|
|
requestAnimationFrame(() => {
|
|
const el = document.getElementById('unified-logs-body')
|
|
if (el) el.scrollTop = el.scrollHeight
|
|
})
|
|
}
|
|
|
|
export function initUnifiedLogs() {
|
|
if (_source) return
|
|
_source = new EventSource('/api/system/logs/stream')
|
|
_source.onmessage = e => {
|
|
try {
|
|
const line = JSON.parse(e.data) as UnifiedLogLine
|
|
_logs = [..._logs.slice(-(MAX_LOGS - 1)), line]
|
|
renderLogs()
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
export function UnifiedLogs() {
|
|
return (
|
|
<LogsSection>
|
|
<LogsHeader>
|
|
<LogsTitle>Logs</LogsTitle>
|
|
</LogsHeader>
|
|
<LogsBody id="unified-logs-body">
|
|
<LogsBodyContent />
|
|
</LogsBody>
|
|
</LogsSection>
|
|
)
|
|
}
|