import { useEffect, useRef, useState, type ReactNode } from 'react'
import { NavMenu } from './components/NavMenu'
import { FilterBar } from './components/FilterBar'
import { LogTimeline, computeBucketMs, computePresetRange, type TimeRange, type TimeRangePresetId } from './components/LogTimeline'
import { LogPreview } from './components/LogPreview'
import { Params } from './components/Params'
import { Metrics } from './components/Metrics'
import { Dashboards } from './components/Dashboards'
import { Alerts } from './components/Alerts'
import { CollapseToggle, PopOutButton, type PopOutSectionId, useSectionCollapsed } from './components/CollapsibleSection'
import {
fetchLogs,
fetchParamDeclarations,
fetchStats,
fetchTimeline,
type LogEntry,
type ParamDeclaration,
type Stats,
type TimelineBucket,
} from './lib/api'
import { DocsNavProvider } from './lib/docsNav'
const POLL_MS = 2000
/// Reads the routing params `PopOutButton` writes: `section` picks which primary section to render full-page, and `startMs`/`endMs` carry over the range for sections with no `LogTimeline` of their own to pick one.
function parsePopOutQuery(): { section: PopOutSectionId | null; range: TimeRange | null } {
const query = new URLSearchParams(window.location.search)
const section = query.get('section') as PopOutSectionId | null
const startMs = Number(query.get('startMs'))
const endMs = Number(query.get('endMs'))
const range = query.has('startMs') && query.has('endMs') && Number.isFinite(startMs) && Number.isFinite(endMs)
? { startMs, endMs }
: null
return { section, range }
}
function StandalonePage({ children }: { children: ReactNode }) {
return (
{children}
)
}
/// Popped out on its own, Params has no lifted-state sibling to share `params`/`loadParams` with, so it owns a minimal fetch here instead.
function StandaloneParams() {
const [params, setParams] = useState([])
async function loadParams() {
try {
setParams(await fetchParamDeclarations())
} catch {
// engine not reachable yet; leave the previous list in place
}
}
useEffect(() => {
loadParams()
}, [])
return (
)
}
/// Metrics/Dashboards own their own data fetching — they only need the range carried over via query params.
function StandaloneRanged({ section, range }: { section: 'metrics' | 'dashboards'; range: TimeRange | null }) {
return {section === 'metrics' ? : }
}
/// The log viewer has its own `LogTimeline`, so it needs its own copy of App's log/timeline polling state instead of a carried-over range.
function StandaloneLog() {
const [logs, setLogs] = useState([])
const [params, setParams] = useState([])
const [buckets, setBuckets] = useState([])
const [range, setRange] = useState(null)
const [activePreset, setActivePreset] = useState('all')
const [filter, setFilter] = useState('')
const [filterError, setFilterError] = useState(null)
const rangeRef = useRef(range)
useEffect(() => {
rangeRef.current = range
}, [range])
useEffect(() => {
fetchParamDeclarations()
.then(setParams)
.catch(() => {
// engine not reachable yet; leave the previous list in place
})
}, [])
function selectPreset(id: TimeRangePresetId) {
setActivePreset(id)
setRange(computePresetRange(id))
}
function handleManualRangeSelect(next: TimeRange | null) {
if (next === null) {
selectPreset('all')
} else {
setActivePreset(null)
setRange(next)
}
}
useEffect(() => {
let cancelled = false
async function poll() {
const effectiveRange =
activePreset && activePreset !== 'all' ? computePresetRange(activePreset) : rangeRef.current
if (activePreset && activePreset !== 'all') {
setRange((current) =>
current && effectiveRange && current.startMs === effectiveRange.startMs && current.endMs === effectiveRange.endMs
? current
: effectiveRange,
)
}
try {
const [nextBuckets, nextLogs] = await Promise.all([
fetchTimeline({
startMs: effectiveRange?.startMs,
endMs: effectiveRange?.endMs,
// Only meaningful for a fixed window — 'all' has no span to derive a resolution from, so it keeps the server's default bucketing.
bucketMs: effectiveRange ? computeBucketMs(effectiveRange.endMs - effectiveRange.startMs) : undefined,
filter: filter || undefined,
}),
fetchLogs({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, limit: 1000, filter: filter || undefined }),
])
if (!cancelled) {
setBuckets(nextBuckets)
setLogs(nextLogs)
setFilterError(null)
}
} catch (err) {
if (!cancelled) {
setFilterError(err instanceof Error ? err.message : String(err))
}
}
}
poll()
const id = setInterval(poll, POLL_MS)
return () => {
cancelled = true
clearInterval(id)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filter, activePreset])
return (
)
}
export default function App() {
const { section, range: standaloneRange } = parsePopOutQuery()
if (section === 'log') return
if (section === 'params') return
if (section === 'metrics' || section === 'dashboards') return
return
}
function FullApp() {
const [stats, setStats] = useState(null)
const [logs, setLogs] = useState([])
const [params, setParams] = useState([])
const [buckets, setBuckets] = useState([])
const [range, setRange] = useState(null)
const [activePreset, setActivePreset] = useState('all')
const [filter, setFilter] = useState('')
const [filterError, setFilterError] = useState(null)
const [logViewerCollapsed, setLogViewerCollapsed] = useSectionCollapsed('log-viewer')
// Read via ref inside the poll loop instead of depending on `range` directly, which would restart the interval every rolling-preset tick.
const rangeRef = useRef(range)
useEffect(() => {
rangeRef.current = range
}, [range])
async function loadParams() {
try {
setParams(await fetchParamDeclarations())
} catch {
// engine not reachable yet; leave the previous list in place
}
}
useEffect(() => {
loadParams()
}, [])
function selectPreset(id: TimeRangePresetId) {
setActivePreset(id)
setRange(computePresetRange(id))
}
// A drag always yields a fixed (non-rolling) range, overriding any active preset.
function handleManualRangeSelect(next: TimeRange | null) {
if (next === null) {
selectPreset('all')
} else {
setActivePreset(null)
setRange(next)
}
}
useEffect(() => {
let cancelled = false
async function poll() {
// Rolling presets re-resolve on this same polling cadence instead of freezing the window at selection time.
const effectiveRange =
activePreset && activePreset !== 'all' ? computePresetRange(activePreset) : rangeRef.current
if (activePreset && activePreset !== 'all') {
setRange((current) =>
current && effectiveRange && current.startMs === effectiveRange.startMs && current.endMs === effectiveRange.endMs
? current
: effectiveRange,
)
}
try {
const [nextStats, nextBuckets, nextLogs] = await Promise.all([
fetchStats(),
fetchTimeline({
startMs: effectiveRange?.startMs,
endMs: effectiveRange?.endMs,
// Only meaningful for a fixed window — 'all' has no span to derive a resolution from, so it keeps the server's default bucketing.
bucketMs: effectiveRange ? computeBucketMs(effectiveRange.endMs - effectiveRange.startMs) : undefined,
filter: filter || undefined,
}),
fetchLogs({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, limit: 1000, filter: filter || undefined }),
])
if (!cancelled) {
setStats(nextStats)
setBuckets(nextBuckets)
setLogs(nextLogs)
setFilterError(null)
}
} catch (err) {
if (!cancelled) {
setFilterError(err instanceof Error ? err.message : String(err))
}
}
}
poll()
const id = setInterval(poll, POLL_MS)
return () => {
cancelled = true
clearInterval(id)
}
// Intentionally excludes `range` to avoid restarting this interval on every rolling-preset tick.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filter, activePreset])
return (
setLogViewerCollapsed(!logViewerCollapsed)} />
>
}
/>
{/* min-h-[...] keeps this from being squeezed to 0 by the sections below it in the shared scroll container — flex-1 alone lets a flex item shrink past its content, which let this section's absolutely-unclipped content spill over Params et al instead of the column just scrolling. */}
)
}