Commit Graph
100 Commits
Author SHA1 Message Date
Hugh Kaznowski 738e90c45c Tickets for FE and actor split 2026-09-07 14:54:21 +01:00
Hugh KaznowskiandClaude Sonnet 5 920629dbad Add a deterministic profile-actor wait primitive for tests (ticket 108)
Ticket 108 anticipated a large test-harness migration (primary-only/
profile-only helpers, migrating ~150 tests in engine/tests.rs), but
that turned out unnecessary: the profile actor reuses EngineSnapshot
as its own snapshot type (see ticket 104), so every existing
EngineSnapshot unit test already exercises primary-shaped and
profile-shaped behavior unchanged -- zero migration needed, confirmed
by the full suite passing untouched through tickets 100-107.

What was still a real gap: tests asserting on profile-actor-owned data
(declared metrics, alerts) after ingesting had no deterministic way to
wait for the profile actor to catch up -- EngineHandle::barrier() only
confirms the primary's queue drained, not that the profile actor has
processed what it published. They were passing on scheduler luck on
the single-threaded test runtime, not a real guarantee.

Added ProfileHandle::processed_sequence, a permanent (not test-only)
watch channel tracking the profile actor's watermark, mirrored onto
EngineHandle as profile_processed_sequence, with a
wait_for_profile_sequence(sequence) test helper built on it. This
replaces the ad hoc on_advance/spawn_observed mechanism ticket 103
added just to prove the wake loop worked -- that test now uses the
same real primitive. Retrofitted it into
promql_query_returns_series_for_seeded_fixture to eliminate its
latent race.

Not done: a dedicated test for the accepted eviction/lag tradeoff
(primary evicts on schedule regardless of profile-actor progress, per
idea 0001's resolved decision) -- deterministically forcing that race
without new test-injection hooks proved more involved than the
remaining scope justified; left as a known gap rather than shipping a
flaky or synthetic test for it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 14:04:26 +01:00
Hugh KaznowskiandClaude Sonnet 5 9322aac7e1 Route bare ottyl_seq/ottyl_ts Promql queries to the primary (ticket 107)
Audited every EngineQuery variant for the cross-snapshot join ticket
107 anticipated: Logs/Timeline/Stats/ParamDeclarations only ever read
primary data (raw text + param declarations, which never left the
primary), and MetricDeclarations/BucketValues/Promql only ever read
profile data -- no variant actually needs both, so no join/fan-out
layer is needed after all.

One real gap surfaced by that audit: a bare ottyl_seq/ottyl_ts Promql
query is evaluated by iterating self.logs_range() (evaluate_promql's
reserved-metric branch), but such queries were routed to the profile
actor's snapshot alongside real declared metrics -- and the profile
actor stores no raw log rows, so these queries silently returned empty
series instead of real data. EngineHandle::ask now parses the query
just far enough to check its metric name and routes ottyl_seq/ottyl_ts
to the primary instead, where evaluate_promql already works correctly
against real log data. Added an actor-level integration test
(promql_bare_ottyl_seq_works_through_the_real_actor_pipeline) that
fails without this fix -- verified by temporarily disabling the
routing check and confirming the test catches it.

Also deduplicated config/profile.rs's local `ask` test helper, which
had already once diverged from api::EngineHandle::ask's routing logic
and caused ticket 104 test failures (fixed then, but the duplication
itself was the root cause) -- it now delegates to the real one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 14:01:29 +01:00
Hugh KaznowskiandClaude Sonnet 5 3ee6dddb0e Document EngineHandle's already-realized primary/profile routing (ticket 106)
Ticket 106 asked for a real way for API handlers to address the
primary vs. profile actor. That routing was already built out of
necessity while landing tickets 104/105 (metric queries had to reach
the profile actor's snapshot for those tickets' own tests to pass) --
EngineHandle ended up as one combined handle (option (b) from the
ticket's own scope note) that internally proxies metric/alert writes
to the profile actor and routes MetricDeclarations/BucketValues/Promql
reads to its snapshot, with no changes needed to any API handler,
reload_engine_declarations/activate_profile, or Profile::load_into_engine
-- they already just send EngineMessages the same way they did before
the split. mcp_server.rs talks to the HTTP API rather than EngineHandle
directly, so it needed no changes either. This commit documents that
resolution in handle.rs so a future reader doesn't go looking for
routing logic that isn't there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 13:56:59 +01:00
Hugh KaznowskiandClaude Sonnet 5 3751508de8 Move alert evaluation to the profile actor (ticket 105)
Alert rules are profile-scoped and read metric buckets, which now live
on the profile actor (ticket 104) rather than the primary. alert_states
and the per-line evaluate_alerts/deliver_for_rule calls move from the
primary's Ingest arm to the profile actor's per-line loop, evaluated
against its own snapshot right after that line's apply_all_metrics
publishes -- matching the primary's original per-message publish-then-
evaluate ordering. The primary's run() no longer needs a SettingsHandle
at all now that alerts are gone from its Ingest arm; settings (for
alert rules and ntfy config, read fresh per line) moves to
engine::profile::spawn instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 13:55:51 +01:00
Hugh KaznowskiandClaude Sonnet 5 ce3e741cb0 Move metric declarations and bucket computation to the profile actor (ticket 104)
Metric declare/update/remove, backfill, and per-line apply_all_metrics
now run on the profile actor (engine::profile) instead of inline in the
primary's ingest closure, per idea 0001's resolved decision that the
primary handles params only. The profile actor reuses EngineSnapshot as
its own snapshot type -- declare_metric/update_metric/apply_all_metrics
work unchanged -- and mirrors the primary's param declarations into its
own snapshot on every wake (and synchronously before handling a
DeclareMetric/UpdateMetric message, since there's no ordering guarantee
between the watch-wake and message branches in tokio::select!) so
metric label/value resolution keeps working without storing a second
copy of raw log text; backfill reads lines through its cloned primary
IndexReader instead.

EngineHandle gained a profile_reader field so MetricDeclarations/
BucketValues/Promql queries route to the profile actor's snapshot
instead of the primary's -- without it, metrics could be declared but
never queried, since the primary no longer computes or stores them.
DeclareMetric/UpdateMetric/RemoveMetricDeclaration messages are still
sent to the primary's mpsc channel and proxied to the profile actor by
handle.rs's run() loop; EngineHandle's write-side API is otherwise
unchanged (ticket 106 gives API handlers a direct route). Alert
evaluation still reads the primary's (now metric-less) snapshot
directly -- an accepted gap until ticket 105 moves it, per idea 0001's
resolved decision that alerts move with metric computation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 13:53:06 +01:00
Hugh KaznowskiandClaude Sonnet 5 033e8bbab5 Stand up the profile actor skeleton and primary->profile watch signaling (ticket 103)
Adds a second actor task with its own left_right-backed ProfileSnapshot
and a tokio::sync::watch the primary's IndexWriter bumps after every
publish, so the profile actor wakes and re-reads newly-visible primary
lines through its own cloned IndexReader instead of polling. No metric
computation, alert evaluation, or query routing moves yet -- this is
pure plumbing, proven with a test that ingests a line on the primary
and observes the profile actor's watermark advance in response.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 13:39:19 +01:00
Hugh KaznowskiandClaude Sonnet 5 2c1aaa6e9c Break ticket 102's actor split into reviewable sub-tickets (103-108)
Ticket 102 (splitting the engine into reader/primary/profile actors)
was a single multi-thousand-line ticket covering a new actor
topology, an EngineHandle API redesign, a query fan-out layer, moving
alerts, and a full test-harness rewrite, with some design decisions
deliberately left open. Splitting it into six sequenced tickets
(profile actor skeleton + signaling, move metric computation, move
alert evaluation, split the handle API, query fan-out, test harness
rework) lets each land as its own reviewable commit instead of one
large speculative rewrite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 13:33:01 +01:00
Hugh KaznowskiandClaude Sonnet 5 756e054527 Extract param extraction into its own EngineSnapshot::ingest_params stage (ticket 101)
Raw storage, log-stream header derivation, and auto-param detection
were inlined directly in the ingest actor's write closure alongside
metric computation. Pulling them into their own method gives the
ingest arm two clearly separated, independently-callable stages —
param extraction (ingest_params) and metric computation
(apply_all_metrics) — with no behavior change, in preparation for
splitting these onto separate actors (ticket 102).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 13:27:49 +01:00
Hugh KaznowskiandClaude Sonnet 5 f43445b280 Arc-ify stored log text to remove per-line double clones (ticket 100)
DataValue::LogLiteral and IngestedLine now carry an Arc<str> instead
of an owned String, so the two clones the ingest path paid per line
(once to move text into the write closure, once to store it) become
cheap refcount bumps instead of full string copies. Also cuts the
per-string-content cost of left_right's full-EngineSnapshot deep clone
on each reconciliation sync.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 13:26:03 +01:00
Hugh Kaznowski 5fee6909f2 Update actor split idea file 2026-09-07 13:14:19 +01:00
Hugh Kaznowski c7173cf9dc Update agents from repowise 2026-09-07 12:37:07 +01:00
Hugh KaznowskiandClaude Sonnet 5 dbe2ea9961 Add histogram_quantile() PromQL query for percentiles
The PromQL layer only had a fixed-width histogram() (per-bucket
counts), with no way to ask for p50/p95/p99. Adds
histogram_quantile(p, metric[range]), interpolating over each
series's retained raw sample values (linear interpolation between
closest ranks) rather than building a separate pre-aggregated
bucketed-histogram representation — exact for the data actually kept.
An optional bracketed range narrows the computation to a trailing
window, mirroring rate()'s range selector.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 12:34:00 +01:00
Hugh KaznowskiandClaude Sonnet 5 4a0fe92ce8 Add gauge/counter semantics to metric declarations (ticket 097)
Metrics previously only declared a data type (String/Number/...), so
rate() could be applied to a point-in-time gauge reading and sum()/avg()
could be applied directly to a cumulative counter reading with no guard
against either. Adds a MetricSemantics enum (Gauge, default; Counter)
threaded through DeclareMetricRequest, the engine's MetricDeclaration
data value, profile persistence, the API/MCP declaration routes, and the
Metrics panel's declaration form, then uses it in evaluate_promql to
reject rate() on a declared gauge and bare sum()/avg() on a declared
counter (suggesting rate() first). The guard only applies to metrics
with a captured value_param — bare occurrence-count metrics are always
safe under both operations, so existing dashboards are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 12:30:06 +01:00
Hugh KaznowskiandClaude Sonnet 5 c3108677ce Adopt rustfmt and clean up clippy warnings
Formats the whole backend to rustfmt defaults and fixes all 23
outstanding clippy warnings (collapsible ifs, clone-to-slice-ref,
type-complexity, single-arm match, unnecessary is_none check), then
wires cargo fmt --check and -D warnings into make lint so both stay
enforced going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 12:21:38 +01:00
Hugh KaznowskiandClaude Sonnet 5 2cc8ac8138 Default JSON log-line viewer nodes to collapsed
A log line whose text parses as JSON was rendering every nested node
fully expanded, dumping large payloads into the timeline instead of a
collapsed summary the user opts into expanding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 12:19:47 +01:00
Hugh Kaznowski 49bfd6b020 Add tickets and ideas 2026-09-07 12:16:36 +01:00
Hugh KaznowskiandClaude Sonnet 5 0c41048e0f Split lib/api.ts by REST domain and add buildSearchParams helper
api.ts had grown to 35 exported symbols in one 432-line file covering
every backend endpoint. Split into webapp/src/lib/api/{shared,logs,
params,metrics,dashboards,alerts,settings}.ts, re-exported from
lib/api/index.ts as a barrel so none of the ~10 importing files needed
their import paths changed (TS/bundler module resolution falls back to
a directory's index.ts when the sibling .ts file is gone).

Also added buildSearchParams to replace fetchLogs/fetchTimeline's
repeated hand-written `if (x !== undefined) search.set(...)` blocks.
Left fetchMetricBucketValues/fetchPromqlSeries's query-building
untouched (byte-for-byte) since the ticket scoped the helper to
fetchLogs/fetchTimeline only.

Reimplemented directly against current main (ticket 089) after the
worktree agent's branch was based on a stale pre-theme-removal state
and reintroduced CSS-upload endpoints main had deliberately deleted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 10:54:47 +01:00
Hugh Kaznowski f323847748 Merge ticket branch t092 2026-09-07 10:47:01 +01:00
Hugh Kaznowski fc2f511d38 Merge ticket branch t090 2026-09-07 10:47:01 +01:00
Hugh KaznowskiandClaude Sonnet 5 8b2d512c94 Extract shared useRangeSelection hook from App.tsx's three page variants
StandaloneRanged, StandaloneLog, and FullApp each reimplemented the same
range/preset state machine (range/activePreset state, a rangeRef synced
via effect, selectPreset/handleManualRangeSelect, and rolling-preset
re-resolution at the top of the poll loop). Move that bookkeeping into
webapp/src/lib/useRangeSelection.ts; each component keeps only its own
polling logic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 10:46:30 +01:00
Hugh KaznowskiandClaude Sonnet 5 28689b1b8d Extract paramBadgeSuffix helper from nested ternary chain
Replace the 4-deep nested ternary building each declared-param badge's
suffix text with a small if/else-if helper (paramBadgeSuffix), called
from the badge JSX. No behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 10:44:24 +01:00
Hugh Kaznowski 1014aeaf68 Merge ticket branch t087 2026-09-07 10:41:34 +01:00
Hugh KaznowskiandClaude Sonnet 5 8008e5611e Extract shared SectionHeader and FormFooter components (ticket 087)
Params/Metrics/Alerts/Dashboards each hand-assembled the same collapsible-
section header wiring (role/tabIndex/aria-expanded/onClick/onKeyDown around
CollapseToggle+HelpIcon+PopOutButton), and Dashboards duplicated it a second
time for its no-active-profile branch. Params/Metrics/Alerts also duplicated
the declare/edit form footer (editing banner, submit label toggle, Cancel
button). Added SectionHeader and FormFooter to CollapsibleSection.tsx and
adopted them at all five call sites; pure extraction, no behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 10:41:17 +01:00
Hugh Kaznowski 000efa5166 Merge ticket branch t088 2026-09-07 10:36:17 +01:00
Hugh Kaznowski 73627090a1 Merge ticket branch t091 2026-09-07 10:36:17 +01:00
Hugh KaznowskiandClaude Sonnet 5 c0174963d8 Extract useClickOutside hook, dedupe popover close-on-outside-click logic
RegexHelpIcon and LogPreview's ColumnPicker each reimplemented the same
mousedown-outside-ref-closes-popover effect; share it via a small hook.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 10:35:41 +01:00
Hugh KaznowskiandClaude Sonnet 5 4cc9205abf Move TimeRange types to lib/timeRange to break import cycle
CollapsibleSection.tsx and LogTimeline.tsx imported from each other
(TimeRange/TimeRangePresetId one way, sectionHeaderClassName the
other). Move the range types/helpers into a dependency-free
webapp/src/lib/timeRange.ts and repoint all importers there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 10:35:05 +01:00
Hugh KaznowskiandClaude Sonnet 5 a47f771087 Extract shared declare/update-metric backfill and auto-param insertion helpers
declare_metric and update_metric duplicated the resolve-params +
backfill_metric + insert-MetricDeclaration sequence; extracted into
resolve_backfill_and_store_metric so the declare-vs-rename difference
is expressed only in caller-side logic. Also extracted
auto_declare_params's per-candidate insertion body into
try_insert_auto_param so the candidate-building and insertion steps
read as separate concerns.

Reimplemented directly against current main (ticket 085) after the
original worktree agent's branch was based on a stale pre-consolidation
snapshot module layout that no longer matches main.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 10:32:27 +01:00
Hugh Kaznowski cdd781c904 Merge ticket branch t083 2026-09-07 10:31:23 +01:00
Hugh Kaznowski d987663aaa Merge ticket branch t084 2026-09-07 10:31:23 +01:00
Hugh Kaznowski 157b4e7b1d Merge ticket branch t086 2026-09-07 10:31:23 +01:00
Hugh Kaznowski 94c52f8a17 Merge ticket branch t082 2026-09-07 10:31:23 +01:00
Hugh KaznowskiandClaude Sonnet 5 f67cfc5d94 Extract filter-compile and sort-by-param helpers out of handle_query_inner
The Logs and Timeline arms of handle_query_inner each duplicated the same
"compile filter, bail out on error" block; the Logs arm also inlined its
sort-helper-index walk. Pull both into named helpers (compile_optional_filter,
logs_sorted_by_param) so the Logs arm reads as "compile filter, then dispatch
to sorted or unsorted." Pure decomposition, no behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 10:30:37 +01:00
Hugh KaznowskiandClaude Sonnet 5 564a0e8b2a Extract filter_value_as_f64 helper in compare_json_value
Flattens the Number arm's nested target-coercion match into a
one-line comparison, matching the shape of the String arm. No
behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 10:29:28 +01:00
Hugh KaznowskiandClaude Sonnet 5 1b6c50075a Split cli::run() into named startup-phase functions
run() bundled settings loading, engine spawn, active-profile resolve/load,
ingest spawn, and session registration inline. Extract each into a named
function (spawn_engine, load_active_profile, spawn_ingest,
register_session_and_log) so run() reads as a sequence of steps. No
behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 10:28:51 +01:00
Hugh KaznowskiandClaude Sonnet 5 4398acfa47 Flatten run_metrics_poll's nested match pyramid
Extract fetch_metrics_body(client, url) -> Result<String, String> to
handle the request/error_for_status/text chain with `?`, centralizing
the three warning messages. run_metrics_poll now matches once on the
result instead of nesting three match arms deep on the happy path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 10:28:13 +01:00
Hugh Kaznowski f96de8f38d Compress impls 2026-09-07 02:04:36 +01:00
Hugh Kaznowski 55820982b1 Simplify test helper 2026-09-07 01:53:16 +01:00
Hugh KaznowskiandClaude Sonnet 5 d0ce524e95 Move approx_log_bytes into a flavour::LogLiteralHelper trait
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 01:24:06 +01:00
Hugh Kaznowski 82fa0dfeb4 Remove uploading css functionality. Themes are hard-coded. Change the code 2026-09-07 01:02:41 +01:00
Hugh Kaznowski 901dcf0a3f Makefile cleans profiling 2026-09-07 00:43:52 +01:00
Hugh KaznowskiandClaude Sonnet 5 1b2f690278 Consolidate EngineSnapshot's impl blocks into snapshot/mod.rs
They were scattered across filter/metrics/params/query/sort.rs by an
earlier file-splitting pass. Files can be split as much as needed, but
a single type's impl should never be split across files — so all 15
impl EngineSnapshot blocks now live in mod.rs, while each submodule
keeps its own types, free helper functions, and tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-07 00:43:38 +01:00
Hugh KaznowskiandClaude Sonnet 5 7df6026a21 Remove tickets 079/080 (no profiling evidence to justify the fix)
Both tickets explicitly scope themselves to "confirm via profiling
before implementing" (079: a trigram/token pre-filter for free-text
regex search; 080: caching parsed JSON per log line for JSON-field
filters). Neither has profiling data showing it's the dominant cost
once ticket 077's range-narrowing lands, and building either
speculatively risks unbounded memory (079) or penalizing the more
common non-JSON case (080) for no measured benefit. Revisit if
`samply` profiling against a realistic log volume shows these are
actually hot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-06 20:51:29 +01:00
Hugh KaznowskiandClaude Sonnet 5 946fd50d53 Seek log/metric-bucket queries by key range instead of scanning
Log reads (logs_since, the unsorted Logs query path) now seek the
IngestedLog range directly from since_sequence/before_sequence instead
of scanning from IndexKey::Start and filtering every prior entry out
in a closure. Metric-comparison filters and bucket_values now seek to
a single metric's Bucket key range instead of scanning every declared
metric's buckets. evaluate_promql sits on top of bucket_values, so its
per-poll dashboard evaluation is narrowed by the same fix without
further changes; caching prior evaluations across polls is left for a
follow-up if profiling ever shows it's needed (per ticket 081's own
scope, not built speculatively).

Resolves tickets 077, 078, 081.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
2026-09-06 20:51:24 +01:00
Hugh Kaznowski 65330d37e0 Add tickets, ideas and samply ignore 2026-09-06 20:45:33 +01:00
Hugh Kaznowski d44c14a83b Improve ingestion performance 2026-09-06 19:02:44 +01:00
Hugh KaznowskiandClaude Sonnet 5 f19fa50a25 Split src/engine/mod.rs into handle/message/snapshot submodules
Pure code-motion refactor, no logic changes. Follows the repo convention
of one struct/enum + its impl per module file:

- engine/handle.rs: the left-right write/read plumbing (EngineOp,
  IndexWriter, IndexReadHandle, IndexReader, EngineHandle) and the actor
  loop (spawn/run) that owns EngineSnapshot.
- engine/message.rs: EngineQuery/EngineMessage/EngineResponse.
- engine/snapshot/: EngineSnapshot itself, split by concern into
  params.rs (declaration + auto-detect), metrics.rs (declare/apply/
  backfill), filter.rs (search-bar filter compilation/eval), sort.rs
  (sort-helper indexes), and query.rs (handle_query_inner + PromQL
  evaluation), with mod.rs holding the struct and the accessors shared
  across all of them.
- engine/tests.rs: the inline test module, kept as one file (per the
  task's own escape hatch) since most tests exercise ingest -> declare
  -> query across several of the new submodules at once rather than one
  in isolation.

Visibility was narrowed per call site: pub(super) where only sibling
snapshot submodules need an item, pub(crate) where engine::handle or
engine::tests (outside the snapshot subtree) also need it.

cargo build --all-targets and cargo test are green (291 passed, 1
ignored, same as before the split).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1ZJ3SrayzLzVndhVq9BmS
2026-09-06 18:36:53 +01:00
Hugh Kaznowski 3b3ace146a Increase mcp scope 2026-09-06 17:34:36 +01:00
Hugh KaznowskiandClaude Sonnet 5 a810ecf396 Split src/ into an api/cli/engine/config module hierarchy
- src/api/: HTTP router + DTOs + the MCP server (the HTTP/MCP-facing
  surface): mod.rs (formerly api.rs), dto.rs, mcp_server.rs.
- src/cli/: argument parsing and process entry (formerly inline in
  main.rs) plus session registry: mod.rs, sessions.rs. main.rs is now
  a thin entry point that just declares modules and calls cli::run().
- src/engine/: the actor + everything it depends on to parse/index/
  query log lines: mod.rs (formerly engine.rs), alerts.rs, promql.rs,
  rate.rs, ingest.rs, logfmt.rs, filter_parser.rs/.pest,
  filter_datetime.rs, index_key.rs, data_value.rs, metric_primitive.rs,
  timestamp.rs.
- src/config/: persisted configuration: mod.rs (formerly settings.rs),
  profile.rs.
- src/consts.rs stays at the crate root, used from every module above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1ZJ3SrayzLzVndhVq9BmS
2026-09-06 17:15:39 +01:00
Hugh KaznowskiandClaude Sonnet 5 311713986a Wrap the left-right index in IndexWriter/IndexReader types
The engine actor and API handlers previously called left_right's
WriteHandle/ReadHandleFactory methods (append/publish/enter/handle)
directly. IndexWriter now exposes mutate()/peek() and IndexReader
exposes handle() (returning an IndexReadHandle with its own enter()),
so how the single-writer/multi-reader index is accessed is named and
scoped to these two types instead of being raw left_right calls
scattered through engine.rs/api.rs/profile.rs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1ZJ3SrayzLzVndhVq9BmS
2026-09-06 17:10:23 +01:00
Hugh KaznowskiandClaude Sonnet 5 3c2995acc1 Convert remaining clean-receiver free functions to methods
Profile (legacy-shape migration, load_into_engine, load_legacy_store),
MetricDeclarationRecord/Dashboard seed constructors, PromqlQuery::parse,
LogStreamHeader (detect/split_row/column_pattern/message_pattern), and
api.rs's EngineHandle::ask/*Error::into_response/ParamDeclarationDto::reserved
all had a single obvious receiver and are now methods on it instead of
free functions taking that type as a parameter.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1ZJ3SrayzLzVndhVq9BmS
2026-09-06 17:08:29 +01:00
Hugh KaznowskiandClaude Sonnet 5 529f79e9da Convert engine.rs index-manipulating functions into EngineSnapshot methods
Free functions that took the engine's index/sort-helper-index state as
a parameter now live on EngineSnapshot (or, for a handful of clean
single-receiver cases, on MetricPrimitive/CompareOp/CompiledFilter/
EngineQuery). Route handlers in api.rs and alert evaluation in
alerts.rs are updated to call the new methods; alerts.rs now threads
an &EngineSnapshot through instead of a raw &BTreeMap so the per-line
alert backfill path doesn't clone the whole index just to call a method.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1ZJ3SrayzLzVndhVq9BmS
2026-09-06 17:03:19 +01:00
Hugh KaznowskiandClaude Sonnet 5 b6401c1c3c Strip all comments outside src/consts.rs
Removes doc comments, module comments, and inline comments across
the backend so the only comments left in the codebase are the brief
ones on the constants they document.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1ZJ3SrayzLzVndhVq9BmS
2026-09-06 16:34:42 +01:00
Hugh KaznowskiandClaude Sonnet 5 284e3562a2 Move scattered magic constants into src/consts.rs
Centralizes engine tuning values, poll intervals, logfmt column
regex fragments, and app/settings identity strings that were
previously defined ad hoc across engine.rs, ingest.rs, logfmt.rs,
profile.rs, settings.rs, and main.rs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1ZJ3SrayzLzVndhVq9BmS
2026-09-06 16:29:03 +01:00
Hugh Kaznowski 1c635bc530 Left-right single-writer, multi-reader index 2026-09-06 16:05:04 +01:00
Hugh KaznowskiandClaude Sonnet 5 9fc7540a5b Resolve tickets 071-074, 076: log-stream contamination, sort, ottyl_fmt column, ingest lag
- 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
2026-09-06 15:12:07 +01:00
Hugh Kaznowski f34dbba6c7 Update skill and tickets 2026-09-06 14:37:03 +01:00
Hugh Kaznowski fc8de1a5b0 Remove wishlist 2026-09-06 14:00:05 +01:00
Hugh KaznowskiandClaude Sonnet 5 c77404ba4e Resolve tickets 067-069: named sessions, MCP server, embedded SKILL.md
Adds a sessions/<name>.json registry (068) so a running ottyl instance can
be discovered by name; an `ottyl mcp --session <name>` subcommand (067)
that starts a short-lived MCP server over stdio (via the official rmcp
SDK), proxying query_logs/declare_param/declare_metric/create_dashboard/
add_panel/get_dashboard tool calls onto the existing HTTP API; and a
SKILL.md (069), embedded into the binary and served at GET /SKILL.md, that
documents how an agent connects to and drives a running instance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1ZJ3SrayzLzVndhVq9BmS
2026-09-06 13:57:30 +01:00
Hugh KaznowskiandClaude Sonnet 5 de9ba6f01b Resolve tickets 059-061: generic log-stream columns + sort helper indexes
- 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
2026-09-06 13:23:10 +01:00
Hugh KaznowskiandClaude Sonnet 5 4ace654019 Resolve tickets 058, 062-066: frontend UI fixes
- 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
2026-09-06 13:04:26 +01:00
Hugh Kaznowski a03ec1fb93 Create tickets 2026-09-06 13:00:21 +01:00
Hugh Kaznowski 4158c63086 Add nom parsers for go and python 2026-09-06 10:45:55 +01:00
Hugh Kaznowski 8f71f2d056 Icons, docs formats 2026-09-06 10:36:26 +01:00
Hugh Kaznowski 17efe222df Add icon 2026-09-06 10:16:11 +01:00
Hugh KaznowskiandClaude Sonnet 5 79d252c1df Implement ticket 057: fix rate() line charts for sparse/bursty occurrences
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
2026-09-06 09:53:12 +01:00
Hugh KaznowskiandClaude Sonnet 5 5493358db8 Fix ticket 052's clear-params test for ticket 055's reserved params
GET /api/params now includes the three non-deletable ottyl_* reserved
entries (ticket 055), so the "clear removes everything" assertion needs
to count only real (non-reserved) declarations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 09:49:02 +01:00
Hugh KaznowskiandClaude Sonnet 5 ec98a3ab18 Merge ticket 055: make ottyl_* reserved params usable in dashboard PromQL queries
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
2026-09-06 09:44:52 +01:00
Hugh KaznowskiandClaude Sonnet 5 85f2a5c0ac Merge ticket 052: allow clearing all params and re-extracting from ingested logs
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 09:44:09 +01:00
Hugh KaznowskiandClaude Sonnet 5 30c301d595 Merge ticket 051: allow deleting a declared metric
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 09:44:05 +01:00
Hugh KaznowskiandClaude Sonnet 5 fdad154c1a Merge ticket 050/054: bound auto-declared param cardinality, recognize log stream format
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 09:44:01 +01:00
Hugh KaznowskiandClaude Sonnet 5 6cdb9dda2c Merge ticket 056: add Light variants for remaining colour schemes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 09:43:57 +01:00
Hugh KaznowskiandClaude Sonnet 5 914aaf88b3 Merge ticket 053: move sort selector from Log Preview to Timeline
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 09:43:52 +01:00
Hugh KaznowskiandClaude Sonnet 5 b15fbcee04 Add ticket 057: rate() line chart x-axis collapses to sparse occurrence window
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 09:43:48 +01:00
Hugh KaznowskiandClaude Sonnet 5 f48d3c6ffb Bound auto-declared param cardinality and recognize log stream format (ticket 050/054)
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
2026-09-06 09:42:50 +01:00
Hugh KaznowskiandClaude Sonnet 5 ff56558b2e Make ottyl_seq/ottyl_ts usable in dashboard PromQL queries (ticket 055)
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
2026-09-06 09:42:19 +01:00
Hugh KaznowskiandClaude Sonnet 5 466c81fa94 Add DELETE /api/metrics/{name} to allow deleting a declared metric (ticket 051)
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
2026-09-06 09:41:07 +01:00
Hugh KaznowskiandClaude Sonnet 5 4e37e2a7ee Add bulk clear-params action that re-extracts from retained logs (ticket 052)
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
2026-09-06 09:39:22 +01:00
Hugh KaznowskiandClaude Sonnet 5 d59b3aec38 Add light variants for red, blue, yellow, pink, orange colour schemes
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
2026-09-06 09:37:21 +01:00
Hugh KaznowskiandClaude Sonnet 5 78a6672ced Move log sort selector from Log Preview to Timeline header
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
2026-09-06 09:34:46 +01:00
Hugh Kaznowski d17cfe9b83 Update claude and tickets 2026-09-06 09:30:38 +01:00
Hugh KaznowskiandClaude Sonnet 5 2908cccd76 Add ticket 051: allow deleting a declared metric
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
2026-09-06 09:11:31 +01:00
Hugh KaznowskiandClaude Sonnet 5 5cb2dfde13 Remove ticket 047 (implemented: unified style presets, colour + layout as one concept)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 02:17:39 +01:00
Hugh Kaznowski 8ab5edc96b Merge ticket 047: unified style presets (colour + layout as one concept) 2026-09-06 02:17:27 +01:00
Hugh KaznowskiandClaude Sonnet 5 5b31f51526 Unify colour scheme and CSS as one Style list in settings UI (ticket 047)
The settings panel had two separate sections — "Colour scheme" (built-in
palettes) and "CSS" (imported/linked stylesheets) — even though the data
model (tickets 012/020) already treats both as equally-selectable themes
via custom_theme_id. Merge them into one "Style" section presenting a
single list: built-in scheme pills plus imported/linked entries, any of
which can be a palette swap or a full layout override, with copy that no
longer implies "colour scheme" and "CSS" are separate features.

Traced and manually verified (via a running dev server) that a
user-supplied stylesheet is not constrained to --c-* variable overrides:
import_css/get_css_content in src/api.rs and src/settings.rs store and
serve the raw content unmodified (no sanitization or selector scoping),
and applyCustomThemeCss injects it as a plain unscoped <link> tag — a
stylesheet with e.g. `flex-direction`/`font-size` rules on real selectors
applies exactly as authored, alongside the required --c-* declarations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 02:17:10 +01:00
Hugh KaznowskiandClaude Sonnet 5 7c331e76b1 Remove ticket 048 (implemented: multi-accent and brighter theme variants)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 02:12:04 +01:00
Hugh Kaznowski 8e7973e4fd Merge ticket 048: multi-accent and brighter theme variants 2026-09-06 02:11:49 +01:00
Hugh KaznowskiandClaude Sonnet 5 5371d3e283 Add multi-accent palettes and a brighter Light Green theme (ticket 048)
Every built-in theme now exposes three accents (--c-accent/-2/-3) instead
of one, so PanelChart's series palette draws distinct colours per theme
via color-mix() rather than a fixed, theme-independent hex list. Existing
presets keep their current base surface/border/text/accent look — the two
new accent vars are purely additive. Adds a new "Light Green" scheme
(light-surfaced, same green/magenta hue family as "Green") selectable
alongside the existing built-ins, both in ColorScheme (frontend + backend)
and the settings picker.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 02:11:33 +01:00
Hugh KaznowskiandClaude Sonnet 5 19044753c6 Remove ticket 045 (implemented: timeline/log ordering by declared param)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 02:04:09 +01:00
Hugh Kaznowski 0b6442a348 Merge ticket 045: timeline/log ordering by declared param 2026-09-06 02:03:53 +01:00
Hugh KaznowskiandClaude Sonnet 5 7efaaa12f1 Remove ticket 049 (implemented: profiles stored inline in settings.json)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 02:03:49 +01:00
Hugh KaznowskiandClaude Sonnet 5 7a38d008fd Merge ticket 049 (inline profiles into settings.json) with auto-derived-param promotion
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
2026-09-06 02:03:22 +01:00
Hugh KaznowskiandClaude Sonnet 5 3d2652e95b Add sort_by ordering for the log viewer, with a missing-param fallback/drop toggle (ticket 045)
/api/logs now accepts sort_by (an already-declared param name) and
sort_missing_fallback: lines carrying that param are ordered by its value,
while lines missing it are either appended in ottyl_seq order (fallback,
the default) or dropped from the response. Default ottyl_ts-tracking
order is unchanged when sort_by is omitted.

The log viewer (both the inline panel and its popout) exposes this as a
"Sort by" control next to the column picker, persisted to localStorage
under one shared key (ottyl.logPreview.sort) — the same global-viewer-
setting convention already used for visible param columns and the log
viewer's height, since there's no per-dashboard scope for this to divide
by.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 01:58:30 +01:00
Hugh KaznowskiandClaude Sonnet 5 e8baf2b004 Inline profile storage into settings.json (ticket 049)
Fold ProfileStore's separate profiles.json into Settings itself, so
settings.json is the single source of truth instead of two files kept
in sync by hand. Settings now carries `profiles: Vec<Profile>` and
`active_profile: Option<String>` directly; every former ProfileStore
mutation (create, delete, set_active, record_param_declaration,
record_metric_declaration, dashboard CRUD, ensure_default) is now a
method on Settings that persists via Settings::save.

A custom Deserialize (via SettingsOnDisk) keeps an old settings.json
(with the removed Profiles pointer struct) loading cleanly, and
Settings::load runs a one-time migration: if profiles is empty and a
legacy <settings_dir>/profiles/profiles.json exists, its contents are
merged in, settings.json is resaved, and the old file is renamed so it
isn't re-imported.

Updated all call sites in api.rs and main.rs to the single-path API,
and ported profile.rs's ProfileStore tests to exercise Settings
directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 01:54:04 +01:00
Hugh KaznowskiandClaude Sonnet 5 70d98562d1 Remove ticket 044 (implemented: ottyl_fmt param + ottyl_seq/ottyl_ts rename)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 01:50:50 +01:00
Hugh Kaznowski a7147f47a0 Merge ticket 044: ottyl_fmt reserved param + ottyl_seq/ottyl_ts rename 2026-09-06 01:50:39 +01:00
Hugh KaznowskiandClaude Sonnet 5 534dc52e05 Rename reserved sequence/timestamp params to ottyl_seq/ottyl_ts; add ottyl_fmt
Ticket 044: `sequence` and `timestamp` collided with ordinary user-declared
param names and didn't signal they're ottyl-owned, so they're renamed
(breaking change, no back-compat alias) to `ottyl_seq`/`ottyl_ts` in the
filter grammar (src/filter_parser.pest, src/filter_parser.rs), alert query
restriction/evaluation (src/alerts.rs), and the filter-bar docs/placeholder
in the webapp.

Also adds a new reserved param `ottyl_fmt` exposing which format a line was
auto-detected as ("json"/"logfmt"/"none") via a new
`crate::logfmt::detect_format`, wired into the same filter grammar and alert
restriction list as `ottyl_seq`/`ottyl_ts`. A mixed-format fixture test in
engine.rs verifies `ottyl_fmt` reflects the actual detected format per line.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 01:50:16 +01:00
Hugh KaznowskiandClaude Sonnet 5 c84276937a Remove ticket 046 (implemented: chart legend squashing)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-06 01:44:25 +01:00
Hugh Kaznowski 287b81989c Merge ticket 046: chart legend squashing 2026-09-06 01:44:18 +01:00