Trim comments across Rust and webapp source to direct one-liners (ticket 030)

Pass over src/*.rs, webapp/src/**, and the in-app "?" docs prose:
condensed multi-paragraph doc comments to single lines that state the
non-obvious why, deleted comments that only restated adjacent code, and
tightened the Docs.tsx help-panel wording. No logic, structure, or code
formatting changed — verified via cargo test (143 passed) and
tsc -b && vite build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
Hugh Kaznowski
2026-09-05 17:02:33 +01:00
co-authored by Claude Sonnet 5
parent 3cef6dc8ae
commit 00c21c8032
32 changed files with 347 additions and 1550 deletions
+65 -77
View File
@@ -1,9 +1,7 @@
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
/// markdown renderer for what is currently five static pages.
/// One accordion section: title plus body as plain JSX, avoiding a markdown renderer for these static pages.
interface DocSection {
id: string
title: string
@@ -21,16 +19,15 @@ const SECTIONS: DocSection[] = [
body: (
<div className="flex flex-col gap-2">
<p>
ottyl turns a CLI program's stdout into a Grafana-like dashboard: pipe a program's output into it, and it
indexes each line in memory as it arrives, then serves a webapp dashboard over the same process.
ottyl turns a CLI program's stdout into a Grafana-like dashboard. Pipe a program's output into it, and it
indexes each line in memory as it arrives while serving a dashboard from the same process.
</p>
<p>
<Code>{'cli_command --verbose | ottyl'}</Code>
</p>
<p>
Everything — ingested log lines, declared params, declared metrics, and derived buckets — lives in one
in-memory index for the life of the process. Nothing is written to disk unless you explicitly save a{' '}
<em>profile</em> or change a <em>setting</em>.
Everything — log lines, params, metrics, and derived buckets — lives in one in-memory index for the
process's lifetime. Nothing touches disk unless you save a <em>profile</em> or change a <em>setting</em>.
</p>
<p className="font-semibold text-text">CLI arguments</p>
<ul className="list-disc pl-5">
@@ -47,19 +44,17 @@ const SECTIONS: DocSection[] = [
</ul>
<p className="font-semibold text-text">Settings</p>
<p>
There is exactly one <em>settings</em> file per machine, holding the colour scheme, any imported/linked CSS,
and which profile is active. Settings "owns" profiles in that sense — it only tracks pointers (known
profile names, which one is active); the profiles themselves (params, metrics, dashboards) are stored
separately, next to the settings file by default.
There is one <em>settings</em> file per machine, holding the colour scheme, any imported/linked CSS, and
which profile is active. Settings only tracks profile pointers — the profiles themselves (params, metrics,
dashboards) are stored separately, next to the settings file by default.
</p>
<p className="font-semibold text-text">Profiles</p>
<p>
A profile is a named bundle of parsing/indexing rules for a particular CLI app — which params and metrics
to derive from its log output, plus any saved dashboards — so restarting ottyl against the same program
doesn't lose your setup. There is always a <Code>Default</Code> profile, and it can't be deleted. Creating
a new profile copies the currently active one (or starts empty if none is active), so variations build on
what you already have rather than starting from scratch. Switching profiles hot-swaps the running engine's
declared params/metrics immediately, with no restart needed.
A profile bundles the parsing/indexing rules for one CLI app — its params and metrics, plus any saved
dashboards — so restarting ottyl against the same program doesn't lose your setup. A <Code>Default</Code>{' '}
profile always exists and can't be deleted. Creating a new profile copies the active one (or starts empty
if none is active), so variations build on what you already have. Switching profiles hot-swaps the running
engine's declared params/metrics immediately, with no restart needed.
</p>
</div>
),
@@ -70,16 +65,16 @@ const SECTIONS: DocSection[] = [
body: (
<div className="flex flex-col gap-2">
<p>
The filter bar above the timeline takes a syntax-aware query combining regex search over raw log text with
comparisons against declared metrics, joined by <Code>and</Code>/<Code>or</Code>:
The filter bar takes a query combining regex search over raw log text with comparisons against declared
metrics, joined by <Code>and</Code>/<Code>or</Code>:
</p>
<p>
<Code>{'regex("error") and sequence<5 or timestamp>12:00'}</Code>
</p>
<p>
<Code>{'metric_name[param]<5'}</Code> compares a metric's captured param value against a number, string, or
timestamp. <Code>sequence</Code> and <Code>timestamp</Code> are reserved metric names — every ingested log
line has both, so they're always available to filter on without declaring anything.
<Code>{'metric_name[param]<5'}</Code> compares a metric's captured param against a number, string, or
timestamp. <Code>sequence</Code> and <Code>timestamp</Code> are reserved metric names present on every log
line, so they're always filterable without declaring anything.
</p>
<p className="font-semibold text-text">Date/time formats</p>
<p>Timestamp comparisons accept several forms:</p>
@@ -91,9 +86,9 @@ const SECTIONS: DocSection[] = [
<li>full dates</li>
</ul>
<p>
The same filter string is sent to both <Code>GET /api/logs</Code> and <Code>GET /api/timeline</Code> as the{' '}
<Code>filter</Code> query param and evaluated on the server; a parse error comes back from the server and
surfaces as inline text under the filter bar rather than crashing the page.
The same filter string goes to both <Code>GET /api/logs</Code> and <Code>GET /api/timeline</Code> as the{' '}
<Code>filter</Code> query param and is evaluated on the server; a parse error surfaces as inline text under
the filter bar rather than crashing the page.
</p>
</div>
),
@@ -104,21 +99,20 @@ const SECTIONS: DocSection[] = [
body: (
<div className="flex flex-col gap-2">
<p>
A parameter declaration tells ottyl how to derive a named value from a raw log line: a regex that must
match the message, plus a capture group within it, plus a type (<Code>string</Code>, <Code>number</Code>,
or <Code>timestamp</Code>). Once declared, every already-ingested line gets retroactively indexed for that
param, and every new line is checked against it as it arrives.
A parameter declaration tells ottyl how to derive a named value from a log line: a regex the message must
match, a capture group, and a type (<Code>string</Code>, <Code>number</Code>, or <Code>timestamp</Code>).
Declaring one retroactively indexes every already-ingested line and checks every new line as it arrives.
</p>
<p>
Params are the building block metrics are made of (a metric declaration is really a message-match regex
plus a set of param matchers), and they're also what drives filtering and the log preview's column picker —
declaring a <Code>duration</Code> param, for instance, makes <Code>duration</Code> available as a column in
the log table and as a comparable value in the filter bar.
Params are the building blocks metrics are made of (a metric is really a message-match regex plus a set of
param matchers), and they drive filtering and the log preview's column picker — declaring a{' '}
<Code>duration</Code> param, for instance, makes it available as both a log-table column and a comparable
value in the filter bar.
</p>
<p>
The Params panel declares a bare param directly (<Code>POST /api/params</Code>) — the same request the
Metrics panel's declaration form makes per param matcher. Declaring one there immediately updates the log
preview's column picker, with no page reload needed.
Metrics panel's declaration form makes per param matcher. Declaring one there updates the log preview's
column picker immediately, with no page reload needed.
</p>
</div>
),
@@ -130,14 +124,14 @@ const SECTIONS: DocSection[] = [
<div className="flex flex-col gap-2">
<p>
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.
select a fixed range, narrowing the log table, metrics, and dashboards below to that window.
</p>
<p>
The preset buttons (<Code>All</Code>, <Code>5m</Code>, <Code>15m</Code>, <Code>1h</Code>,{' '}
<Code>24h</Code>) are Grafana-style shortcuts for the same thing: <Code>All</Code> clears the range
entirely, and a relative preset is <em>rolling</em> — its window keeps advancing to stay "last N minutes
from now" rather than freezing at the moment you picked it. Dragging on the timeline while a preset is
active switches back to a fixed (non-rolling) range, same as dragging normally would.
<Code>24h</Code>) are Grafana-style shortcuts: <Code>All</Code> clears the range entirely, and a relative
preset is <em>rolling</em> — its window keeps advancing to stay "last N minutes from now" instead of
freezing at selection time. Dragging while a preset is active switches to a fixed (non-rolling) range, same
as a normal drag.
</p>
</div>
),
@@ -149,8 +143,8 @@ const SECTIONS: DocSection[] = [
<div className="flex flex-col gap-2">
<p>
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.
column picker (top-right) adds a column per declared param, showing that line's derived value (blank if it
didn't match) — a quick way to eyeball params without switching to the Metrics panel.
</p>
</div>
),
@@ -167,20 +161,19 @@ const SECTIONS: DocSection[] = [
</p>
<p>
Each param is either a <strong>label</strong> (the default) or the metric's single <strong>value</strong>{' '}
param. Label params identify which bucket an occurrence belongs to; a value param's captured number becomes
that occurrence's stored value instead of a label — e.g. a <Code>duration</Code> param marked{' '}
<em>value</em> lets you query the distribution or latest reading of durations, not just how often they
occurred. A metric with only label params still works exactly as before: every occurrence's value defaults
to a fixed <Code>1</Code> (a pure occurrence count).
param. Labels identify which bucket an occurrence belongs to; a value param's captured number becomes the
occurrence's stored value instead of a label — e.g. a <Code>duration</Code> param marked <em>value</em>{' '}
lets you query the distribution or latest reading, not just how often it occurred. A metric with only
label params defaults every occurrence's value to a fixed <Code>1</Code> (a pure occurrence count).
</p>
<p>
Declaring a metric backfills it against everything already ingested, then continues indexing new lines as
they arrive — there's no need to restart ingestion or re-declare after the fact.
Declaring a metric backfills it against everything already ingested, then keeps indexing new lines as they
arrive — no need to restart ingestion or re-declare after the fact.
</p>
<p>
The Metrics panel lets you declare a metric, pick one from the declared list, and see its bucket values
grouped by label combination over the active time range — occurrence count per group, plus a small trend
sparkline. That same data is what dashboard panels query via PromQL (see below), including two forms{' '}
sparkline. Dashboard panels query that same data via PromQL (see below), including two forms{' '}
<em>derived</em> from a counter's value param rather than stored separately:
</p>
<ul className="list-disc pl-5">
@@ -188,7 +181,7 @@ const SECTIONS: DocSection[] = [
<span className="font-semibold text-text">
<Code>histogram(metric_name[width])</Code>
</span>{' '}
— buckets every matching event's value into fixed-width buckets of size <Code>width</Code>, e.g.{' '}
— buckets each matching event's value into fixed-width buckets of size <Code>width</Code>, e.g.{' '}
<Code>histogram(durations[10])</Code>.
</li>
<li>
@@ -208,9 +201,9 @@ const SECTIONS: DocSection[] = [
body: (
<div className="flex flex-col gap-2">
<p>
Dashboards work like Grafana: each one is a named set of panels, and each panel is a PromQL query paired
with a chart type — bar, line, or pie. Panels preview live as you edit the query, and a malformed query
shows an inline error instead of breaking the page.
Dashboards work like Grafana: each is a named set of panels, and each panel is a PromQL query paired with a
chart type — bar, line, or pie. Panels preview live as you edit the query, and a malformed query shows an
inline error instead of breaking the page.
</p>
<ul className="list-disc pl-5">
<li>
@@ -218,8 +211,7 @@ const SECTIONS: DocSection[] = [
x-axis.
</li>
<li>
<span className="font-semibold text-text">Bar</span> — the latest value per series (a snapshot of "where
things are now").
<span className="font-semibold text-text">Bar</span> — the latest value per series (a snapshot of "now").
</li>
<li>
<span className="font-semibold text-text">Pie</span> — each series' share of the summed total across the
@@ -227,8 +219,8 @@ const SECTIONS: DocSection[] = [
</li>
</ul>
<p>
Dashboards are saved per profile, so they travel with a profile when you switch or copy it, and require an
active profile to create or save — pick or create one from the settings menu first.
Dashboards are saved per profile, so they travel with it when you switch or copy it — an active profile is
required to create or save one; pick or create one from the settings menu first.
</p>
</div>
),
@@ -239,12 +231,12 @@ const SECTIONS: DocSection[] = [
body: (
<div className="flex flex-col gap-2">
<p>
An alert rule fires a notification when its condition matches a newly-ingested log line — evaluated per
line as it arrives, not backfilled against history. There are four condition types:
An alert rule fires a notification when its condition matches a newly-ingested log line, evaluated per line
as it arrives rather than backfilled against history. There are four condition types:
</p>
<ul className="list-disc pl-5">
<li>
<span className="font-semibold text-text">Immediate</span> — fires once per line matching a query.
<span className="font-semibold text-text">Immediate</span> — fires once per matching line.
</li>
<li>
<span className="font-semibold text-text">Threshold</span> — fires when a query's match count within a
@@ -260,8 +252,8 @@ const SECTIONS: DocSection[] = [
</li>
</ul>
<p>
Query-based conditions (Immediate/Threshold/Ratio) reuse the filter bar's query syntax, but only the subset
that evaluates against a single line's own fields — <Code>regex(...)</Code>, <Code>sequence</Code>,{' '}
Query-based conditions (Immediate/Threshold/Ratio) reuse the filter bar's syntax, but only the subset that
evaluates a single line's own fields — <Code>regex(...)</Code>, <Code>sequence</Code>,{' '}
<Code>timestamp</Code>, and <Code>and</Code>/<Code>or</Code>. A <Code>metric_name[param]</Code> comparison
isn't supported in an alert query.
</p>
@@ -270,10 +262,9 @@ const SECTIONS: DocSection[] = [
<Code>5m</Code>, <Code>1h</Code>.
</p>
<p>
Every rule delivers to an <em>ntfy</em> sink: a topic (required) and an optional server override (defaults
to <Code>https://ntfy.sh</Code>). "Send test notification" delivers to whatever topic/server is currently
entered in the form, without needing to save the rule first — handy for verifying the sink before
committing to it.
Every rule delivers to an <em>ntfy</em> sink: a topic (required) and an optional server override (default{' '}
<Code>https://ntfy.sh</Code>). "Send test notification" delivers to the form's current topic/server without
needing to save the rule first.
</p>
</div>
),
@@ -284,9 +275,9 @@ const SECTIONS: DocSection[] = [
body: (
<div className="flex flex-col gap-2">
<p>
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 Settings panel controls process-wide state: colour scheme, the max number of logs kept in memory
(blank means unlimited), and imported/linked CSS. Changes apply immediately and persist to the settings
file on disk — no save button needed.
</p>
</div>
),
@@ -298,15 +289,14 @@ const SECTIONS: DocSection[] = [
<div className="flex flex-col gap-2">
<p>
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 <em>General idea</em>{' '}
section above for what a profile stores.
params/metrics in the running engine immediately, no restart needed — see <em>General idea</em> above for
what a profile stores.
</p>
</div>
),
},
]
/** How long a flashed section stays highlighted before settling back to normal. */
const FLASH_DURATION_MS = 1200
export function DocsPanel({ target }: { target?: DocsTarget | null }) {
@@ -314,9 +304,7 @@ export function DocsPanel({ target }: { target?: DocsTarget | null }) {
const [flashId, setFlashId] = useState<string | null>(null)
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({})
// 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.
// Keyed on `target?.nonce`, not `target?.sectionId`, so clicking the same "?" icon twice re-triggers the flash instead of no-op'ing when already open.
useEffect(() => {
if (!target) return
setOpenId(target.sectionId)