Commit Graph
79 Commits
Author SHA1 Message Date
Hugh KaznowskiandClaude Sonnet 5 87209ea401 Make Alerts section collapsible
Wire the Alerts section into the shared CollapsibleSection mechanism
(CollapseToggle + useSectionCollapsed), matching the pattern used by
Metrics and Params, with its own persisted 'alerts' section id.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:44:22 +01:00
Hugh KaznowskiandClaude Sonnet 5 e6e5bc8f50 Merge ticket/008-docs-worked-examples
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:38:58 +01:00
Hugh KaznowskiandClaude Sonnet 5 049c4f8311 Remove tickets/008-docs-add-worked-examples.md (implemented)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:38:56 +01:00
Hugh KaznowskiandClaude Sonnet 5 1db3adde90 Add worked examples to docs pages
Add concrete, verified examples to the General idea, Filtering,
Parameters, Metrics, Dashboards, and Settings docs sections, covering
common tasks like declaring a value-role duration param, building a
requests dashboard with line/bar/pie panels, and filter-bar syntax
gotchas (quoted vs. unquoted values, metric-only [param] comparisons).
Verified each example against the actual filter/PromQL parsers and
engine behavior in src/filter_parser.rs, src/promql.rs, and
src/engine.rs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:38:49 +01:00
Hugh KaznowskiandClaude Sonnet 5 80515e0f84 Merge ticket/005-sharp-angular-timeline-spikes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:38:43 +01:00
Hugh KaznowskiandClaude Sonnet 5 5e2e556ea1 Merge ticket/011-approximate-heap-tracker
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:38:32 +01:00
Hugh KaznowskiandClaude Sonnet 5 2c509b51fc Remove tickets/011-approximate-heap-tracker.md (implemented)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:38:29 +01:00
Hugh KaznowskiandClaude Sonnet 5 0834c246d6 Add approximate heap/memory tracker for ingested logs (ticket 011)
Tracks a running total of ingested log bytes (approximated as each
log's raw UTF-8 text length), incremented on ingest and decremented
in evict_excess_logs when a log is dropped. Exposed via StatsDto's
new approx_bytes_total field on GET /api/stats, and displayed in
NavMenu's stats panel formatted with toLocaleString() (comma
separators).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:38:08 +01:00
Hugh KaznowskiandClaude Sonnet 5 de48e218a1 Use sharp/angular tops for timeline event spikes
Remove the rounded-t Tailwind class from timeline bars so spike peaks
render as sharp, flat-topped rectangles instead of rounded corners,
making bucket boundaries clearer to read. Verified in the browser in
both light and dark theme.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:37:27 +01:00
Hugh KaznowskiandClaude Sonnet 5 b1e509efbd Record JSON log ingestion design decision, file follow-up tickets (ticket 003)
Decision covers detection, invalid-JSON handling, and a prioritized feature
roadmap (repowise decision fdb47034ad954e3ea0b898fdc2750c32). Files three
follow-up implementation tickets (014-016) and removes the investigation
ticket now that the decision is recorded.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:36:27 +01:00
Hugh KaznowskiandClaude Sonnet 5 bb513b5eac Merge ticket/013-ntfy-sink-inline-fields
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:35:43 +01:00
Hugh KaznowskiandClaude Sonnet 5 ca6898a9b3 Merge ticket/001-remove-dashboard-nav-entry
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:35:33 +01:00
Hugh KaznowskiandClaude Sonnet 5 83c617d628 Remove tickets/001-remove-dashboard-nav-entry.md (implemented)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:35:30 +01:00
Hugh KaznowskiandClaude Sonnet 5 70d9956059 Lay out ntfy sink server URL and topic inputs on one line
Combines the ntfy server URL and topic fields into a single flex row
separated by a "/" to visually communicate that they compose into one
destination (server + topic), instead of stacking as two full-width
rows.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:35:24 +01:00
Hugh KaznowskiandClaude Sonnet 5 6d0cb92c19 Remove redundant "Dashboard" nav bar entry
NavMenu rendered a static, non-functional "Dashboard" span alongside
Presets/Settings/Docs buttons. It didn't toggle any panel — the real
Dashboards content (App.tsx's <Dashboards />) is already always visible
in the main content area, so it needed no nav entry to be reachable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:35:24 +01:00
Hugh Kaznowski 893464b3b1 Create tickets 2026-09-05 18:31:22 +01:00
Hugh KaznowskiandClaude Sonnet 5 0a21582282 Simplify docs and add concrete examples for input fields
Rewrites doc-panel descriptions to be direct and absolute (no edge-case
hedging), and adds concrete example values for params, metrics, filter
queries, alert conditions, and CLI arguments. Also updates the Alerts/
Settings/Presets sections to describe the ntfy-sink-in-Settings and
profile-dropdown changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:19:37 +01:00
Hugh KaznowskiandClaude Sonnet 5 e69ce4e0ba Replace the Presets side panel with an inline profile dropdown
Presets is no longer a collapsible NavMenu tab. It's now a compact
dropdown in the sidebar listing profiles, with a "+" button that prompts
for a name and creates+activates a copy of the current profile, and a
"×" button that deletes the active profile (disabled for Default).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:17:23 +01:00
Hugh KaznowskiandClaude Sonnet 5 12381545c8 Show the resolved default settings path in --help; add Prometheus/OpenMetrics polling
--settings-path's help text now names the actual resolved default path
(computed once via LazyLock) instead of only describing where to look for
one. Also adds an optional --metrics-url flag that polls a Prometheus/
OpenMetrics-compatible endpoint on an interval, pushing samples straight
into a metric's bucket (bypassing log-line regex matching) so they're
queryable via PromQL/dashboards alongside log-derived metrics.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:16:13 +01:00
Hugh KaznowskiandClaude Sonnet 5 18c2d72d22 Move the ntfy alert sink to Settings, shared by all alert rules
Alert rules no longer carry their own ntfy topic/server; Settings now
holds one process-wide ntfy sink (with its own "send test notification"),
and the Alerts panel is left to just define rule conditions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:13:51 +01:00
Hugh KaznowskiandClaude Sonnet 5 ae4e550c1f Anchor log viewer scroll to visible content and add 1m/2m timeline presets
The log table no longer jumps when a sliding window prunes rows from the
front: scroll position now tracks the same visible row instead of holding
scrollTop constant in pixels. Also adds 1m/2m to the timeline quick-range
presets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 18:08:23 +01:00
Hugh Kaznowski 070e835fa4 Resolve ui stuff 2026-09-05 18:00:56 +01:00
Hugh KaznowskiandClaude Sonnet 5 00c21c8032 Trim comments across Rust and webapp source to direct one-liners (ticket 030)
Pass over src/*.rs, webapp/src/**, and the in-app "?" docs prose:
condensed multi-paragraph doc comments to single lines that state the
non-obvious why, deleted comments that only restated adjacent code, and
tightened the Docs.tsx help-panel wording. No logic, structure, or code
formatting changed — verified via cargo test (143 passed) and
tsc -b && vite build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 17:02:33 +01:00
Hugh KaznowskiandClaude Sonnet 5 3cef6dc8ae Remove unused tower-http dependency (ticket 029)
Audited every entry in Cargo.toml and webapp/package.json against actual
usage. tower-http was declared but never referenced in src/ (no ServeDir,
CorsLayer, or other tower_http usage) — it was only pulled in transitively
via reqwest anyway, so it's dead weight as a direct dependency. All other
Cargo.toml and package.json entries have live usage (including include_dir,
which looked unused via grep but is required transitively by the
fs_embed::fs_embed! macro, and tailwindcss, which is pulled in via
@import "tailwindcss" in src/index.css rather than a JS import).

cargo test (143 passed) and webapp npm run build both pass after pruning.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 16:50:54 +01:00
Hugh KaznowskiandClaude Sonnet 5 0c4b596e3c Add pop-out button to open a section in its own tab (ticket 036)
Each of the four primary sections (Log viewer, Params, Metrics,
Dashboards) gets a PopOutButton next to its CollapseToggle/HelpIcon that
opens `?section=<id>` in a new tab via window.open. App.tsx branches on
that query param to render just the one section full-page, with no
NavMenu/FilterBar/sibling chrome — Metrics/Dashboards get the range that
was active at pop-out time via startMs/endMs query params (they have no
LogTimeline of their own to pick one), Params gets a small self-contained
params fetch since it has no LogPreview sibling to share state with, and
the popped-out Log viewer owns its own copy of the log/timeline polling
state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 16:49:12 +01:00
Hugh KaznowskiandClaude Sonnet 5 7db1e0f055 Add collapsible primary UI sections (ticket 035)
Log viewer (FilterBar+LogTimeline+LogPreview), Params, Metrics, and
Dashboards can each be collapsed/expanded via a toggle next to their
existing help icon. Collapsed state persists per-section in
localStorage (ottyl.collapsedSections) and content is hidden with the
`hidden` attribute rather than unmounted, so in-progress form state
(a half-filled Metrics/Params form, in-progress Dashboards panel
edits) survives a collapse/expand cycle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 16:46:22 +01:00
Hugh KaznowskiandClaude Sonnet 5 dd1a0a234d Add Params declaration UI (ticket 034)
New Params panel (Log viewer / Params / Metrics / Dashboards) declares a
bare param via POST /api/params, mirroring Metrics' declare-then-list
pattern including inline 400 error surfacing. Lifted the params list from
LogPreview's own effect into App.tsx so declaring a param there updates
the Columns picker immediately, without a page reload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 16:43:53 +01:00
Hugh KaznowskiandClaude Sonnet 5 41c9a3660b Add downloadable/re-uploadable custom themes (ticket 033)
Themes download as a small `:root { --c-*: ...; }` CSS file (full
canonical variable set); uploading validates that set is complete and
registers the file as a CssEntry the theme picker can select. A new
`is_theme` flag on CssEntry distinguishes theme entries from arbitrary
imported/linked CSS so they don't leak into the page unconditionally
like the existing "Configured CSS" list does — a selected custom
theme's CSS is injected only while active, with built-in html.<scheme>
classes removed so its :root override isn't outranked in the cascade.
Linked (URL) custom themes reuse the existing CSS link mechanism and
inherit its non-deletable semantics.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 16:41:39 +01:00
Hugh KaznowskiandClaude Sonnet 5 7bf323dd50 Add Alerts webapp UI (ticket 020)
New Alerts panel: lists rules with condition summaries and an enabled
toggle, and a condition-type-specific editor (Immediate/Threshold/NewValue/
Ratio) with client-side validation before save, reusing FilterBar's query
input styling for query-based conditions. Adds a test-notification endpoint
(POST /api/settings/alerts/test) so an ntfy topic/server can be verified
without saving a rule first, plus a docs section and help icons matching
ticket 021's pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 16:34:14 +01:00
Hugh KaznowskiandClaude Sonnet 5 dd3320812c Add alert rules with FilterBar-based triggers and ntfy delivery (tickets 017-019)
Adds AlertRule/AlertCondition/AlertSink models with CRUD persisted on
Settings (mirroring CssEntry's shape), per-line trigger evaluation hooked
into the engine's ingest path (Immediate/Threshold/NewValue/Ratio
conditions), and best-effort ntfy delivery spawned off the ingest hot path.

Design notes:
- Alert queries reuse FilterBar syntax but only support the
  regex()/sequence/timestamp subset (no metric[param] comparisons), since
  resolving those would require a full bucket rescan per alert per ingested
  line rather than cost proportional to new logs.
- Ratio's zero-denominator case skips evaluation for that line rather than
  treating the ratio as infinite.
- NewValue's seen-value set is in-memory only and does not survive a
  restart, matching the engine's other non-persisted runtime state.

engine::run now takes a SettingsHandle (moved from api.rs to settings.rs so
engine.rs can depend on it without a circular import) to read the current
alert rules on each ingest.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 16:30:14 +01:00
Hugh KaznowskiandClaude Sonnet 5 758feca780 Rework colour themes as greyscale + accent-only colour (ticket 032)
light/dark are now exact lightness mirrors of one true neutral scale,
grey is that same canonical neutral scale, and each named theme
(red/green/blue/yellow/pink/orange) derives its surface/border/text
greys by desaturating its own Tailwind hue at each scale step (HSL
S->0, lightness kept), leaving saturated colour only on --c-accent*.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 16:21:50 +01:00
Hugh KaznowskiandClaude Sonnet 5 7694dcf120 Embed webapp into the binary with fs-embed, drop --static-dir
ottyl was requiring webapp/dist to be shipped alongside the binary and
served via ServeDir/ServeFile pointed at a --static-dir CLI arg. Embed
the built webapp at compile time instead, so ottyl is a single
self-contained executable with no runtime static directory dependency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 16:21:02 +01:00
Hugh KaznowskiandClaude Sonnet 5 1d1b0eee01 Add timeline start/end markers, cursor hover, and live drag readout (ticket 031)
LogTimeline now always shows the first/last bucket's index and timestamp,
a hover readout that tracks the cursor with the bucket at/before it, and
a live-updating start/end readout while dragging a selection.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 16:20:18 +01:00
Hugh Kaznowski b8a55b3caa Update tickets 2026-09-05 16:17:18 +01:00
Hugh KaznowskiandClaude Sonnet 5 b89fda39a0 Add Grafana-style time range presets: All, 5m, 15m, 1h, 24h (ticket 024)
LogTimeline gets a preset button row (All + four relative windows)
next to the existing drag-select interaction. App.tsx lifts the
active preset alongside `range`; a relative preset re-resolves to a
fresh {startMs, endMs} on the same 2s cadence the app already polls
on, so it visibly rolls forward rather than freezing at selection
time. Dragging on the timeline (or picking a different preset)
overrides back to a fixed range, matching today's drag behavior.

The rolling re-resolution reads/writes `range` via a ref inside the
poll loop instead of depending on `range` directly, since a naive
`setRange` inside an effect keyed on `range` would restart the effect
(and its interval) every tick — turning "poll every 2s" into "poll as
fast as React can re-render." Verified live: exactly 3 /api/stats
calls over ~6.2s with a rolling preset active, and console/network
free of errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 15:57:58 +01:00
Hugh Kaznowski 363018793f Update wishlist 2026-09-05 15:55:18 +01:00
Hugh KaznowskiandClaude Sonnet 5 7e53574408 Dashboards: remove manual save, autosave everything (ticket 027)
Removes the explicit "Save dashboard" button and its saving-state
gating. A debounced effect (600ms, keyed on the draft) now persists
dashboard/panel edits automatically — renames, panel add/remove, and
per-field edits (title, PromQL, chart type) all flow through the same
`draft` state that already backed the old button, so no separate
autosave plumbing per mutation was needed. A `lastSavedRef` snapshot
of the last-persisted { name, panels } shape lets the effect skip
scheduling a save when nothing actually changed, avoiding a spurious
autosave both on dashboard selection and on the re-render that
follows the effect's own successful save.

Save state (idle/saving/saved/error) surfaces as small inline text
near the panel-add button rather than a blocking dialog; a failed
autosave leaves the in-progress edit in place so it isn't lost.
Verified against a live server: 18 rapid keystrokes produce exactly
one PUT, edits persist across a reload, and a simulated 500 shows
"Autosave failed" without reverting the field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 15:52:38 +01:00
Hugh KaznowskiandClaude Sonnet 5 abc061a7b1 Add contextual "?" help icons linking into Docs (ticket 021)
New HelpIcon component + a DocsNavProvider context (webapp/src/lib/docsNav.tsx)
let any component request "open Docs to section X", bumping a nonce each
time so a repeat click re-triggers even if Docs is already open to that
section. NavMenu forwards that request into DocsPanel as a `target` prop,
which expands the right accordion section, scrolls it into view, and
flashes it (a brief accent-tinted background/border, settling back after
1.2s) — verified visually with a live server + Playwright. Opening Docs via
the plain NavMenu button explicitly clears any pending target, so that path
is unchanged: no forced section, no flash.

Added a "?" icon to FilterBar, LogTimeline, LogPreview, Metrics, Dashboards,
and NavMenu's own Settings/Presets panels, plus new Docs sections for
Timeline, Log preview, Settings, and Presets (previously undocumented).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 15:49:49 +01:00
Hugh Kaznowski ce711ab70e SImplify 2026-09-05 15:39:07 +01:00
Hugh KaznowskiandClaude Sonnet 5 1da00e3586 Seed a default dashboard with 2 starter panels (ticket 025)
ensure_default now seeds a brand-new Default profile (not one that
already existed) with a "log_category" counter metric and an
"Overview" dashboard containing a line and a pie panel, both querying
that metric so each renders both series from a single PromQL query
like any user-created panel.

The counter classifies every line into exactly "category=error" or
"category=other" via two new fixed-label fields on metric
declarations: DeclareMetricRequest::matched_labels/unmatched_labels.
These apply literal label values instead of parsing captured text,
which is what keeps a case-insensitive match (e.g. "ERROR" vs "Error")
collapsed into one clean series instead of fragmenting by source
casing, and is also the only way to express the "line does NOT match"
side at all: plain regex alternation can't do it without lookaround,
which the `regex` crate doesn't support (a catch-all alternation
branch always wins at the search's leftmost position). Both fields
default to None and are backward compatible with existing profiles.

Seeded panels are ordinary editable/deletable dashboard state,
persisted through the normal profile save path; ensure_default never
re-seeds or duplicates them on a later call once the profile exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 15:38:27 +01:00
Hugh KaznowskiandClaude Sonnet 5 8faf12f819 Add Duration-type metric param: elapsed time since previous same-label match (ticket 023)
MetricPrimitiveKind::Duration/MetricPrimitive::Duration(i64 ms) is a
new primitive kind. On a metric's Value-role param (ticket 022), kind
Duration is special-cased in apply_metric_to_log: instead of parsing
the captured text, the value is derived as elapsed time since the
previous counter event with the same label set, using the ingestion
timestamp. That "previous event" lookup is tracked via a new
IndexKey::LastMetricEvent/DataValue::LastEventTimestamp bookkeeping
entry rather than Bucket.values itself, because a label set's first
occurrence has nothing to diff against and pushes no Bucket entry at
all (per spec: omit, don't emit a bogus zero) — but it still needs to
be recorded so the *next* occurrence has something to diff against.

Duration values compose with the existing bucket/PromQL surface
unchanged (coerce_to_f64, filter comparisons, histogram()/gauge()).
On any other param (Label-role metric param, or a standalone
ParamDeclaration), Duration kind instead parses captured text as a
literal i64 millisecond count, same as Number. Profile round-trip
(ProfileMetricPrimitiveKind) and the Metrics.tsx declaration form are
updated to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 15:24:28 +01:00
Hugh KaznowskiandClaude Sonnet 5 0c57ed2953 Metrics rethink: counters as the primitive, histogram()/gauge() derived (ticket 022)
Every metric declaration is now explicitly documented and modeled as
a counter: a match is an occurrence/increment, optionally labeled.
The bucket representation (BucketValue { values: Vec<(Sequence,
Timestamp, MetricPrimitive)> }) already was this; what it lacked was
any way to capture a *value* distinct from the occurrence-count-of-1
marker. Adds RegexToMetricPrimitiveMatcher::role (ParamRole::Label,
the default, or ParamRole::Value — at most one per metric, enforced
at declare time) so a metric can designate one param's captured
number as its per-event value instead of a label.

promql.rs/engine.rs add two new PromQL-evaluable forms built on top
of that value, both consumed as regular PromqlSeriesDto series like
sum()/avg()/rate():
- histogram(metric_name[width]): buckets matching events' values into
  fixed-width buckets, one series per bucket labeled `le`.
- gauge(metric_name): the most-recent value per label combination in
  the queried range.

Existing raw counter queries (metrics with only Label-role params,
sum/avg/rate) are unchanged. Profile files saved before this ticket
still load (role defaults to Label via #[serde(default)]). Metrics.tsx
gains a label/value role picker per param, and the Docs panel explains
the model and the two derived query forms.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 15:18:25 +01:00
Hugh KaznowskiandClaude Sonnet 5 1d0f324c43 Add settings-configurable cap on logs kept in memory (ticket 026)
Settings.max_logs (default None = unlimited) is pushed to the engine
via a new EngineMessage::SetMaxLogs, enforced on every ingest and
whenever the setting changes at runtime (no restart required).
Eviction drops the oldest (lowest-sequence) logs; sequence numbering
isn't renumbered and buckets already derived from an evicted log are
left in place. Exposed via PATCH /api/settings (max_logs: number |
null) and a NavMenu numeric field (blank = unlimited).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 15:12:12 +01:00
Hugh KaznowskiandClaude Sonnet 5 aaa5596030 Add --file ingestion with restart-safe tailing (ticket 020)
ingest::run_file tails a path like tail -f: reads existing content,
follows appended lines, and detects file recreation/truncation (via
inode on unix, or a shrinking size) to resume tailing the new
instance from its start instead of exiting or stalling. Sequence
numbers keep incrementing across a recreation event since they're
ottyl's own line counter, not tied to the source file. stdin ingest
is unchanged and remains the default when --file is omitted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 15:08:20 +01:00
Hugh KaznowskiandClaude Sonnet 5 b3701cd2e9 Default --port to 0 for OS-assigned ephemeral port (ticket 019)
Reads back the actual bound port via listener.local_addr() since
binding to 0 means the requested and real ports differ.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 15:06:46 +01:00
Hugh Kaznowski 51e577f01e Docs, ticks, working app 2026-09-05 15:05:39 +01:00
Hugh KaznowskiandClaude Sonnet 5 41362aa3d8 Add Dashboards UI: PromQL-backed bar/line/pie panels saved per profile (ticket 014)
Backend: replace Profile.dashboards' opaque JSON stub with typed
Dashboard/Panel structs, add ProfileStore CRUD (create/save/list/delete
dashboard) and matching /api/settings/profiles/:name/dashboards[/:id] routes,
with tests covering the HTTP round trip and persistence across a simulated
restart.

Frontend: add recharts (lightweight, React 19-compatible) and a Dashboards
component with a dashboard builder (create dashboards, add/remove/edit panels
with a PromQL query + chart type), live per-panel previews that respect the
active time range and show malformed-query errors inline instead of crashing,
and load/save wired to the new profile-backed persistence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 13:29:45 +01:00
Hugh Kaznowski ae21a7a0ce Merge branch 'worktree-agent-a6820222665a87090'
# Conflicts:
#	webapp/src/lib/api.ts
2026-09-05 13:23:08 +01:00
Hugh Kaznowski 8e31a7c1df Merge branch 'worktree-agent-a1b2ebf28f9c558d0'
# Conflicts:
#	webapp/src/App.tsx
2026-09-05 13:22:08 +01:00
Hugh KaznowskiandClaude Sonnet 5 801ac36b78 Add NavMenu settings/presets UI and profile HTTP routes (ticket 011)
Adds thin HTTP routes for profile CRUD and colour-scheme updates
(GET/PATCH /api/settings, GET/POST /api/settings/profiles,
DELETE /api/settings/profiles/:name, POST /api/settings/profiles/:name/activate)
following the existing CSS-route pattern in api.rs, with activate hot-swapping
the running engine's declared params/metrics so switching profiles needs no
restart. Builds out the NavMenu frontend with Settings (colour scheme picker,
CSS import/list/delete) and Presets (list/create/switch/delete profiles with
declaration counts) panels, and a useSettingsPanel hook that applies the
active colour scheme and imported CSS to the document live.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 13:21:31 +01:00