diff --git a/tickets/058-declared-params-not-refreshed-live-in-ui.md b/tickets/058-declared-params-not-refreshed-live-in-ui.md deleted file mode 100644 index 6a417a5..0000000 --- a/tickets/058-declared-params-not-refreshed-live-in-ui.md +++ /dev/null @@ -1,42 +0,0 @@ -# 058 — Newly auto-declared params don't appear in the UI until a manual refresh - -## Summary - -`auto_declare_params` / `auto_declare_log_stream_params` (`src/engine.rs:383`, -`:451`) can add new param declarations on any ingested line, but the -frontend only ever fetches the param list once, on mount -(`webapp/src/App.tsx:278-284`, `fetchParamDeclarations().then(setParams)`). -The main poll loop (`App.tsx:300-350`, interval from -`webapp/src/lib/polling.ts`) refetches timeline entries and logs every -tick but never re-fetches param declarations — the only other place -`setParams` is called is the `onDeclared` callback fired by a user's own -manual edit in `Params.tsx`. So a param that gets auto-declared mid-run -from freshly ingested lines is invisible in the Params panel until the -page is reloaded. - -There is no push infrastructure (SSE/websocket) anywhere in the -codebase; all live-refresh today is plain polling. - -## Scope - -- Add a periodic (or piggybacked on the existing poll loop) refetch of - param declarations in `App.tsx`, e.g. call `fetchParamDeclarations()` - alongside the existing timeline/log poll in the loop at - `App.tsx:300-350`, or on its own lighter-weight interval if refetching - every tick is wasteful. -- Only update state when the declaration list actually changed, to avoid - needlessly re-rendering `Params.tsx` on every poll tick. -- Out of scope: introducing SSE/websocket push infrastructure — polling - is consistent with how the rest of the app already refreshes. - -## Acceptance criteria - -- Ingesting new lines that trigger auto-declaration of a previously - unseen param causes that param to appear in the Params UI within one - poll interval, without a page reload. -- Existing manual param declaration/edit flows (`Params.tsx`'s - `onDeclared`) are unaffected. - -## Depends on - -None. diff --git a/tickets/062-app-title-should-be-ottyl.md b/tickets/062-app-title-should-be-ottyl.md deleted file mode 100644 index d777e27..0000000 --- a/tickets/062-app-title-should-be-ottyl.md +++ /dev/null @@ -1,24 +0,0 @@ -# 062 — Browser tab title says "webapp" instead of "ottyl" - -## Summary - -`webapp/index.html:7` has `webapp`. Nothing else sets a -user-visible title — no `document.title` assignment exists in -`App.tsx`, and `webapp/package.json`'s `"name": "webapp"` is a separate -dev-tooling field not surfaced to users. The only fix needed is the -`index.html` title tag. - -## Scope - -- Change `webapp` to `ottyl` in - `webapp/index.html`. -- Out of scope: renaming the `webapp` directory or `package.json` name - field — those are internal and not user-facing. - -## Acceptance criteria - -- The browser tab shows "ottyl" when the dashboard is open. - -## Depends on - -None. diff --git a/tickets/063-params-list-drop-redundant-group-suffix.md b/tickets/063-params-list-drop-redundant-group-suffix.md deleted file mode 100644 index 63eb5d5..0000000 --- a/tickets/063-params-list-drop-redundant-group-suffix.md +++ /dev/null @@ -1,38 +0,0 @@ -# 063 — Declared params list shows a redundant "group name" alongside type - -## Summary - -`webapp/src/components/Params.tsx:331-337` renders each param as -`{p.name} ({p.kind}, ...)`, where the third segment is one of `elapsed -since previous`, nothing (for booleans), `json field {path}`, or `group -{p.capture_group}`. There is no distinct "group" concept in the domain -model — `src/dto.rs`, `src/profile.rs`, and `src/engine.rs` have no -`group` field at all. What the UI calls "group" is actually -`capture_group` (`dto.rs:22`, `profile.rs:69`), the regex named-capture -group used to extract the param's value. By convention, params are -declared with `(?P...)`, so `capture_group` is almost always -identical to `p.name` — displaying it is redundant noise in the common -case (`name (type, name)`). - -## Scope - -- Change the default rendering to `name (type)`. -- Only append the extra suffix when it's actually informative: - - Keep `json field {path}` and `elapsed since previous` suffixes — - these carry information not implied by `name`/`type`. - - For the capture-group case, only show it when - `p.capture_group !== p.name` (i.e. the regex group name genuinely - differs from the declared param name); otherwise omit it. -- Out of scope: changing the underlying `capture_group` field or param - declaration model — this is a display-only fix. - -## Acceptance criteria - -- A param whose capture group matches its name renders as `name (type)`. -- A param whose capture group differs from its name still shows that - distinction (e.g. `name (type, group other_name)`). -- `json field` and `elapsed since previous` suffixes are unchanged. - -## Depends on - -None. diff --git a/tickets/064-promql-panel-input-clobbered-by-autosave.md b/tickets/064-promql-panel-input-clobbered-by-autosave.md deleted file mode 100644 index 29a4ff2..0000000 --- a/tickets/064-promql-panel-input-clobbered-by-autosave.md +++ /dev/null @@ -1,48 +0,0 @@ -# 064 — Typing a panel's PromQL query gets clobbered by in-flight autosave responses - -## Summary - -In `webapp/src/components/Dashboards.tsx`, the panel query `` -(`:408-411`) is bound directly to `draft.panels[...].promql` / -`updatePanel`, and `draft` is autosaved via a 600ms-debounced effect -(`AUTOSAVE_DEBOUNCE_MS = 600`, line 34; effect at `:190-214`). On save -success, `setDraft(saved)` (`:203`) unconditionally replaces the entire -local draft with the server's response — including whatever `promql` -string was in flight at request time. If the user types more characters -after the debounce fires but before the save response returns, those -keystrokes exist only in local `draft` state and get overwritten the -instant the stale `saved` response resolves, producing the reported -"characters inserted then removed" behavior. A second effect -(`Dashboards.tsx:184-188`) also resets `draft` whenever `dashboards` -changes — which the same save's `setDashboards` call triggers — -compounding the race. - -## Scope - -- Stop unconditionally overwriting in-flight-edited fields on autosave - response. Options (pick one, document the choice): - - Track which panel/field is actively focused/being edited and skip - overwriting that specific field when reconciling `saved` into - `draft`. - - Only merge server-assigned fields (ids, timestamps) from `saved` - into `draft` rather than replacing `draft` wholesale. - - Guard the reconciliation with a "pending edits since request was - sent" check, applying `saved` only when no newer local edit - occurred after the request was issued. -- Also fix the related staleness effect at `Dashboards.tsx:184-188` so - it doesn't clobber in-progress edits when `dashboards` changes as a - side effect of the same save. -- This does not require adopting Redux or another state library — the - fix is local to this component's save/edit reconciliation. - -## Acceptance criteria - -- Typing continuously in a panel's PromQL input while an autosave - round-trip is in flight never drops or reorders characters. -- Autosave still persists the query to the backend and other - server-assigned fields (ids, etc.) still get reconciled into local - state correctly. - -## Depends on - -None. diff --git a/tickets/065-filter-bar-help-should-describe-filter-syntax-not-regex.md b/tickets/065-filter-bar-help-should-describe-filter-syntax-not-regex.md deleted file mode 100644 index 5b2dd52..0000000 --- a/tickets/065-filter-bar-help-should-describe-filter-syntax-not-regex.md +++ /dev/null @@ -1,41 +0,0 @@ -# 065 — Filter bar's help/prompt foregrounds regex syntax instead of the filter bar's own syntax - -## Summary - -A comprehensive filter-bar syntax reference already exists — operators, -`[param]` comparisons, datetime formats, worked examples — behind the -`` in `webapp/src/components/Docs.tsx:76-140`, -wired up at `FilterBar.tsx:32-34`. The problem is emphasis, not missing -docs: the input's `placeholder` (`FilterBar.tsx:38`) leads with -`regex("error") and ottyl_seq<5 or ottyl_ts>12:00`, making regex look -like the primary syntax, and a second `` (a pure regex -cheat-sheet, `webapp/src/components/RegexHelpIcon.tsx`) sits more -prominently next to the input than the actual filter-syntax help icon. -A user skimming the bar sees "regex" twice before noticing the filter -bar has its own broader query language. - -## Scope - -- Change the input `placeholder` at `FilterBar.tsx:38` to lead with a - non-regex filter-bar example (e.g. - `ottyl_ts>-15m and requests[duration]>200`), keeping a `regex(...)` - clause further in the example rather than first if desired. -- Reorder/re-emphasize the two help icons so the general filter-syntax - `HelpIcon` (→ `Docs.tsx`'s filtering section) reads as the primary - affordance, with the regex-specific cheat sheet secondary/clearly - labeled as being about regex specifically, not the filter bar as a - whole. -- Out of scope: rewriting the content of `Docs.tsx`'s filtering section - — it's already accurate and thorough; this is about surfacing it - correctly, not authoring new documentation. - -## Acceptance criteria - -- The filter bar's placeholder text no longer leads with a bare - `regex(...)` example as the first thing a user sees. -- The general filter-syntax help is at least as discoverable/prominent - as the regex-only cheat sheet. - -## Depends on - -None. diff --git a/tickets/066-filter-bar-value-doesnt-reflect-active-preset-or-sort.md b/tickets/066-filter-bar-value-doesnt-reflect-active-preset-or-sort.md deleted file mode 100644 index cf7540e..0000000 --- a/tickets/066-filter-bar-value-doesnt-reflect-active-preset-or-sort.md +++ /dev/null @@ -1,53 +0,0 @@ -# 066 — Filter bar text doesn't reflect the active time-range preset or sort order - -## Summary - -In `webapp/src/App.tsx`, `filter` (the raw typed filter text, `:264`), -`activePreset`/`range` (`:262-263`, set via `selectPreset` / -`handleManualRangeSelect`, `:286-298`), and `sortSettings` (`:266`) are -three entirely separate pieces of state. -`` (`:355`) only -ever reflects the manually-typed `filter` string — selecting a -quick-pick time-range preset or changing the sort order never touches -`filter`, so the filter bar can show empty/stale text even though a -preset or sort order is actively constraining or ordering what's -displayed. Separately, `FilterBar.tsx:22` -(`const [draft, setDraft] = useState(value)`) initializes its internal -`draft` from `value` only once on mount and never resyncs if `value` -changes externally afterward (e.g. a programmatic clear) — a related -staleness bug worth fixing alongside this. - -## Scope - -- Decide and implement one of: - - Synthesize a read-only textual summary of the active preset/sort - state and display it in or alongside the filter bar when the user - hasn't typed a manual filter (e.g. showing the equivalent - `ottyl_ts>-Xm`-style clause for the active preset). - - Make preset-selection and sort-order actions actually write into - the `filter` query string itself (e.g. `selectPreset` appends/ - updates an `ottyl_ts` clause), so `filter` is always the single - source of truth for what's displayed in the bar. - Document the choice in the PR — this is a real design decision, not - just a bug fix. -- Fix `FilterBar.tsx:22` so the internal `draft` resyncs when the - `value` prop changes externally (not just on mount), so programmatic - filter changes (from whichever approach above, or an external clear) - are reflected in the input. -- Out of scope: changing how presets/sort settings are computed or - stored server-side — this is about surfacing existing state in the - filter bar's displayed value. - -## Acceptance criteria - -- Selecting a quick-pick time-range preset causes the filter bar to - visibly reflect that a time constraint is active (either via a - synthesized display or an actual written filter clause). -- Changing sort order is likewise visible via the filter bar's - displayed state. -- The filter bar's input value stays in sync with externally-driven - changes to the active filter, not just the user's own typing. - -## Depends on - -None. diff --git a/webapp/index.html b/webapp/index.html index faa932c..62dc54a 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -4,7 +4,7 @@ - webapp + ottyl
diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index fa6b5f4..15c00f6 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -254,6 +254,28 @@ function StandaloneRanged({ ) } +/// Ticket 058: only replaces `params` state when the declaration list actually changed, so a param +/// list refetched on every poll tick doesn't force `Params.tsx` to re-render when nothing changed. +function updateParamsIfChanged( + setParams: (updater: (current: ParamDeclaration[]) => ParamDeclaration[]) => void, + next: ParamDeclaration[], +) { + setParams((current) => (JSON.stringify(current) === JSON.stringify(next) ? current : next)) +} + +/// Ticket 066: text shown under the filter bar when the user hasn't typed a manual filter, so an active preset/sort order is still visible somewhere near the filter input instead of leaving it looking empty/stale. +function describeActiveState(activePreset: TimeRangePresetId | null, sortSettings: LogSortSettings): string | null { + const parts: string[] = [] + if (activePreset && activePreset !== 'all') { + const preset = TIME_RANGE_PRESETS.find((p) => p.id === activePreset) + parts.push(`last ${preset?.label ?? activePreset}`) + } + if (sortSettings.sortBy) { + parts.push(`sorted by ${sortSettings.sortBy}`) + } + return parts.length > 0 ? parts.join(', ') : null +} + /// 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([]) @@ -311,7 +333,7 @@ function StandaloneLog() { ) } try { - const [nextBuckets, nextLogs] = await Promise.all([ + const [nextBuckets, nextLogs, nextParams] = await Promise.all([ fetchTimeline({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, @@ -327,10 +349,13 @@ function StandaloneLog() { sortBy: sortSettings.sortBy ?? undefined, sortMissingFallback: sortSettings.sortBy ? sortSettings.sortMissingFallback : undefined, }), + // Ticket 058: params can get auto-declared from freshly ingested lines mid-run, not just from a user's own edit. + fetchParamDeclarations(), ]) if (!cancelled) { setBuckets(nextBuckets) setLogs(nextLogs) + updateParamsIfChanged(setParams, nextParams) setFilterError(null) } } catch (err) { @@ -352,7 +377,12 @@ function StandaloneLog() { return (
- +
setLogViewerCollapsed(!logViewerCollapsed)} /> diff --git a/webapp/src/components/Dashboards.tsx b/webapp/src/components/Dashboards.tsx index bd7cb64..1d138a5 100644 --- a/webapp/src/components/Dashboards.tsx +++ b/webapp/src/components/Dashboards.tsx @@ -153,6 +153,9 @@ export function Dashboards({ range, activePreset }: { range: TimeRange | null; a const [saveError, setSaveError] = useState(null) // Last-persisted shape for the current draft, so the autosave effect can skip when nothing actually changed. const lastSavedRef = useRef(null) + // Ticket 064: latest `draft`, read inside the autosave timeout's async callback so a save response can tell + // whether the user kept typing after the request went out, instead of unconditionally overwriting `draft`. + const draftRef = useRef(draft) const [collapsed, setCollapsed] = useSectionCollapsed('dashboards') async function loadDashboards(profileName: string, selectAfterLoad?: string) { @@ -181,11 +184,21 @@ export function Dashboards({ range, activePreset }: { range: TimeRange | null; a // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + useEffect(() => { + draftRef.current = draft + }, [draft]) + + // Deliberately keyed only on `selectedId`, not `dashboards` — this effect's job is to load a *newly selected* + // dashboard's content into `draft`. Keying it on `dashboards` too meant every autosave's own `setDashboards` + // call (below) re-ran this effect and clobbered whatever the user had typed since the save was kicked off + // (ticket 064). Reading `dashboards` from the closure here still sees the just-loaded array, since + // `setSelectedId`/`setDashboards` from `loadDashboards` land in the same render. useEffect(() => { const found = dashboards.find((d) => d.id === selectedId) ?? null setDraft(found) lastSavedRef.current = found ? JSON.stringify(dashboardSaveShape(found)) : null - }, [selectedId, dashboards]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedId]) // Skips entirely once the draft matches what's already persisted — covers both re-selecting an unchanged dashboard and this effect's own post-save re-render. useEffect(() => { @@ -198,9 +211,14 @@ export function Dashboards({ range, activePreset }: { range: TimeRange | null; a setSaveStatus('saving') try { const saved = await saveDashboard(activeProfile, dashboardId, dashboardSaveShape(draft)) - lastSavedRef.current = JSON.stringify(dashboardSaveShape(saved)) + lastSavedRef.current = snapshot setDashboards((prev) => prev.map((d) => (d.id === saved.id ? saved : d))) - setDraft(saved) + // Only reconcile `saved` into `draft` if nothing newer was typed while the request was in flight — + // otherwise this stale response would overwrite those in-progress keystrokes (ticket 064). + const current = draftRef.current + if (current && current.id === dashboardId && JSON.stringify(dashboardSaveShape(current)) === snapshot) { + setDraft(saved) + } setSaveStatus('saved') setSaveError(null) } catch (err) { diff --git a/webapp/src/components/FilterBar.tsx b/webapp/src/components/FilterBar.tsx index d7418dc..b472e37 100644 --- a/webapp/src/components/FilterBar.tsx +++ b/webapp/src/components/FilterBar.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { HelpIcon } from './HelpIcon' import { RegexHelpIcon } from './RegexHelpIcon' @@ -13,14 +13,22 @@ export function FilterBar({ onChange, error, collapseToggle, + activeSummary, }: { value: string onChange: (value: string) => void error?: string | null collapseToggle?: ReactNode + /** Ticket 066: a read-only textual summary of the active time-range preset / sort order, shown below the input when the user hasn't typed a manual filter — the filter text itself stays the single source of truth for what's actually sent to the server. */ + activeSummary?: string | null }) { const [draft, setDraft] = useState(value) + // Resync when `value` changes externally (e.g. a programmatic clear) rather than only reading it once on mount. + useEffect(() => { + setDraft(value) + }, [value]) + return (
- - + + + Filter syntax + {collapseToggle} setDraft(e.target.value)} /> + + + regex(...) +
{error &&

{error}

} + {!error && !draft && activeSummary &&

Active: {activeSummary}

}
) } diff --git a/webapp/src/components/Params.tsx b/webapp/src/components/Params.tsx index 2765cc7..e03c67b 100644 --- a/webapp/src/components/Params.tsx +++ b/webapp/src/components/Params.tsx @@ -333,7 +333,11 @@ export function Params({ params, onDeclared }: { params: ParamDeclaration[]; onD ? ', elapsed since previous' : p.kind === 'boolean' ? '' - : `, ${p.json_field_path ? `json field ${p.json_field_path}` : `group ${p.capture_group}`}`} + : p.json_field_path + ? `, json field ${p.json_field_path}` + : p.capture_group !== p.name + ? `, group ${p.capture_group}` + : ''} ) ))}