diff --git a/WISHLIST.md b/WISHLIST.md deleted file mode 100644 index f7ddb89..0000000 --- a/WISHLIST.md +++ /dev/null @@ -1,8 +0,0 @@ -Use fs-embed crate for bundling the webapp. It should not be a clap arg (remove --static-dir arg). -Remove unused crates and deps etc -Reduce all comments to very clear direct and simple 1-liners if necessary. If the comment is not necessary, completely remove the comment. The code should be clear to read. The same goes for the docs in the webapp. -The timeline should have markers at the start and end indicating sequence and timestamp. Hovering over the timeline should also indicate what is under the cursor (next before the cursor). When dragging across the timeline, the start and end of the drag should be updated realtime as well. -Themes: light and dark are black and white, but inverted. Grey is greyscale. All the colours should be a greyscale equivalent of the shades they are representing. Almost all elements should be in that colour - background, component borders, ui elements etc. They should have complementary colours for accents. -It should be possible to download a theme, so that it can be modified and reuploaded as a custom theme. - - diff --git a/tickets/017-ntfy-alerts.md b/tickets/017-ntfy-alerts.md index b00baed..a06c420 100644 --- a/tickets/017-ntfy-alerts.md +++ b/tickets/017-ntfy-alerts.md @@ -1,24 +1,49 @@ -# 017 — Ntfy.sh alerts (future idea) - -> The README lists this under "Ideas... that can be implemented in the future, but -> not now." Not scoped for current implementation — tracked here so it isn't lost. +# 017 — Alert definitions & settings persistence ## Summary -Let users configure matchers of interest and an ntfy.sh channel, and send a -notification to that channel when a matching event occurs. +Foundation for the alerting feature: a persisted `AlertRule` model plus CRUD, +following the shape `Settings` already uses for `CssEntry`/`Profiles` +([008](008-settings-model.md)) — one process-wide list, saved to disk on every +mutation. -## Scope (sketch, not final) +An `AlertRule` has: -- Matcher definition, likely reusing the FilterBar query language - ([005](005-filterbar-pest-parser.md)) or the metric declaration language - ([002](002-metric-declarations.md)) rather than inventing a third syntax. -- Ntfy channel config, persisted via settings ([008](008-settings-model.md)). -- Trigger evaluation on log ingest (engine-side, to avoid re-scanning) or on bucket - update, sending an HTTP request to the configured ntfy.sh channel. +- A **condition** (see [018](018-alert-trigger-evaluation.md) for the types and + how they're evaluated) — for this ticket, just store it as an opaque, already + parsed/validated value; don't implement evaluation here. +- A **sink**: for now, only `Ntfy { topic: String, server: Option }` + (`server` defaults to `https://ntfy.sh` when absent) — modeled as an enum so + [additional sinks](020-alerts-webapp-ui.md) can be added later without + reshaping `AlertRule`. +- Bookkeeping: `id`, `name`, `enabled: bool`. + +## Scope + +- `AlertRule` struct (or similar) in a new `src/alerts.rs`, `Vec` + added to `Settings` (`src/settings.rs`), persisted the same way + `css_entries`/`profiles` already are. +- CRUD methods on `Settings` (create/update/delete/list), mirroring + `import_css`/`remove_css`'s pattern: mutate in memory, then `self.save(...)`. +- HTTP endpoints in `src/api.rs` for CRUD, matching the existing settings + endpoints' conventions (routes, status codes, error JSON shape). +- Id generation: reuse the approach in `generate_imported_css_id` (hash + name+content+timestamp) rather than adding a `uuid` dependency. + +## Acceptance criteria + +- Alert rules round-trip through `save`/`load` like the rest of `Settings` + (see `round_trips_through_save_and_load` for the existing pattern to extend). +- CRUD endpoints create, list, update, and delete rules, and reject unknown + ids the same way `CssError::NotFound` does today. +- No evaluation or delivery logic lives here — this ticket only stores rules. ## Depends on - [008 — Settings model](008-settings-model.md) -- [005 — FilterBar pest parser](005-filterbar-pest-parser.md) (likely, for matcher - syntax reuse) + +## Depended on by + +- [018 — Alert trigger evaluation](018-alert-trigger-evaluation.md) +- [019 — Ntfy sink delivery](019-ntfy-sink-delivery.md) +- [020 — Alerts webapp UI](020-alerts-webapp-ui.md) diff --git a/tickets/018-alert-trigger-evaluation.md b/tickets/018-alert-trigger-evaluation.md new file mode 100644 index 0000000..1e483f3 --- /dev/null +++ b/tickets/018-alert-trigger-evaluation.md @@ -0,0 +1,56 @@ +# 018 — Alert trigger evaluation + +## Summary + +Evaluate `AlertRule` conditions ([017](017-ntfy-alerts.md)) against the live log +stream and fire a delivery ([019](019-ntfy-sink-delivery.md)) when one matches, +without re-scanning already-ingested logs — hook into the same ingest path +`engine.rs` already uses to update timelines/metrics/dashboards on each new +log. + +## Scope + +Condition types to support, all built on primitives ottyl already has (the +FilterBar query language, [005](005-filterbar-pest-parser.md), and metric +declarations, [002](002-metric-declarations.md)) rather than a new syntax: + +- **Immediate** — fire once per log line matching a FilterBar query, as soon + as it's ingested. +- **Threshold** — a declared metric's value, or a FilterBar query's match + count, crosses a fixed level within a rolling time window. +- **New value** — a specified log field takes on a value that hasn't appeared + before (within the alert's lifetime, or a bounded lookback — decide and + document which). +- **Ratio** — the match counts of two FilterBar queries, compared over the + same rolling window, cross a configured ratio. + +Each condition stores its own window/threshold parameters (already sketched as +an opaque value on `AlertRule` in 017 — this ticket defines and parses that +shape). + +- Evaluation runs inline on ingest (or on bucket update, whichever the engine + already does per-log) — no polling loop and no periodic full re-scan. +- Rolling-window state (counts, last-seen values) is kept in memory per rule; + it does not need to survive a restart. +- On a match, hand off to the configured sink ([019](019-ntfy-sink-delivery.md)) + — this ticket owns *deciding* to fire, not *delivering*. +- Respect `AlertRule.enabled`: disabled rules are skipped entirely. + +## Acceptance criteria + +- Each of the four condition types fires exactly when its condition is met, + and not otherwise (covered by unit tests per type, feeding synthetic logs + through the same ingest path). +- Evaluation cost is proportional to new logs ingested, not to total log + volume — no scan of the full log history per evaluation. +- A disabled rule never fires. + +## Depends on + +- [017 — Alert definitions & settings persistence](017-ntfy-alerts.md) +- [005 — FilterBar pest parser](005-filterbar-pest-parser.md) +- [002 — Metric declarations](002-metric-declarations.md) + +## Depended on by + +- [019 — Ntfy sink delivery](019-ntfy-sink-delivery.md) diff --git a/tickets/018-sequence-diagram.md b/tickets/018-sequence-diagram.md deleted file mode 100644 index 55ceee0..0000000 --- a/tickets/018-sequence-diagram.md +++ /dev/null @@ -1,28 +0,0 @@ -# 018 — Sequence diagram of events (future idea) - -> The README lists this under "Ideas... that can be implemented in the future, but -> not now." Not scoped for current implementation — tracked here so it isn't lost. - -## Summary - -Let users declare sequence-diagram participants from params or matchers (e.g. -`node_id`), then render a sequence diagram of captured events of interest. No -relation between nodes is modeled — only event sequence/ordering determines the -diagram. - -## Scope (sketch, not final) - -- Participant declaration referencing an existing param - ([001](001-param-declarations.md)), e.g. "one participant per distinct value of - `node_id`". -- Event-of-interest selection, likely reusing FilterBar syntax - ([005](005-filterbar-pest-parser.md)) to define which log lines become diagram - events. -- Rendering: a sequence-diagram view (e.g. mermaid `sequenceDiagram`) driven by - ordered matching events per participant. - -## Depends on - -- [001 — Param declarations](001-param-declarations.md) -- [005 — FilterBar pest parser](005-filterbar-pest-parser.md) (likely, for - event-matcher syntax reuse) diff --git a/tickets/019-default-port-zero.md b/tickets/019-default-port-zero.md deleted file mode 100644 index 1f0bee9..0000000 --- a/tickets/019-default-port-zero.md +++ /dev/null @@ -1,29 +0,0 @@ -# 019 — Default port should be 0 - -## Summary - -`--port` currently defaults to `8420` (`Args` in `main.rs`). The wishlist asks for a -default of `0`, i.e. let the OS assign an ephemeral free port rather than colliding -with whatever else is running on a fixed port. - -## Scope - -- Change `Args::port`'s default to `0`. -- After `TcpListener::bind` succeeds, read back the actual bound port (via - `listener.local_addr()`) since binding to `0` means the requested port and the - real one differ. -- Print the real bound port in the startup log line (it already prints - `args.port`, which would otherwise always read `0`). -- Consider whether the actual port needs to be surfaced anywhere else (e.g. a - `GET /api/stats`-style endpoint) — not required unless something already depends - on knowing it programmatically today; check before adding. - -## Acceptance criteria - -- Running `ottyl` with no `--port` binds an OS-assigned free port instead of a fixed - one, and the startup message prints that actual port. -- `--port ` still binds the requested fixed port as before. - -## Depends on - -None. diff --git a/tickets/019-ntfy-sink-delivery.md b/tickets/019-ntfy-sink-delivery.md new file mode 100644 index 0000000..5f0bcb3 --- /dev/null +++ b/tickets/019-ntfy-sink-delivery.md @@ -0,0 +1,44 @@ +# 019 — Ntfy sink delivery + +## Summary + +Deliver a fired alert ([018](018-alert-trigger-evaluation.md)) to the +[ntfy](https://ntfy.sh) topic configured on its `AlertRule` ([017](017-ntfy-alerts.md)): +a plain HTTP POST of the notification body to `{server}/{topic}`, no SDK +needed. + +## Scope + +- A small async sender: `POST {server}/{topic}` with the alert's message as + the request body (ntfy's plain-text publish API — no auth, no extra + dependency beyond an HTTP client ottyl can already reach for, or a minimal + one if none exists yet). +- Message content: rule name plus enough context to be useful (matched log + line for Immediate/New value, current value vs. threshold for + Threshold/Ratio). +- Delivery runs off the ingest hot path — spawn it (e.g. + `tokio::spawn`/an mpsc queue) so a slow or unreachable ntfy server never + blocks log ingestion. +- Failure handling: log the error to stderr and drop the notification — no + retry queue or backoff for this ticket (matches the project's existing + "best-effort, log and move on" style, e.g. `remove_css`'s file-removal + fallback). + +## Acceptance criteria + +- A fired alert results in exactly one HTTP POST to the rule's configured + `{server}/{topic}`. +- Ntfy being unreachable/erroring is logged but does not panic, block + ingestion, or crash the process. +- Covered by a test against a local mock HTTP server asserting the request + path/body. + +## Depends on + +- [017 — Alert definitions & settings persistence](017-ntfy-alerts.md) +- [018 — Alert trigger evaluation](018-alert-trigger-evaluation.md) + +## Depended on by + +- [020 — Alerts webapp UI](020-alerts-webapp-ui.md) (for a "send test + notification" action) diff --git a/tickets/020-alerts-webapp-ui.md b/tickets/020-alerts-webapp-ui.md new file mode 100644 index 0000000..14339a9 --- /dev/null +++ b/tickets/020-alerts-webapp-ui.md @@ -0,0 +1,41 @@ +# 020 — Alerts webapp UI + +## Summary + +A webapp page for managing `AlertRule`s ([017](017-ntfy-alerts.md)) end to +end: create/edit/enable/delete rules and configure their ntfy sink, without +touching `settings.json` by hand. + +## Scope + +- A new route/panel (matching how `Dashboards`/`Metrics` are already surfaced + in `App.tsx`) listing existing alert rules: name, condition summary, + enabled state. +- A rule editor: + - Condition type picker (Immediate / Threshold / New value / Ratio, see + [018](018-alert-trigger-evaluation.md)) with type-specific fields, reusing + `FilterBar`'s query input for the query-based conditions rather than a + separate text field. + - Ntfy sink fields: topic and optional server override. + - Enabled toggle. +- "Send test notification" button wired to the sink delivery endpoint from + [019](019-ntfy-sink-delivery.md), so a topic/server can be verified without + waiting for a real trigger. +- Contextual "?" help icons on the condition-type picker and sink fields, + matching [021](021-docs-contextual-help.md)'s pattern. + +## Acceptance criteria + +- Rules created/edited/deleted in the UI persist across a page reload (i.e. + round-trip through the CRUD endpoints from 017). +- Each condition type's fields validate before save (e.g. Ratio requires two + queries and a threshold). +- "Send test notification" delivers to the currently-entered topic/server + without requiring the rule to be saved first. + +## Depends on + +- [017 — Alert definitions & settings persistence](017-ntfy-alerts.md) +- [018 — Alert trigger evaluation](018-alert-trigger-evaluation.md) +- [019 — Ntfy sink delivery](019-ntfy-sink-delivery.md) +- [021 — Docs contextual help](021-docs-contextual-help.md) diff --git a/tickets/020-file-input-ingestion.md b/tickets/020-file-input-ingestion.md deleted file mode 100644 index 3da2d02..0000000 --- a/tickets/020-file-input-ingestion.md +++ /dev/null @@ -1,39 +0,0 @@ -# 020 — File input for ingestion, with restart-safe tailing - -## Summary - -`ingest.rs` only reads stdin today. The wishlist asks for an optional file path as -an alternative input source, plus resilience to the log file being recreated (e.g. -the host process being tailed restarts and truncates/recreates its log file) — -ottyl should keep tailing rather than treating that as EOF-and-done. - -## Scope - -- New `clap` arg (e.g. `--file `), mutually exclusive-in-spirit with stdin: - when given, ingest reads from the file instead of stdin; when absent, behavior is - unchanged (stdin, as today). -- File-mode ingestion must behave like `tail -f`: keep reading as new lines are - appended, don't exit on reaching current EOF. -- Detect file recreation (the common "log rotated / process restarted and reopened - the path" case) — e.g. by tracking the inode/file id and re-opening when it - changes, or noticing truncation (current read offset > file size) — and resume - tailing the new file from its start rather than exiting or getting stuck. -- Sequence numbers continue incrementing across a recreation event (per - `IndexKey::IngestedLog`, sequence is ottyl's own line counter, not tied to the - source file). -- Reuse the existing `IngestedLine` / `EngineMessage::Ingest` plumbing — this only - changes where lines come from, not how they're indexed. - -## Acceptance criteria - -- `ottyl --file some.log` ingests existing content and then picks up appended - lines live. -- Deleting and recreating the file at the same path (simulating a host restart) - does not kill ottyl or stop ingestion — new content from the recreated file - continues to be ingested. -- Running with no `--file` is unchanged: stdin ingestion as today. - -## Depends on - -None — parallels the existing `ingest::run`, doesn't require any of the metrics/ -filter machinery. diff --git a/tickets/021-docs-contextual-help.md b/tickets/021-docs-contextual-help.md deleted file mode 100644 index a9d6ec9..0000000 --- a/tickets/021-docs-contextual-help.md +++ /dev/null @@ -1,35 +0,0 @@ -# 021 — Contextual "?" help icons linking into Docs - -## Summary - -Every major UI section should have a "?" icon that opens the Docs panel scrolled -to (and highlighting) the relevant section, instead of the user having to find it -themselves in the Docs accordion. - -## Scope - -- A small reusable `HelpIcon`/`HelpLink` component: renders a "?" affordance, - accepts a target docs section id, and on click opens the Docs panel - (`NavMenu`'s `docs` panel / `Docs.tsx`) navigated to that section. -- Add one to each major component per the README's block diagram: `FilterBar`, - `LogTimeline`, `LogPreview`, `Metrics`, `Dashboards`, and NavMenu's own - Settings/Presets panels. -- Docs panel: support being opened with a target section id (not just opened to - the top/last-open accordion item) — expand the right accordion section and - scroll it into view. -- "Flash" behavior: when navigated to via a "?" icon, the target paragraph/section - visually flashes (e.g. a brief background-color pulse) so the user's eye finds it - immediately, then settles back to normal styling. - -## Acceptance criteria - -- Clicking a "?" icon on any of the listed components opens Docs, expands the - correct accordion section, scrolls it into view, and it visibly flashes. -- Clicking the same icon twice in a row re-triggers the flash (not just a no-op - because the panel is already open to that section). -- Docs opened normally (via the NavMenu Docs button, not a "?" icon) behaves as - today — no flash, no forced section. - -## Depends on - -None — builds on the existing `Docs.tsx` / NavMenu docs panel. diff --git a/tickets/022-metrics-counters-rethink.md b/tickets/022-metrics-counters-rethink.md deleted file mode 100644 index 38cfcb1..0000000 --- a/tickets/022-metrics-counters-rethink.md +++ /dev/null @@ -1,53 +0,0 @@ -# 022 — Metrics rethink: counters as the primitive, histograms/gauges derived - -## Summary - -The wishlist calls for rethinking the metrics model: everything a metric declares -should fundamentally be a **counter** (an occurrence at a point in time, optionally -labeled). Histograms and gauges are not separate declaration kinds — they're -*derived views* computed from counter data (bucketing counter events into -histogram buckets; reducing counter series into a gauge-style "current value"). -This changes how `MetricDeclaration`/`BucketValue` are modeled and how the -already-shipped PromQL-style query support (`promql.rs`) evaluates -`rate()`/`sum()`/histogram/gauge-shaped queries. - -## Scope - -- Re-express metric declarations as counter declarations: a match increments a - counter for the resolved label set at the matched timestamp. This is close to - today's `BucketValue{values: Vec<(Sequence, Timestamp, MetricPrimitive)>}` — audit - whether the existing bucket representation already is this, or whether captured - non-counter values (e.g. a captured `Number` used as a gauge reading today) need - to be reinterpreted as counter increments plus a separately-derived gauge. - Document the resulting semantics clearly since this is a model change to code - that already exists and has callers (`engine.rs`, `promql.rs`, `Metrics.tsx`, - `Dashboards.tsx`). -- Histogram derivation: given a counter's events over a time range, bucket them - (fixed-width or user-specified boundaries) and expose bucket counts — likely as - a new PromQL-evaluable form (e.g. `histogram(metric_name[param])`) or a query - option, consumed the same way existing series are. -- Gauge derivation: given a counter's events, derive a "last value" / "value at - time T" series from the most recent matching counter event — likely as another - derived query form (e.g. `gauge(metric_name[param])`), rather than a distinct - declaration/storage kind. -- Update `promql.rs`'s evaluator to support these derived forms without breaking - existing raw counter queries. -- Update `Metrics.tsx`'s declaration form/help text and `Dashboards.tsx`'s panel - config if the query surface changes. - -## Acceptance criteria - -- A declared metric behaves as a counter: each match is an increment/event, not an - arbitrary overwritten value. -- A histogram-derived query against a counter with several distinct captured - values buckets them correctly across a seeded fixture. -- A gauge-derived query against a counter returns the most-recent value at a given - point in the selected time range. -- Existing raw counter queries (sum/rate over a plain counter, as today) continue - to work unchanged. - -## Depends on - -None — this is a rework of the already-shipped metric/bucket/PromQL code, not new -foundational work. [023](023-duration-param.md), [025](025-default-dashboard-seed.md) -build on the resulting model. diff --git a/tickets/023-duration-param.md b/tickets/023-duration-param.md deleted file mode 100644 index 06b3fa3..0000000 --- a/tickets/023-duration-param.md +++ /dev/null @@ -1,37 +0,0 @@ -# 023 — Duration-type parameter - -## Summary - -Support a `Duration` parameter kind: its value is the time elapsed since the -previous trigger of the *same* metric with the *same* labels/params, measured -using whichever timestamp field the user has configured (ingestion time by -default, or a param-derived timestamp). - -## Scope - -- New `MetricPrimitive`/param-kind variant for durations (e.g. milliseconds as an - `i64`, consistent with how `Timestamp` is already stored). -- Derivation rule: for a metric match with a given label set, look up that same - label set's previous counter event ([022](022-metrics-counters-rethink.md)) and - compute `this_event_time - previous_event_time` using the configured time - source (ingestion timestamp, or a declared timestamp param per the README's - "deriving a timestamp different from the ingested timestamp" note). -- First occurrence of a given label set has no previous event — define and - document the behavior (e.g. omit the duration value, or emit `null`/zero; - omitting is likely correct so it doesn't pollute histograms with a bogus zero). -- Expose duration values the same way other derived/counter data is queried - (bucket lookup, PromQL, Metrics UI) so it composes with existing dashboards. - -## Acceptance criteria - -- Two log lines matching the same metric+labels, seconds apart, produce a - duration value equal to that elapsed time on the second occurrence. -- The first occurrence of a given label set produces no duration value (not a - zero or garbage value). -- Duration values are queryable via the same bucket/PromQL surface as other - metric data. - -## Depends on - -- [022 — Metrics rethink](022-metrics-counters-rethink.md) (durations are computed - between successive counter events of the same metric). diff --git a/tickets/024-time-range-controls.md b/tickets/024-time-range-controls.md deleted file mode 100644 index 899ca66..0000000 --- a/tickets/024-time-range-controls.md +++ /dev/null @@ -1,41 +0,0 @@ -# 024 — Time range controls (Grafana-style presets) - -## Summary - -Today the only way to set the active time range is dragging over `LogTimeline` -(`App.tsx`'s `range` state, passed to `LogPreview`/`Metrics`/`Dashboards` as -`startMs`/`endMs`). The wishlist wants explicit controls like Grafana's: an "All" -option and relative presets ("last 5 minutes", etc.), not just drag-to-select. - -## Scope - -- A time-range control component (dropdown or button row) offering at least: - "All" (no range — today's `range === null` default) and a handful of relative - presets (e.g. last 5m / 15m / 1h / 24h), placed near `LogTimeline` alongside the - existing drag-select interaction. -- Selecting a preset computes `{startMs, endMs}` relative to now and feeds the - same `range` state `App.tsx` already threads through to `LogTimeline`, - `LogPreview`, `Metrics`, and `Dashboards` — no new plumbing needed downstream, - since all four already accept/consume a range. -- Relative presets need to "move with time" while active (e.g. "last 5 minutes" - should mean *rolling* last 5 minutes, not a frozen window from selection time) — - decide and implement a refresh cadence (e.g. re-resolve the relative range on - the same interval the app already polls for new data) versus a one-shot - resolved range; document the choice. -- Manually dragging on `LogTimeline` after a preset is active should override it - back to a fixed (non-rolling) range, same as today. - -## Acceptance criteria - -- Selecting "All" clears the range the same way today's implicit default does. -- Selecting a relative preset narrows `LogPreview`, `LogTimeline`, `Metrics`, and - `Dashboards` to that window. -- A rolling preset's window visibly advances over time without user interaction, - if that's the chosen behavior (see Scope). -- Dragging on `LogTimeline` while a preset is active switches back to a fixed - range bounded by the drag. - -## Depends on - -None — the underlying range plumbing (`App.tsx` → `LogTimeline`/`LogPreview`/ -`Metrics`/`Dashboards`) already exists; this only adds the control surface. diff --git a/tickets/025-default-dashboard-seed.md b/tickets/025-default-dashboard-seed.md deleted file mode 100644 index df141a1..0000000 --- a/tickets/025-default-dashboard-seed.md +++ /dev/null @@ -1,39 +0,0 @@ -# 025 — Seed a default dashboard with 2 default panels - -## Summary - -`ProfileStore::ensure_default` creates the `Default` profile with no dashboards. -The wishlist wants every fresh Default profile to come with a starter dashboard: -a line chart of "error" vs. non-"error" log volume, and a pie chart of the same -ratio. - -## Scope - -- On `ensure_default` creating a brand-new `Default` profile (not on every - startup — only when the profile itself is newly created), seed one - `Dashboard` with two panels: - - Line chart: log message count over time, split into "contains `error`" vs. - "does not contain `error`" series. - - Pie chart: same split, as a ratio/total-count breakdown. -- Express both panels as PromQL-style queries against a counter - ([022](022-metrics-counters-rethink.md)) — this likely requires an implicit - built-in "log matched regex X" counter concept (e.g. auto-declaring a metric - for `regex("error")` vs. its complement), or a raw-text regex count primitive - in the PromQL surface if one doesn't already exist. Decide and document which. -- Seeded dashboard/panels must persist through the normal profile save path (same - as any user-created dashboard) so it's editable/deletable like any other. - -## Acceptance criteria - -- A fresh install (no existing settings/profile store) starts up with a `Default` - profile that already has one dashboard containing the line and pie panels - described above, without any user action. -- Restarting with an already-existing `Default` profile does not re-seed or - duplicate the dashboard. -- The seeded panels render real data against a log stream containing a mix of - "error" and non-"error" lines. - -## Depends on - -- [022 — Metrics rethink](022-metrics-counters-rethink.md) (defines what a - "count of matching logs" counter/query looks like). diff --git a/tickets/026-max-logs-in-memory.md b/tickets/026-max-logs-in-memory.md deleted file mode 100644 index 95a5059..0000000 --- a/tickets/026-max-logs-in-memory.md +++ /dev/null @@ -1,34 +0,0 @@ -# 026 — Settings: maximum logs kept in memory - -## Summary - -ottyl currently keeps every ingested log forever in the in-memory `BTreeMap`. The -wishlist wants a settings-configurable cap on how many logs are retained, with no -cap (unlimited) as the default. - -## Scope - -- New `Settings` field (e.g. `max_logs: Option`), `None`/unset = unlimited, - matching the existing "by default it should be unlimited" requirement. -- Enforce the cap in the engine's ingest path: once the `IngestedLog` range - exceeds the configured max, evict the oldest entries (lowest `sequence`) to - bring it back under the cap. -- Decide and document what happens to data that references evicted logs: - - Params/metrics already derived into `Bucket` values from an evicted log are - **not** retroactively removed (buckets are aggregate data, not raw logs) — - state this explicitly so it's not treated as a bug later. - - `sequence` numbering does not reset/renumber on eviction. -- Expose the setting in the Settings API/NavMenu settings panel (a numeric input, - clearable back to "unlimited"). - -## Acceptance criteria - -- With no cap configured, behavior is unchanged (unbounded retention, as today). -- Setting a cap and ingesting past it keeps memory usage bounded — oldest logs - are evicted, newest `max_logs` remain queryable via `GET /api/logs`. -- Changing the cap at runtime (via settings) takes effect on the next ingest - without requiring a restart. - -## Depends on - -None — extends the existing `Settings` model. diff --git a/tickets/027-dashboards-autosave.md b/tickets/027-dashboards-autosave.md deleted file mode 100644 index 0d7c054..0000000 --- a/tickets/027-dashboards-autosave.md +++ /dev/null @@ -1,33 +0,0 @@ -# 027 — Dashboards: remove manual save, autosave everything - -## Summary - -`Dashboards.tsx` currently has an explicit "Save dashboard" button -(`saveDashboard` called on click). The wishlist wants dashboards to always -autosave — no explicit save step, matching how settings/profile changes already -persist immediately elsewhere in the app. - -## Scope - -- Remove the "Save dashboard" button and the `saving`/dirty-draft gating around - it in `Dashboards.tsx`. -- Persist panel/name edits automatically: debounce edits (e.g. text field - keystrokes) so autosave doesn't fire on every keystroke, but don't require any - user action to commit a change. -- Cover all mutations that currently route through the save button: panel - add/remove/edit (title, PromQL, chart type), dashboard rename. -- Surface save failures unobtrusively (e.g. a small inline status/error near the - dashboard, not a blocking dialog) since there's no longer a discrete "Save" - click to attach an error to. - -## Acceptance criteria - -- Editing a panel's title, query, or chart type persists without clicking - anything, verified by reloading the page and seeing the change retained. -- Rapid typing in a text field doesn't spam the save endpoint on every keystroke. -- A failed autosave (e.g. simulated API error) surfaces to the user without - losing the in-progress edit. - -## Depends on - -None — modifies the existing `Dashboards.tsx` save path. diff --git a/tickets/028-fs-embed-static-bundling.md b/tickets/028-fs-embed-static-bundling.md new file mode 100644 index 0000000..bb16e2e --- /dev/null +++ b/tickets/028-fs-embed-static-bundling.md @@ -0,0 +1,37 @@ +# 028 — Bundle the webapp with fs-embed, drop `--static-dir` + +## Summary + +`fs-embed` is already a `Cargo.toml` dependency but unused (`grep -rn +"fs_embed" src/` turns up nothing) — `src/api.rs` still serves the webapp via +`ServeDir`/`ServeFile` pointed at a `--static-dir` CLI arg +(`src/main.rs:35`), which requires shipping `webapp/dist` alongside the +binary. Embed the built webapp into the binary instead so `ottyl` is a single +self-contained executable. + +## Scope + +- Use `fs-embed` to embed `webapp/dist` at build time. +- Wire the embedded filesystem into `src/api.rs` in place of the + `ServeDir::new(static_dir).fallback(ServeFile::new(index_html))` setup + (`src/api.rs:100-101`), preserving the SPA fallback-to-`index.html` + behavior. +- Remove the `--static-dir` clap arg (`src/main.rs:30-35`) and its plumbing + through `api::router` (`src/api.rs:91`, `src/main.rs:108`). +- Update the test helper that builds a router against a temp static dir + (`src/api.rs:729-744`) to match the new embedding approach. +- `webapp/dist` must exist at `cargo build` time (i.e. the webapp build step + stays a prerequisite) — document that in the README's build instructions if + not already covered. + +## Acceptance criteria + +- `cargo build` (with `webapp/dist` present) produces a binary that serves + the webapp with no `--static-dir` flag and no `webapp/dist` directory + needed at runtime. +- `--static-dir` no longer appears in `--help` output or `Args`. +- Existing API tests pass against the embedded-filesystem router. + +## Depends on + +None. diff --git a/tickets/029-remove-unused-deps.md b/tickets/029-remove-unused-deps.md new file mode 100644 index 0000000..c5cb2d4 --- /dev/null +++ b/tickets/029-remove-unused-deps.md @@ -0,0 +1,29 @@ +# 029 — Remove unused crates and dependencies + +## Summary + +Audit `Cargo.toml` and `webapp/package.json` for dependencies that are +declared but not actually used, and remove them. + +## Scope + +- Check each `[dependencies]`/`[dev-dependencies]` entry in `Cargo.toml` + against actual usage (e.g. `cargo +nightly udeps`, or grepping for each + crate's root module name), same for `webapp/package.json` (e.g. `npx + depcheck`). +- Remove anything unused. If a dependency was added ahead of a specific + ticket that hasn't landed yet (e.g. `fs-embed` before + [028](028-fs-embed-static-bundling.md) wires it in), leave it — this ticket + is about dead weight, not blocking in-flight work. +- Re-run the full test suite and a production webapp build after pruning to + confirm nothing silently depended on a "removed" package. + +## Acceptance criteria + +- No dependency remains in either manifest without a corresponding usage in + the codebase. +- `cargo test` and the webapp build both pass after pruning. + +## Depends on + +None. diff --git a/tickets/030-comment-cleanup.md b/tickets/030-comment-cleanup.md new file mode 100644 index 0000000..a870e93 --- /dev/null +++ b/tickets/030-comment-cleanup.md @@ -0,0 +1,35 @@ +# 030 — Cut comments down to clear, direct one-liners + +## Summary + +Pass over the Rust codebase, the webapp source, and the webapp's in-app docs, +trimming every comment/doc-comment to a single clear, direct line where one is +needed at all, and removing it outright where it isn't. + +## Scope + +- `src/*.rs`: many modules currently carry multi-paragraph `///`/`//!` blocks + (e.g. `src/settings.rs`'s module doc and per-method docs). Keep only what + explains a non-obvious *why* (a constraint, an invariant, a workaround); + drop restatements of what the code already says via naming. +- `webapp/src/**`: same treatment for inline comments and any JSDoc-style + blocks. +- The webapp's in-app documentation content (surfaced via the "?" help icons + from [021](021-docs-contextual-help.md)) — reduce prose there the same way: + clear and direct, not verbose. +- This is a mechanical cleanup pass, not a refactor — don't change behavior or + restructure code while doing it; keep it reviewable as a comments-only diff + where possible. + +## Acceptance criteria + +- No multi-line comment blocks remain outside of license/generated headers + (if any). +- Every remaining comment states a non-obvious reason, not a restatement of + the adjacent code. +- `cargo test` and the webapp build/tests still pass (confirms nothing beyond + comments changed). + +## Depends on + +None. diff --git a/tickets/031-timeline-markers-and-hover.md b/tickets/031-timeline-markers-and-hover.md new file mode 100644 index 0000000..1dd650c --- /dev/null +++ b/tickets/031-timeline-markers-and-hover.md @@ -0,0 +1,40 @@ +# 031 — Timeline start/end markers, cursor hover, and live drag readout + +## Summary + +`LogTimeline` (`webapp/src/components/LogTimeline.tsx`) shows a bar per +bucket with only a per-bar `title` tooltip on native hover, and a drag +selection that's silent until `handleMouseUp` fires `onSelectRange`. Add +always-visible start/end labels, a hover readout that tracks the cursor, and +a live-updating readout while dragging. + +## Scope + +- **Start/end markers**: render the sequence index and timestamp of the + first and last visible bucket at the two ends of the timeline (using + `domainStart`/`domainEnd`, already computed at + `LogTimeline.tsx:52-53`), always visible, not hover-only. +- **Cursor hover readout**: on `onMouseMove` (already wired for drag at + `handleMouseMove`, `LogTimeline.tsx:65`), when not dragging, compute the + bucket at or immediately before the cursor via `xToMs` and show its + timestamp/count in a readout near the cursor — replacing reliance on the + native per-bar `title` attribute (`LogTimeline.tsx:135`). +- **Live drag readout**: while `dragging` is true (`LogTimeline.tsx:78`), show + the resolved start/end (`xToMs(dragStartX)`/`xToMs(dragCurrentX)`) updating + on every `handleMouseMove`, not just after `handleMouseUp` resolves the + final range — the drag rectangle already updates live (`selRect`, + `LogTimeline.tsx:79-84`); this adds the numeric readout next to it. + +## Acceptance criteria + +- The timeline always shows the sequence/timestamp of its first and last + bucket, without needing to hover. +- Moving the mouse over the timeline (not dragging) shows the timestamp/count + of the bucket under (or just before) the cursor, updating as the mouse + moves. +- While dragging, the displayed start/end timestamps update continuously as + the drag extends, matching the already-live selection rectangle. + +## Depends on + +None — builds on the existing `LogTimeline` drag/hover plumbing. diff --git a/tickets/032-greyscale-theme-rework.md b/tickets/032-greyscale-theme-rework.md new file mode 100644 index 0000000..3bc7d5b --- /dev/null +++ b/tickets/032-greyscale-theme-rework.md @@ -0,0 +1,47 @@ +# 032 — Rework colour themes as greyscale equivalents with accent-only colour + +## Summary + +Today's colour themes (`webapp/src/index.css`) only vary `--c-accent`/ +`--c-accent-soft`/`--c-accent-text` per theme (`html.red`, `.green`, `.blue`, +`.yellow`, `.pink`, `.orange` at lines 97-130) — every theme shares the same +dark `:root` surfaces/borders/text. `html.grey` (lines 75-95) is a Tailwind +slate palette, which reads blue-tinted rather than neutral. The wishlist +wants: `light`/`dark` as true black-and-white inverses of each other, `grey` +as true neutral greyscale, and every other named theme (`red`/`green`/etc.) +to desaturate its named hue to the matching greyscale lightness for +surfaces/borders/text, reserving actual saturated colour for accents only. + +## Scope + +- `:root` (dark) and `html.light`: convert to a true black/white greyscale + scale (surfaces, borders, text all `#000`–`#fff` greys, inverted between + the two), replacing the current warm/cool-tinted neutrals. +- `html.grey`: replace the slate-derived palette (lines 76-90) with the same + neutral greyscale scale as dark/light (i.e. `grey` becomes the canonical + greyscale theme other themes derive from), keeping only its own accent + distinct if one is still wanted. +- `red`/`green`/`blue`/`yellow`/`pink`/`orange`: for each, desaturate that + hue to compute a matching greyscale lightness scale for + `--c-surface-*`/`--c-border-*`/`--c-text-*` (e.g. convert the hue to HSL, + zero the saturation, keep the lightness steps), then keep that theme's + actual saturated colour only on `--c-accent*`. +- Document the derivation (e.g. a short comment or a small script/table) so a + new themed variant can be added consistently later. + +## Acceptance criteria + +- `light` and `dark` are literal greyscale inverses of each other (every + `--c-*` value in one is the other's lightness mirrored, no hue). +- `grey` uses a neutral (zero-saturation) scale, not a blue-tinted one. +- Each named colour theme's surfaces/borders/text render as neutral greys at + a lightness matching that colour's own luminance, with the named colour + visible only via `--c-accent*`. + +## Depends on + +None. + +## Depended on by + +- [033 — Downloadable/uploadable custom themes](033-custom-theme-download-upload.md) diff --git a/tickets/033-custom-theme-download-upload.md b/tickets/033-custom-theme-download-upload.md new file mode 100644 index 0000000..405cc2e --- /dev/null +++ b/tickets/033-custom-theme-download-upload.md @@ -0,0 +1,42 @@ +# 033 — Downloadable and re-uploadable custom themes + +## Summary + +Let a user export the active theme's `--c-*` variable values as a file, edit +it, and load it back in as a custom theme — reusing the CSS import/link +machinery `Settings` already has (`CssEntry`, `import_css`/`link_css` in +`src/settings.rs`) rather than inventing a parallel storage mechanism. + +## Scope + +- **Download**: given a theme name (one of the built-ins from + [032](032-greyscale-theme-rework.md), or a previously-uploaded custom one), + produce a small CSS (or JSON) file containing just its `--c-*` variable + values, served for download from the webapp's theme picker. +- **Upload**: accept such a file back, validate it defines the expected set + of `--c-*` variables, and register it as a selectable custom theme — + persisted via the existing `import_css`-style flow (a new CSS entry scoped + under a `html.` selector, or a dedicated theme entry if that + shape doesn't fit `CssEntry` cleanly — decide during implementation which + reuses more of the existing settings model). +- **Linked** custom themes: since `CssEntry::Linked` already models + "reference a URL, don't own the content," a custom theme can also be + registered by URL instead of upload, consistent with how linked CSS works + today. +- Theme picker UI: list built-in themes alongside any uploaded/linked custom + ones, each with a download action. + +## Acceptance criteria + +- Downloading a theme produces a file that, re-uploaded unmodified, recreates + the same theme. +- Editing a downloaded theme's variable values and re-uploading it registers + a distinct selectable theme without touching the built-ins. +- A linked (URL) custom theme behaves like linked CSS today: not deletable + via the "remove" action that applies to uploaded ones, per + `CssError::NotImported`'s existing semantics. + +## Depends on + +- [032 — Greyscale theme rework](032-greyscale-theme-rework.md) +- [008 — Settings model](008-settings-model.md) (`CssEntry` import/link flow) diff --git a/tickets/034-params-declaration-ui.md b/tickets/034-params-declaration-ui.md new file mode 100644 index 0000000..5be790b --- /dev/null +++ b/tickets/034-params-declaration-ui.md @@ -0,0 +1,41 @@ +# 034 — Params declaration UI + +## Summary + +`POST /api/params` (ticket 004) already lets a param be declared — name, +`message_match_regex`, `capture_group`, `kind` — and `GET /api/params` +(`fetchParams`, `webapp/src/lib/api.ts:245`) already feeds LogPreview's +"Columns" picker (`webapp/src/components/LogPreview.tsx:63`) so a declared +param can be shown as a log table column. But nothing in the webapp can +*create* one — the only way to declare a param today is a raw `POST +/api/params` call. Add a UI for it, following the same declare-then-list +pattern `Metrics` already uses for `POST /api/metrics` +(`webapp/src/components/Metrics.tsx`). + +## Scope + +- A "Params" section in the primary UI (alongside Log viewer / Metrics / + Dashboards in `App.tsx`), listing currently-declared params (name, regex, + capture group, kind) via `fetchParams`. +- A declare form: name, `message_match_regex`, `capture_group`, kind + (string/number/timestamp — mirror `MetricPrimitiveKind`), submitting via a + new `declareParam` call to `POST /api/params`. +- Surface `400` validation errors from the backend (bad regex, unknown + capture group) inline on the form, matching how `Metrics`' declare form + reports its own validation errors. +- After a successful declare, refresh the params list so it (and + LogPreview's Columns picker) immediately reflects the new param without a + page reload. +- Contextual "?" help icon on the section, matching [021](021-docs-contextual-help.md)'s pattern. + +## Acceptance criteria + +- A param can be declared from the UI and immediately appears in LogPreview's + Columns picker without a page reload. +- An invalid regex or unknown capture group shows the backend's error message + on the form instead of failing silently. +- The declared-params list matches `GET /api/params` after a page reload. + +## Depends on + +- [021 — Docs contextual help](021-docs-contextual-help.md) diff --git a/tickets/035-collapsible-sections.md b/tickets/035-collapsible-sections.md new file mode 100644 index 0000000..0faa006 --- /dev/null +++ b/tickets/035-collapsible-sections.md @@ -0,0 +1,34 @@ +# 035 — Collapsible primary UI sections + +## Summary + +`App.tsx` stacks the Log viewer (`LogTimeline` + `LogPreview`), Params +([034](034-params-declaration-ui.md)), `Metrics`, and `Dashboards` vertically +with no way to collapse any of them. On a small screen — or just when only +one section is being worked on — the others take up space and force +scrolling. Make each section collapsible. + +## Scope + +- Each of the four sections (Log viewer, Params, Metrics, Dashboards) gets a + collapse/expand toggle in its header, next to its existing "?" help icon. +- Collapsed state persists across reloads per-section, via `localStorage` + (same pattern as `LogPreview`'s `VISIBLE_PARAM_COLUMNS_STORAGE_KEY` at + `webapp/src/components/LogPreview.tsx:13`). +- A collapsed section still shows its header (title + toggle), just not its + body/content. +- Collapsing a section must not tear down state that would be lost/reset on + re-expand (e.g. `Dashboards`' in-progress panel edits, `Metrics`' declare + form) — hide with CSS/conditional render at the container level, not by + unmounting stateful subtrees. + +## Acceptance criteria + +- Each section can be independently collapsed and re-expanded. +- Reloading the page preserves each section's collapsed/expanded state. +- Collapsing a section with unsaved in-progress input (e.g. a half-filled + Metrics declare form) and re-expanding it preserves that input. + +## Depends on + +- [034 — Params declaration UI](034-params-declaration-ui.md) diff --git a/tickets/036-section-open-in-new-tab.md b/tickets/036-section-open-in-new-tab.md new file mode 100644 index 0000000..b568a73 --- /dev/null +++ b/tickets/036-section-open-in-new-tab.md @@ -0,0 +1,42 @@ +# 036 — Open a section in its own tab + +## Summary + +`App.tsx` has no routing at all — `NavMenu`, `FilterBar`, `LogTimeline` + +`LogPreview`, Params ([034](034-params-declaration-ui.md)), `Metrics`, and +`Dashboards` all render together in one fixed layout +(`webapp/src/App.tsx:94-108`). Add a small icon next to each section's +collapse toggle ([035](035-collapsible-sections.md)) that opens *just that +section* in a new browser tab — no `NavMenu`, no other sections — for +pop-out/multi-monitor use. + +## Scope + +- A "pop out" icon button in each of the four section headers (Log viewer, + Params, Metrics, Dashboards), alongside the collapse toggle and "?" help + icon. +- Clicking it opens a new tab (`window.open`, not same-tab navigation) at a + URL identifying that section, e.g. `?section=metrics`. +- Minimal client-side routing in `App.tsx`/`main.tsx`: when a `section` + query param is present, render only that one section full-page — no + `NavMenu`, no `FilterBar` unless the Log viewer section needs it, no + sibling sections. +- The standalone view keeps the section's own functionality intact (e.g. the + popped-out Metrics tab can still declare metrics; popped-out Dashboards + can still edit/autosave panels) — it's a layout change, not a read-only + view. +- The popped-out tab reflects the current `range` state (time range) at the + moment it's opened, passed via query params, since it has no `LogTimeline` + of its own to select one (unless the Log viewer section itself is what's + popped out). + +## Acceptance criteria + +- Clicking a section's pop-out icon opens a new tab showing only that + section, with no nav bar and no other sections. +- The popped-out section is fully functional (not a static snapshot). +- Closing the popped-out tab has no effect on the original tab's state. + +## Depends on + +- [035 — Collapsible primary UI sections](035-collapsible-sections.md)