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
Alerts, Metrics, Params, and Dashboards now toggle collapsed state when
their header row is clicked, instead of requiring a dedicated collapse
button. The chevron (CollapseToggle) becomes a purely visual, non-focusable
indicator for these sections; HelpIcon and PopOutButton now stop
propagation so they keep working without also collapsing the section.
The log viewer's toggle (App.tsx/FilterBar) keeps its dedicated button,
since that row is a filter toolbar with an editable input rather than a
plain title bar, so CollapseToggle's onToggle prop stays available for
that case.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
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
The backend already stored both under one CssEntry list (Imported/Linked,
differing only by the is_theme flag), but the NavMenu Settings panel split
them into two visually-duplicated sections — "Custom themes" with its own
upload/link forms and list, and "Import CSS"/"Configured CSS" with a nearly
identical upload form and a separate list — and useSettingsPanel exposed
parallel importCssEntry/importThemeEntry and (theme-only) linkThemeEntry
wrappers around the same underlying API calls.
Consolidate to one "CSS" section: a single list of all CssEntries (each
tagged "theme" and selectable when is_theme is set), one import form and
one link form, each with a "Use as theme" checkbox instead of a duplicate
theme-only form. useSettingsPanel now exposes a single importCssEntry(name,
content, isTheme) and a generic linkCssEntry(url, isTheme) — the latter also
fixes the prior asymmetry where linking (vs. uploading) was only available
for themes, not arbitrary CSS.
No backend or settings-schema changes were needed: is_theme already
persists correctly, so existing custom-theme and imported-CSS
configuration keeps working unmodified under the unified UI.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
"grey" and "dark" rendered identically apart from accent colour, so
drop "grey" as a selectable colour scheme everywhere in the frontend
(index.css, api.ts's ColorScheme type, the NavMenu picker, the
downloadable BUILTIN_THEMES table) and keep "dark" as the single
equivalent theme.
Existing settings.json files saved with color_scheme: "grey" keep
loading correctly: the backend ColorScheme enum aliases the legacy
"grey" serde value onto Dark, so old configs transparently migrate to
"dark" without any manual intervention.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
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
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
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
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
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
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
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
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
--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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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