diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx
index 0680619..07565e9 100644
--- a/webapp/src/App.tsx
+++ b/webapp/src/App.tsx
@@ -6,6 +6,7 @@ import { LogPreview } from './components/LogPreview'
import { Metrics } from './components/Metrics'
import { Dashboards } from './components/Dashboards'
import { fetchLogs, fetchStats, fetchTimeline, type LogEntry, type Stats, type TimelineBucket } from './lib/api'
+import { DocsNavProvider } from './lib/docsNav'
const POLL_MS = 2000
@@ -49,15 +50,17 @@ export default function App() {
}, [range, filter])
return (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+
)
}
diff --git a/webapp/src/components/Dashboards.tsx b/webapp/src/components/Dashboards.tsx
index 5d41c8a..a693d67 100644
--- a/webapp/src/components/Dashboards.tsx
+++ b/webapp/src/components/Dashboards.tsx
@@ -14,6 +14,7 @@ import {
type Panel,
type PromqlSeries,
} from '../lib/api'
+import { HelpIcon } from './HelpIcon'
const CHART_TYPES: ChartType[] = ['line', 'bar', 'pie']
@@ -168,7 +169,8 @@ export function Dashboards({ range }: { range: TimeRange | null }) {
if (!activeProfile) {
return (
-
+
+
Dashboards — activate a profile (see the settings menu) to create and save dashboards.
)
@@ -176,7 +178,10 @@ export function Dashboards({ range }: { range: TimeRange | null }) {
return (
-
Dashboards — profile "{activeProfile}"
+
+
+ Dashboards — profile "{activeProfile}"
+
{error && (
{error}
)}
diff --git a/webapp/src/components/Docs.tsx b/webapp/src/components/Docs.tsx
index 89a2d80..8129301 100644
--- a/webapp/src/components/Docs.tsx
+++ b/webapp/src/components/Docs.tsx
@@ -1,4 +1,5 @@
-import { useState, type ReactNode } from 'react'
+import { useEffect, useRef, useState, type ReactNode } from 'react'
+import type { DocsTarget } from '../lib/docsNav'
/// One accordion section: a title plus its body, rendered as plain JSX so
/// each page can mix prose, lists, and inline code without pulling in a
@@ -122,6 +123,32 @@ const SECTIONS: DocSection[] = [
+ The timeline above the log table renders log volume per bucket over the active window. Drag across it to
+ select a fixed time range — it narrows the log table, metrics, and dashboards below to that window; click
+ "Clear selection" to go back to the default (rolling "all time") view.
+
+ The log table lists ingested lines newest-last, each with its sequence number, timestamp, and raw text. The
+ column picker (top-right) adds a column per declared param, showing that line's derived value for it (or a
+ blank if it didn't match) — a quick way to eyeball params without switching to the Metrics panel.
+
+ The Settings panel controls process-wide state: colour scheme, the maximum number of logs kept in memory
+ (blank means unlimited), and imported/linked CSS. These apply immediately and persist to the single
+ settings file on disk — no save button needed.
+
+ The Presets panel manages profiles: create, switch, and delete them. Switching activates a profile's
+ params/metrics in the running engine immediately, with no restart needed — see the General idea{' '}
+ section above for what a profile stores.
+
+
+ ),
+ },
]
-export function DocsPanel() {
+/** How long a flashed section stays highlighted before settling back to normal. */
+const FLASH_DURATION_MS = 1200
+
+export function DocsPanel({ target }: { target?: DocsTarget | null }) {
const [openId, setOpenId] = useState(SECTIONS[0].id)
+ const [flashId, setFlashId] = useState(null)
+ const sectionRefs = useRef>({})
+
+ // Keyed on `target?.nonce` (not `target?.sectionId`) so clicking the same
+ // "?" icon twice in a row re-triggers the flash rather than being a no-op
+ // because the panel is already open to that section.
+ useEffect(() => {
+ if (!target) return
+ setOpenId(target.sectionId)
+ const raf = requestAnimationFrame(() => {
+ sectionRefs.current[target.sectionId]?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
+ })
+ setFlashId(target.sectionId)
+ const timeout = setTimeout(() => setFlashId(null), FLASH_DURATION_MS)
+ return () => {
+ cancelAnimationFrame(raf)
+ clearTimeout(timeout)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [target?.nonce])
return (