- 076: gate auto-declared log-stream column/message params
(`ParamDeclaration::log_stream_gated`) behind `is_log_stream_line` at
extraction time, since their fixed-offset regex has no content anchor
and previously extracted garbage from continuation lines and the
header row itself (the regex crate doesn't support lookaround, so
this couldn't be baked into the regex text).
- 074: end-to-end regression test confirms sorting a real `log stream`
capture by a log-stream column (e.g. PID) now actually reorders
results, since 076's fix removes the garbage values that were
corrupting the sort helper index; also add a "Building sort
index…" indicator in the timeline UI for a slow first-time sort.
- 072: attach the reserved `ottyl_fmt` pseudo-param to each
`LogEntryDto`'s params so it can be picked as a Log Preview column
(previously listed as pickable but always blank).
- 073: track a rolling 5s high-watermark of ingestion-to-engine-processing
lag and surface it in the nav stats popover next to approx_bytes_total.
- 071: group the regex(...) help button back with the filter bar's other
controls, and restructure the Filtering docs section into scannable
per-feature subsections (also fixes stale log_type/process/pid mention).
Tickets 070 (CLI scoping) and 075 (API unresponsiveness investigation)
are left in place per their own scope — both are explicitly
conversation/investigation tasks, not implementation tickets.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1ZJ3SrayzLzVndhVq9BmS
- 059: log stream header/column detection is now generic
whitespace-columnar splitting (2+ spaces, single tab) instead of one
fixed six-column regex. Declared params come from whatever columns
the header actually names, plus a trailing unlabelled message field
that's never swallowed by the last named column.
- 060: timeline sort-by-property now walks an incrementally-maintained
per-sort-key helper index (BTreeMap keyed so "has the param, ordered
by value" sorts before "missing it, ordered by ottyl_seq") instead of
materializing and re-sorting the full filtered range on every query.
- 061: helper indexes are swept for staleness on ingest and dropped
after 5 minutes unused; a purged (or never-built) index transparently
rebuilds via the same lazy one-time scan on the next sorted query.
- Also invalidate a param's helper index on manual re-declaration,
removal, or ClearParams, so a redefined regex can't leave a sort
helper index serving values derived under the old declaration.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1ZJ3SrayzLzVndhVq9BmS
- 058: poll loop now refetches param declarations so auto-declared
params appear live without a page reload
- 062: browser tab title says "ottyl" instead of "webapp"
- 063: declared params list omits the redundant capture-group suffix
when it matches the param name
- 064: dashboard autosave no longer clobbers in-flight PromQL edits;
reconciliation is guarded against edits made after the save request
was sent
- 065: filter bar placeholder/help icons lead with filter-bar syntax
instead of regex
- 066: filter bar surfaces the active time-range preset/sort order and
resyncs its draft when the value prop changes externally
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1ZJ3SrayzLzVndhVq9BmS
A rate() series over a metric whose occurrences are confined to a short
burst within a much wider queried range (e.g. sum by (code)
(rate(KeyPressed[5m])) against a ~10h log where all key presses happen
in the first 10s) rendered as broken: the line simply stopped at the
last real occurrence, and the chart's x-axis auto-scaled to the tiny
span of actual data points rather than the queried range, silently
narrowing a 10-hour view down to ~10 seconds with no indication anything
was off.
Backend (src/engine.rs): windowed_rate_series now appends a synthetic
boundary point at start_ms/end_ms (computed via the existing
rate::rate_at against the original occurrence points, so a boundary
inside a still-warm window gets its true partial value rather than a
hardcoded 0) whenever the real series doesn't already reach that edge.
Wired into both the bare rate() and sum()/avg(rate()) evaluate_promql
branches, which already had start_ms/end_ms in scope.
Frontend (PanelChart.tsx/Dashboards.tsx): the line chart's XAxis now
uses an explicit numeric domain pinned to the panel's queried range
(passed down from Dashboards.tsx's PanelPreview) instead of recharts'
default auto-scale-to-data-extent, so sparse data can no longer make
the axis look narrower than what was actually queried.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
Resolved conflicts with ticket 051's unknown-metric guard in evaluate_promql
(reordered so ottyl_fmt's categorical rejection and the reserved-name
skip both run before 051's declaration/bucket-data check, since
ottyl_seq/ottyl_ts are synthesized from the log index and never have a
MetricDeclaration or bucket entries) and merged both tickets' additions
to the reserved-params Docs.tsx paragraph.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
auto_declare_params (src/engine.rs) previously never overwrote or pruned a
declaration, so a burst of unusual key=value-shaped lines could permanently
inflate the params UI forever. A real macOS `log stream` dump
(huig_dump.txt, copied into src/fixtures/log_stream_sample.txt) discovered
220 distinct auto-declared params from under 700 lines, mostly one-off
WiFi/radio stat counters (20+ pairs on a single line), plus base64-blob
false positives from key()'s greedy alnum matching landing right before
`==` padding.
Fix, combining two bounds per the ticket's options:
- src/logfmt.rs: extract_pairs/pair now rejects a candidate pair whose
bareword value starts with a bare '=' (looks_like_base64_padding_artifact)
— the exact shape produced when a base64/hash blob's padding sits right
after a run of alnum chars that key() consumed as if it were a key.
- src/engine.rs: auto_declare_params now enforces a per-line cap
(MAX_NEW_AUTO_PARAMS_PER_LINE = 8) on new params from one line, and a
total ceiling (MAX_AUTO_DECLARED_PARAMS = 64) on auto-derived params
overall — the per-line cap alone doesn't bound unbounded key *variety*
across many lines, so both are needed.
Also adds structured extraction for log stream's fixed-column shape
(Timestamp Thread Type Activity PID TTL, then `process: message`):
detect_format gains a new "log_stream" ottyl_fmt value (checked first,
since it's the most specific of the four), and
auto_declare_log_stream_params declares log_type/process/pid as standalone
queryable params the first time such a line is seen — derived positionally
from the column layout rather than =-scanning, so there's no ambiguity or
false-positive risk. This is a separate always-attempted extraction
alongside the existing key=value pass, not a replacement for it: a log
stream line's message tail can still contain real key=value pairs (e.g.
the WiFi stat lines) that auto_declare_params keeps discovering
independently. This also fully addresses ticket 054, confirming huig_dump.txt
is space-padded fixed-width columns, not TSV, and that no separate TSV
format is needed.
Throughput (src/fixtures/log_stream_sample.txt x50, release build):
before (caps disabled) ~369k lines/sec, 206 auto-declared params;
after (caps enabled) ~1.20M lines/sec, 64 auto-declared params — about
3.2x faster, confirming the ever-growing declaration set was the
bottleneck rather than per-line regex/nom scanning cost.
Removes tickets/050 and tickets/054 now that both are implemented.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
evaluate_promql previously resolved every metric name through bucket_values,
which only returns data for a real MetricDeclaration — so a dashboard panel
querying ottyl_seq/ottyl_ts/ottyl_fmt (already supported in the filter bar
and alert conditions) silently returned an empty series. ottyl_seq/ottyl_ts
are now synthesized directly from the ingested-log index instead of requiring
a declaration; ottyl_fmt is categorical (json/logfmt/none) with no sensible
numeric reading, so it's rejected up front with a clear error pointing back
at the filter bar rather than returning nothing.
GET /api/params now also lists the three reserved names (marked `reserved:
true`, distinct from `auto_derived`) purely for discoverability — merged in
at the API layer only, not the engine's internal ParamDeclarations query, so
profile-switch removal and metric-param-promotion logic are unaffected.
declare_param also now rejects declaring a param under one of these names.
Docs.tsx and the Params panel are updated to reflect the new dashboard
support and the reserved/non-editable listing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
Wires the engine's existing RemoveMetricDeclaration message (previously
only used internally by delete_profile's cleanup) up to a new DELETE
route, persists the removal into the active profile so it survives a
restart, and adds a delete affordance in the Metrics UI.
Decisions:
- Bucket-value history under the deleted metric's name is purged, not
left orphaned (engine.rs's new remove_metric_declaration reuses
update_metric's clear_metric_data helper). This is what lets a
dashboard panel or PromQL query still referencing the metric by name
fail with a readable "unknown metric" error instead of silently
returning stale data.
- evaluate_promql now errors when a metric has neither a declaration nor
any bucket data, since a metric ingested via --metrics-url scraping
never gets a MetricDeclaration but must stay queryable. Verified with
a seeded fixture (promql_query_against_deleted_metric_fails_with_readable_error
in src/api.rs).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
DELETE /api/params clears the entire standalone-param set — manual and
auto-derived alike — then immediately re-runs auto_declare_params against
every IndexKey::IngestedLog entry still held in the engine's index (oldest
to newest), so params come back fresh from currently-retained log text
without requiring new lines to be ingested. This is a deliberate full
reset, unlike reload_engine_declarations's profile-switch cleanup, which
preserves auto-derived params since those are engine-discovered facts
rather than profile state; here the immediate re-extraction step makes
clearing them safe.
Settings::clear_param_declarations empties the active profile's persisted
params so they don't reappear on restart. The webapp's Params section gets
a single "Clear all" button (with a confirm dialog and a status line
reporting removed/re-derived/logs_considered) rather than a per-param
delete loop; logs_considered surfaces that lines already evicted by
max_logs can't regain their params.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
Extends ticket 048's light_green pattern (light-inverted surface/border/text
scale + light's neutral danger colours, same base and accent hues as the
dark-surfaced counterpart) to the remaining five colour schemes across all
six files that must stay in sync: index.css, themes.ts, api.ts, settings.rs,
useSettingsPanel.ts, and NavMenu.tsx.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
The "Sort by" / "fall back to ottyl_seq" control now lives in
LogTimeline's header next to the time-range presets, since the setting
actually drives the shared range/log fetch rather than being a
Log-Preview-only concern. LogSortSettings and its load/save helpers
moved into LogTimeline.tsx alongside SortPicker; the localStorage key
was renamed to ottyl.logTimeline.sort, with loadLogSortSettings falling
back to the old ottyl.logPreview.sort key so an existing choice isn't
lost on first load after the move. LogPreview now just renders logs in
whatever order the caller already sorted them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
There is no DELETE route, no persisted-removal path, and no UI affordance
for removing a metric once declared — the engine's RemoveMetricDeclaration
message only ever fires as part of whole-profile deletion today.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
worktree-agent-a19b9ef959dcda6b2 (ticket 049) was branched from before "Auto
derived format" (1a481d3) landed on main, so its synchronous
persist_metric_declaration(_update) and ProfileStore::record_metric_declaration(_update)
didn't know about promoting auto-derived engine params into the persisted
profile. This merge reconciles the two:
- src/api.rs: kept the async persist_metric_declaration/_update methods and
promoted_param_records helper from "Auto derived format", but switched their
persistence from ProfileStore::load/record_metric_declaration to the new
Settings-based settings.record_metric_declaration(_update), matching ticket
049's single-settings.json model. Dropped the now-unused
active_profile_location helper in favor of locking self.settings directly.
- src/profile.rs: removed the standalone ProfileStore struct/impl entirely
(049 folds profiles into Settings), keeping load_legacy_profile_store for
migrating a pre-049 profiles.json. Extended record_metric_declaration and
record_metric_declaration_update (now methods on Settings) to keep taking
promoted_params: &[ParamDeclarationRecord], persisting promoted auto-derived
params alongside the metric via Settings::save. Moved the ticket 037/038
legacy-shape migration (migrate_legacy_metric_params /
migrate_duration_value_params) out of the removed ProfileStore::load into a
new pub(crate) migrate_legacy_profile_shapes(profiles), invoked from
Settings::load (via a new Settings::migrate_legacy_profile_shapes) so
profiles inlined in settings.json still get migrated forward on every load.
Rewrote the ProfileStore-based tests (legacy-shape migration tests, the
no-op-migration test, and the auto-derived-param promotion round-trip test)
to exercise Settings directly, including a write_legacy_settings_json helper
that replaces the old write_legacy_profiles_json.
- src/api.rs tests: rewrote declaring_metric_against_auto_derived_param_promotes_it_into_the_profile
to create/activate the profile via settings_handle.lock() and reload via
Settings::load instead of ProfileStore.
Also fixed one clippy::collapsible_if warning introduced by the new
Settings::migrate_legacy_profile_shapes.
cargo build, cargo test (222 passed, 0 failed, 0 ignored), and
cargo clippy --all-targets all pass; no new clippy warnings beyond
pre-existing ones in files untouched by this merge.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
Removes the "Use as theme" checkbox/is_theme distinction from
NavMenu.tsx's CSS import and link forms — every imported/linked
stylesheet is now always validated against the canonical --c-*
variable set and becomes a selectable theme, same as today's
is_theme:true path. Drops is_theme entirely from the backend
(CssEntry, ImportCssRequest, LinkCssRequest, CssEntryDto) and from
the frontend CssEntry type; an existing settings.json with a stored
is_theme:false entry still deserializes fine (the field is simply
unknown/ignored) and that entry becomes selectable like any other,
so no migration step is needed. Also drops the now-dead
always-on-overlay CSS injection path from useSettingsPanel.ts, and
changes the CSS filename placeholder from "name.css" to "enter name".
Implemented directly against main rather than merging the dispatched
worktree branch for this ticket, since that branch's base predated
ticket 012 (the custom-themes/imported-CSS consolidation) and its
NavMenu structure no longer matched current main.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
Resolves conflicts with tickets 006 (header-click collapse) and 009
(all-dashboards print view) in CollapsibleSection.tsx, Dashboards.tsx,
and Metrics.tsx: dropped a spurious onToggle addition from the
worktree's stale base that duplicated already-merged header-collapse
behavior, keeping only the activePreset plumbing; wired activePreset
through Dashboards' non-empty-state header too, which the source
branch (based on a pre-print-view commit) never had to touch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
Resolves conflicts with ticket 018 (boolean param kind) in
src/engine.rs, webapp/src/lib/api.ts, and Params.tsx: boolean-kind
params and JSON-field-path extraction now coexist as independent
extraction modes alongside regex capture groups.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
RegexToMetricPrimitiveMatcher and the standalone param declaration
(DeclareParamRequest/DataValue::ParamDeclaration) gain an optional
json_field_path, an alternative to capture_group that reads a
dot-separated field path (e.g. "foo.bar") out of lines that parse as
JSON objects. It's additive and serde-defaulted, so existing
regex-based params/metrics are unaffected. Params.tsx now offers a
"JSON field" extraction option when the message match regex looks
like a JSON object.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
Extends the FilterBar query language with a json[field] <op> value
comparison, parsed by filter_parser.pest/rs alongside metric[param],
and evaluated in engine.rs by parsing each line as JSON at query time
(lines that aren't valid JSON, or lack the field, simply don't match).
Also wired into alerts.rs since json[field], like regex/sequence/
timestamp, only needs the line's own text.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
LogPreview.tsx now detects when a log line's raw text parses as a JSON
object and renders it as a collapsible tree instead of raw text; any
other line (non-JSON or invalid JSON) keeps today's plain-text
rendering unchanged. Client-side only, no backend changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
Adds a drag-to-resize handle between the log viewer and the sections
below it in the main layout, addressing the log view being too small
with no way to enlarge it. Height is tracked in state, clamped to a
sane range, and persisted to localStorage (ottyl.logViewerHeightPx)
following the app's existing per-viewer UI-state persistence pattern
(see CollapsibleSection.tsx, LogPreview.tsx).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
Adds an "All dashboards" selector alongside the existing click-to-select
single-dashboard view. Selecting it stacks every dashboard vertically in
normal document flow, each preceded by a heading naming it, with no
fixed-height or scroll-clipping containers, so the browser's native print
function can render the full stack across pages.
Verified in-browser: single-dashboard editing still works unchanged, and
"All dashboards" renders all dashboards (including an empty one) stacked
with named separators; confirmed via computed layout that only the
pre-existing app-level scroll container grows to fit the content, with no
clipping container inside the Dashboards section itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o