Update tickets

This commit is contained in:
Hugh Kaznowski
2026-09-05 16:17:18 +01:00
parent b89fda39a0
commit b8a55b3caa
24 changed files with 528 additions and 391 deletions
-8
View File
@@ -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.
+40 -15
View File
@@ -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<String> }`
(`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<AlertRule>`
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)
+56
View File
@@ -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)
-28
View File
@@ -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)
-29
View File
@@ -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 <n>` still binds the requested fixed port as before.
## Depends on
None.
+44
View File
@@ -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)
+41
View File
@@ -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)
-39
View File
@@ -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 <path>`), 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.
-35
View File
@@ -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.
-53
View File
@@ -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.
-37
View File
@@ -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).
-41
View File
@@ -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.
-39
View File
@@ -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).
-34
View File
@@ -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<usize>`), `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.
-33
View File
@@ -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.
+37
View File
@@ -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.
+29
View File
@@ -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.
+35
View File
@@ -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.
+40
View File
@@ -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.
+47
View File
@@ -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)
@@ -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.<custom-name>` 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)
+41
View File
@@ -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)
+34
View File
@@ -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)
+42
View File
@@ -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)