From 00c21c80322b7b131ba662e972aaff5a8b3c57bb Mon Sep 17 00:00:00 2001
From: Hugh Kaznowski
Date: Sat, 5 Sep 2026 17:02:33 +0100
Subject: [PATCH] Trim comments across Rust and webapp source to direct
one-liners (ticket 030)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
---
src/alerts.rs | 88 +----
src/api.rs | 188 +----------
src/data_value.rs | 26 +-
src/dto.rs | 117 ++-----
src/engine.rs | 333 +++----------------
src/filter_datetime.rs | 59 +---
src/filter_parser.rs | 51 +--
src/index_key.rs | 14 +-
src/ingest.rs | 25 +-
src/main.rs | 16 +-
src/metric_primitive.rs | 14 +-
src/profile.rs | 253 +++-----------
src/promql.rs | 76 +----
src/settings.rs | 125 ++-----
src/timestamp.rs | 6 +-
tickets/030-comment-cleanup.md | 35 --
webapp/src/App.tsx | 36 +-
webapp/src/components/Alerts.tsx | 6 +-
webapp/src/components/CollapsibleSection.tsx | 18 +-
webapp/src/components/Dashboards.tsx | 39 +--
webapp/src/components/Docs.tsx | 142 ++++----
webapp/src/components/FilterBar.tsx | 12 +-
webapp/src/components/HelpIcon.tsx | 6 +-
webapp/src/components/LogPreview.tsx | 12 +-
webapp/src/components/LogTimeline.tsx | 13 +-
webapp/src/components/Metrics.tsx | 14 +-
webapp/src/components/NavMenu.tsx | 8 +-
webapp/src/components/PanelChart.tsx | 23 +-
webapp/src/lib/api.ts | 46 +--
webapp/src/lib/docsNav.tsx | 2 +-
webapp/src/lib/themes.ts | 40 +--
webapp/src/lib/useSettingsPanel.ts | 54 +--
32 files changed, 347 insertions(+), 1550 deletions(-)
delete mode 100644 tickets/030-comment-cleanup.md
diff --git a/src/alerts.rs b/src/alerts.rs
index 9ca9eea..ea8e370 100644
--- a/src/alerts.rs
+++ b/src/alerts.rs
@@ -1,21 +1,6 @@
-//! Alert rules: definitions (ticket 017), trigger evaluation (ticket 018),
-//! and ntfy delivery (ticket 019).
+//! Alert rules: definitions, trigger evaluation, and ntfy delivery.
//!
-//! ## Design: why alert queries only support regex/sequence/timestamp
-//!
-//! `Immediate`/`Threshold`/`Ratio` conditions reuse FilterBar query strings
-//! (see `crate::filter_parser`), evaluated against a *single newly-ingested
-//! line* on `EngineMessage::Ingest` (see `evaluate_alerts`, called from
-//! `engine::run`). `engine.rs`'s own `compile_filter_query` resolves
-//! `metric[param]` comparisons by scanning that metric's *entire* bucket
-//! history up front (`compile_metric_comparison`) — reusing that per alert
-//! check, per ingested line, would make alert evaluation cost proportional to
-//! total log volume rather than to new logs ingested, which this ticket
-//! requires. So alert queries here only support the subset of FilterBar that
-//! evaluates against a single line's own fields — `regex(...)`, `sequence`,
-//! `timestamp`, and `and`/`or` — via `eval_alert_expr` below. A `metric[param]`
-//! comparison in an alert query is rejected with a clear error (logged, the
-//! rule just never fires) rather than silently misbehaving.
+//! Alert queries are restricted to regex/sequence/timestamp — a metric[param] comparison would require a full bucket rescan per line, breaking the proportional-cost guarantee.
use std::collections::hash_map::DefaultHasher;
use std::collections::{HashMap, HashSet, VecDeque};
@@ -34,7 +19,6 @@ use crate::filter_parser::{self, CompareOp, FilterExpr, FilterValue};
use crate::settings::Settings;
use crate::timestamp::Timestamp;
-/// A single alert rule definition (ticket 017).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AlertRule {
pub id: String,
@@ -44,36 +28,23 @@ pub struct AlertRule {
pub sink: AlertSink,
}
-/// What triggers an alert rule (ticket 018).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum AlertCondition {
- /// Fires once per log line matching `query`.
Immediate { query: String },
- /// Fires when `query`'s match count within a rolling `window_ms` crosses
- /// `threshold` (via `op`).
+ /// Fires when `query`'s match count within a rolling `window_ms` crosses `threshold` (via `op`).
Threshold {
query: String,
op: CompareOp,
threshold: f64,
window_ms: i64,
},
- /// Fires the first time `field_regex` matches a line and `capture_group`
- /// captures a value not seen before *for this rule*. Seen values live
- /// only in memory (`AlertRuntimeState::seen_values`) for the process's
- /// lifetime — a restart forgets them, so previously-seen values can fire
- /// again. This is a deliberate simplicity tradeoff (matches the
- /// codebase's general "no restart persistence for in-memory engine
- /// state" pattern), not an oversight.
+ /// Seen values live only in memory — a restart forgets them, so previously-seen values can fire again; a deliberate simplicity tradeoff, not an oversight.
NewValue {
field_regex: String,
capture_group: String,
},
- /// Fires when `count(query_a) / count(query_b)` (rolling over
- /// `window_ms`) crosses `ratio` (via `op`). A zero `count_b` skips
- /// evaluation entirely for that line (rather than treating the ratio as
- /// infinite) — there's no meaningful "actual vs threshold" comparison to
- /// report yet.
+ /// A zero `count_b` skips evaluation for that line rather than treating the ratio as infinite.
Ratio {
query_a: String,
query_b: String,
@@ -83,9 +54,7 @@ pub enum AlertCondition {
},
}
-/// Where a fired alert is delivered (ticket 019). Modeled as an enum (rather
-/// than `AlertRule` holding a bare topic/server) so more sink kinds can be
-/// added later without reshaping `AlertRule`.
+/// Modeled as an enum (rather than bare fields on `AlertRule`) so more sink kinds can be added later without reshaping it.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum AlertSink {
@@ -96,7 +65,6 @@ pub enum AlertSink {
},
}
-/// Error returned by alert-rule mutations/lookups. Mirrors `CssError`'s shape.
#[derive(Debug)]
pub enum AlertError {
NotFound,
@@ -120,9 +88,7 @@ impl From for AlertError {
}
}
-/// Generates an id for a newly-created alert rule. Same approach as
-/// `settings::generate_imported_css_id`: hash the name and current time
-/// rather than pulling in a `uuid` dependency.
+/// Hashes the name and current time rather than pulling in a `uuid` dependency.
fn generate_alert_rule_id(name: &str) -> String {
let mut hasher = DefaultHasher::new();
name.hash(&mut hasher);
@@ -135,7 +101,6 @@ fn generate_alert_rule_id(name: &str) -> String {
}
impl Settings {
- /// Creates a new alert rule, persisting settings to `settings_path`.
pub fn create_alert_rule(
&mut self,
settings_path: &Path,
@@ -150,9 +115,6 @@ impl Settings {
Ok(rule)
}
- /// Replaces an existing alert rule's fields (id stays fixed), persisting
- /// settings to `settings_path`. Returns `AlertError::NotFound` if no rule
- /// with `id` exists.
pub fn update_alert_rule(
&mut self,
settings_path: &Path,
@@ -176,8 +138,6 @@ impl Settings {
Ok(updated)
}
- /// Removes an alert rule by id, persisting settings to `settings_path`.
- /// Returns `AlertError::NotFound` if no rule with `id` exists.
pub fn remove_alert_rule(&mut self, settings_path: &Path, id: &str) -> Result<(), AlertError> {
let index = self
.alert_rules
@@ -190,11 +150,7 @@ impl Settings {
}
}
-/// Per-rule in-memory evaluation state (ticket 018): rolling match-timestamp
-/// windows for `Threshold`/`Ratio`, and seen-values for `NewValue`. Lives
-/// only for the engine process's lifetime (see `AlertCondition::NewValue`'s
-/// doc comment) — not persisted, not backfilled against already-ingested
-/// logs.
+/// Per-rule in-memory evaluation state; lives only for the process's lifetime — not persisted, not backfilled against already-ingested logs.
#[derive(Debug, Default)]
pub struct AlertRuntimeState {
threshold_matches: VecDeque,
@@ -203,9 +159,7 @@ pub struct AlertRuntimeState {
seen_values: HashSet,
}
-/// Drops every entry older than `window_ms` relative to `now_ms` from the
-/// front of a rolling window. `timestamps` is push_back-ordered, so once a
-/// front entry is within the window every entry behind it is too.
+/// `timestamps` is push_back-ordered, so once the front entry is within the window, every entry behind it is too.
fn prune_window(timestamps: &mut VecDeque, now_ms: i64, window_ms: i64) {
while let Some(&front) = timestamps.front() {
if now_ms - front > window_ms {
@@ -216,9 +170,7 @@ fn prune_window(timestamps: &mut VecDeque, now_ms: i64, window_ms: i64) {
}
}
-/// Evaluates a FilterBar query string against a single log line's own fields
-/// only — see this module's doc comment for why `metric[param]` comparisons
-/// aren't supported here.
+/// Evaluates a FilterBar query against a single line's own fields only (metric[param] comparisons aren't supported — see module doc).
fn eval_alert_query(query: &str, sequence: u64, timestamp: Timestamp, text: &str) -> Result {
let expr = filter_parser::parse(query).map_err(|err| err.to_string())?;
eval_alert_expr(&expr, sequence, timestamp, text)
@@ -261,12 +213,7 @@ fn eval_alert_expr(expr: &FilterExpr, sequence: u64, timestamp: Timestamp, text:
}
}
-/// Evaluates every enabled rule's condition against one newly-ingested line,
-/// mutating each rule's runtime state (rolling windows / seen-values) along
-/// the way. Returns `(rule, message)` for every rule that fired — the caller
-/// (`engine::run`) hands each off to `deliver_for_rule`. A malformed query
-/// (bad regex, unsupported metric comparison) is logged to stderr and simply
-/// never matches, rather than panicking or propagating.
+/// A malformed query (bad regex, unsupported metric comparison) is logged and simply never matches, rather than panicking.
pub fn evaluate_alerts(
rules: &[AlertRule],
states: &mut HashMap,
@@ -362,9 +309,7 @@ fn evaluate_condition(
let count_a = state.ratio_a_matches.len() as f64;
let count_b = state.ratio_b_matches.len() as f64;
if count_b == 0.0 {
- // Zero denominator: skip evaluation rather than treat the
- // ratio as infinite — there's nothing meaningful to compare
- // against `ratio` yet.
+ // Zero denominator: skip rather than treat the ratio as infinite.
return None;
}
let observed = count_a / count_b;
@@ -377,10 +322,7 @@ fn evaluate_condition(
}
}
-/// Delivers one fired alert to its rule's configured sink. Errors are logged
-/// to stderr and dropped — no retry queue, matching `remove_css`'s
-/// best-effort style. Called via `tokio::spawn` from the ingest hot path
-/// (`engine::run`) so a slow/unreachable sink never blocks ingestion.
+/// Errors are only logged (no retry queue); spawned off the ingest hot path so a slow/unreachable sink never blocks ingestion.
pub async fn deliver_for_rule(rule: AlertRule, message: String) {
match rule.sink {
AlertSink::Ntfy { topic, server } => {
@@ -392,9 +334,7 @@ pub async fn deliver_for_rule(rule: AlertRule, message: String) {
}
}
-/// Publishes `message` to `{server}/{topic}` via ntfy's plain-text publish
-/// API (no auth, raw body). Standalone and `AlertRule`-free so a future
-/// "send test notification" endpoint can call it directly.
+/// Standalone and `AlertRule`-free so a "send test notification" endpoint can call it directly.
pub async fn deliver_ntfy(server: &str, topic: &str, message: &str) -> Result<(), String> {
let url = format!("{}/{}", server.trim_end_matches('/'), topic);
let response = reqwest::Client::new()
diff --git a/src/api.rs b/src/api.rs
index 116fbd8..c3e4c1f 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -29,8 +29,7 @@ static WEBAPP_DIST: EmbedDir = fs_embed::fs_embed!("webapp/dist");
struct ApiState {
engine: EngineHandle,
settings: SettingsHandle,
- /// Path to the settings JSON file on disk. Imported CSS files are stored
- /// alongside it, in its parent directory (see `ApiState::css_dir`).
+ /// Imported CSS files are stored alongside this path's parent directory (see `ApiState::css_dir`).
settings_path: Arc,
}
@@ -42,13 +41,7 @@ impl ApiState {
.unwrap_or_else(|| PathBuf::from("."))
}
- /// Persists a newly-declared param into the active profile, if any (see
- /// `ProfileStore::record_param_declaration`), so it survives a restart.
- /// A no-op (not an error) when no profile is currently active — the
- /// engine declaration above has already succeeded regardless. Failures
- /// (e.g. I/O) are logged to stderr rather than surfaced to the caller: the
- /// engine declaration is the source of truth for the running process, and
- /// this is a best-effort persistence step layered on top.
+ /// Best-effort: a no-op if no profile is active, and I/O failures are only logged, since the engine declaration is already the source of truth.
fn persist_param_declaration(&self, request: &DeclareParamRequest) {
let Some((store_path, profile_name)) = self.active_profile_location() else {
return;
@@ -76,7 +69,6 @@ impl ApiState {
}
}
- /// The active profile's store path and name, if a profile is currently active.
fn active_profile_location(&self) -> Option<(PathBuf, String)> {
let settings = self.settings.lock().unwrap();
let profile_name = settings.profiles.active_profile.clone()?;
@@ -126,8 +118,7 @@ pub fn router(engine: EngineHandle, settings: SettingsHandle, settings_path: Pat
.fallback(serve_webapp)
}
-/// Serves the webapp embedded in the binary (see `WEBAPP_DIST`), falling
-/// back to `index.html` for any unmatched path so client-side routing works.
+/// Falls back to `index.html` for unmatched paths so client-side routing works.
async fn serve_webapp(uri: axum::http::Uri) -> Response {
let path = uri.path().trim_start_matches('/');
let file = WEBAPP_DIST
@@ -167,11 +158,6 @@ async fn ask(engine: &EngineHandle, query: EngineQuery) -> Result,
Query(params): Query,
@@ -186,8 +172,6 @@ async fn get_logs(
}
}
-/// `GET /api/timeline` — same `filter` query param and error mapping as
-/// `get_logs` above.
async fn get_timeline(
State(state): State,
Query(params): Query,
@@ -210,10 +194,6 @@ async fn get_stats(State(state): State) -> impl IntoResponse {
}
}
-/// `GET /api/params` — lists all declared params (name, regex, capture
-/// group, kind) as `Vec`. Mirrors `list_metrics` below
-/// (ticket 015 needed this for LogPreview's column picker; ticket 004 only
-/// exposed the equivalent for metrics).
async fn list_params(State(state): State) -> impl IntoResponse {
match ask(&state.engine, EngineQuery::ParamDeclarations).await {
Ok(EngineResponse::ParamDeclarations(declarations)) => Json(declarations).into_response(),
@@ -222,14 +202,6 @@ async fn list_params(State(state): State) -> impl IntoResponse {
}
}
-/// `POST /api/params` — declares a param. Body mirrors `DeclareParamRequest`:
-/// `{ "name": "...", "message_match_regex": "...", "capture_group": "...",
-/// "kind": "string" | "number" | "timestamp" }`. On success, also persists
-/// the declaration into the active profile (if any — see
-/// `ApiState::persist_param_declaration`) and returns `201 Created` with the
-/// declaration as `ParamDeclarationDto`. On a validation failure (bad regex,
-/// unknown capture group), returns `400 Bad Request` with
-/// `{ "error": "..." }`.
async fn declare_param(
State(state): State,
Json(body): Json,
@@ -260,8 +232,6 @@ async fn declare_param(
}
}
-/// `GET /api/metrics` — lists all declared metrics (name, regex, params) as
-/// `Vec`.
async fn list_metrics(State(state): State) -> impl IntoResponse {
match ask(&state.engine, EngineQuery::MetricDeclarations).await {
Ok(EngineResponse::MetricDeclarations(declarations)) => Json(declarations).into_response(),
@@ -270,11 +240,6 @@ async fn list_metrics(State(state): State) -> impl IntoResponse {
}
}
-/// `POST /api/metrics` — declares a metric. Body mirrors
-/// `DeclareMetricRequest`: `{ "name": "...", "message_match_regex": "...",
-/// "params": [{ "param_name": "...", "capture_group": "...", "kind": "..." }] }`.
-/// Same success/failure shape as `declare_param` above, persisting into the
-/// active profile via `ApiState::persist_metric_declaration`.
async fn declare_metric(
State(state): State,
Json(body): Json,
@@ -306,9 +271,7 @@ async fn declare_metric(
}
}
-/// Parses the `labels` query param (`"foo=bar,baz=qux"`) into a
-/// `key -> expected value` map. Malformed pairs (no `=`) are skipped rather
-/// than rejected — see `BucketValuesParams` doc comment for the format.
+/// Malformed pairs (no `=`) are skipped rather than rejected.
fn parse_labels_filter(raw: &str) -> BTreeMap {
raw.split(',')
.filter_map(|pair| pair.split_once('='))
@@ -316,9 +279,6 @@ fn parse_labels_filter(raw: &str) -> BTreeMap {
.collect()
}
-/// `GET /api/metrics/:name` — queries bucket values for one metric. Query
-/// params: `labels` (see `parse_labels_filter`), `start_ms`, `end_ms`,
-/// `since_sequence`, `limit` (mirroring `/api/logs`/`/api/timeline`).
async fn get_metric_bucket_values(
State(state): State,
AxumPath(metric_name): AxumPath,
@@ -340,12 +300,6 @@ async fn get_metric_bucket_values(
}
}
-/// `GET /api/query` — evaluates a PromQL query (ticket 013; see `crate::promql` for
-/// the supported grammar) against declared metrics. Query params: `promql` (the
-/// query string), `start_ms`, `end_ms` (bound which points are included, mirroring
-/// `/api/logs`/`/api/metrics/:name`). Returns `Vec` — chart-ready
-/// `labels` + `(timestamp_ms, value)` series — on success (`200 OK`), or
-/// `400 Bad Request` with `{ "error": "..." }` for a malformed query.
async fn run_promql_query(
State(state): State,
Query(params): Query,
@@ -365,10 +319,6 @@ async fn run_promql_query(
}
}
-/// Maps a `CssError` from a settings mutation/lookup to a response status
-/// and message: `NotFound` -> 404, `NotImported` -> 400 (the id is valid but
-/// refers to a linked entry, which this operation doesn't apply to), `Io` ->
-/// 500.
fn css_error_response(err: CssError) -> axum::response::Response {
match err {
CssError::NotFound => (StatusCode::NOT_FOUND, err.to_string()).into_response(),
@@ -377,18 +327,12 @@ fn css_error_response(err: CssError) -> axum::response::Response {
}
}
-/// `GET /api/settings/css` — lists all configured CSS entries (imported and
-/// linked) as `Vec`.
async fn list_css(State(state): State) -> impl IntoResponse {
let settings = state.settings.lock().unwrap();
let entries: Vec = settings.css_entries.iter().map(CssEntryDto::from).collect();
Json(entries).into_response()
}
-/// `POST /api/settings/css` — imports raw CSS content. Body:
-/// `{ "filename": "theme.css", "content": "body { ... }" }`. Stores the
-/// content as a file alongside the settings file, adds a `CssEntry::Imported`
-/// to settings, and returns the new entry as `CssEntryDto` (201 Created).
async fn import_css(
State(state): State,
Json(body): Json,
@@ -414,11 +358,7 @@ async fn import_css(
}
}
-/// `POST /api/settings/css/link` — registers a reference to an external CSS
-/// path/URL. Body: `{ "path_or_url": "https://example.com/style.css" }`.
-/// Content is never copied; the entry is never deletable via
-/// `DELETE /api/settings/css/:id`. Returns the new entry as `CssEntryDto`
-/// (201 Created).
+/// Content is never copied, and the entry can never be deleted via `DELETE /api/settings/css/:id`.
async fn link_css(
State(state): State,
Json(body): Json,
@@ -436,10 +376,6 @@ async fn link_css(
}
}
-/// `DELETE /api/settings/css/:id` — removes an imported CSS entry (file +
-/// settings reference). Rejects with 400 if `id` refers to a `Linked` entry
-/// (never deletable this way) and 404 if `id` is unknown. On success returns
-/// 204 No Content.
async fn remove_css(
State(state): State,
AxumPath(id): AxumPath,
@@ -452,10 +388,7 @@ async fn remove_css(
}
}
-/// `GET /api/settings/css/:id/content` — serves back the raw CSS content of
-/// an imported entry (`content-type: text/css`). Rejects with 400 for a
-/// linked entry (there's no local content to serve — the frontend should use
-/// `path_or_url` directly) and 404 for an unknown id.
+/// Rejects a linked entry with 400 — there's no local content; the frontend should use `path_or_url` directly.
async fn get_css_content(
State(state): State,
AxumPath(id): AxumPath,
@@ -473,8 +406,6 @@ async fn get_css_content(
}
}
-/// Maps an `AlertError` to a response status/message: `NotFound` -> 404,
-/// `Io` -> 500. Mirrors `css_error_response` above.
fn alert_error_response(err: AlertError) -> axum::response::Response {
match err {
AlertError::NotFound => (StatusCode::NOT_FOUND, err.to_string()).into_response(),
@@ -482,18 +413,11 @@ fn alert_error_response(err: AlertError) -> axum::response::Response {
}
}
-/// `GET /api/settings/alerts` — lists all configured alert rules as
-/// `Vec`.
async fn list_alert_rules(State(state): State) -> impl IntoResponse {
let settings = state.settings.lock().unwrap();
Json(settings.alert_rules.clone()).into_response()
}
-/// `POST /api/settings/alerts` — creates an alert rule. Body:
-/// `{ "name": "...", "enabled": true, "condition": { "kind": "immediate" | \
-/// "threshold" | "newvalue" | "ratio", ... }, "sink": { "kind": "ntfy", \
-/// "topic": "...", "server": "..." | null } }`. Returns `201 Created` with
-/// the created rule (including its generated `id`).
async fn create_alert_rule(
State(state): State,
Json(body): Json,
@@ -505,9 +429,6 @@ async fn create_alert_rule(
}
}
-/// `PUT /api/settings/alerts/:id` — replaces an alert rule's fields (id in
-/// the URL is authoritative). Returns the updated rule, or `404 Not Found`
-/// if no rule with `id` exists.
async fn update_alert_rule(
State(state): State,
AxumPath(id): AxumPath,
@@ -520,8 +441,6 @@ async fn update_alert_rule(
}
}
-/// `DELETE /api/settings/alerts/:id` — removes an alert rule. Returns
-/// `204 No Content`, or `404 Not Found` if no rule with `id` exists.
async fn delete_alert_rule(State(state): State, AxumPath(id): AxumPath) -> impl IntoResponse {
let mut settings = state.settings.lock().unwrap();
match settings.remove_alert_rule(&state.settings_path, &id) {
@@ -530,11 +449,7 @@ async fn delete_alert_rule(State(state): State, AxumPath(id): AxumPath
}
}
-/// `POST /api/settings/alerts/test` — delivers a test notification to an
-/// ntfy topic/server without requiring a saved rule, so the sink can be
-/// verified before saving. Body: `{ "topic": "...", "server": "..." | null }`.
-/// Returns `204 No Content` on success, `502 Bad Gateway` if ntfy rejects
-/// or is unreachable.
+/// Delivers a test notification without a saved rule so the sink can be verified before saving.
async fn test_alert_notification(Json(body): Json) -> impl IntoResponse {
let server = body.server.unwrap_or_else(|| "https://ntfy.sh".to_string());
match deliver_ntfy(&server, &body.topic, "Test notification from ottyl").await {
@@ -543,20 +458,13 @@ async fn test_alert_notification(Json(body): Json)
}
}
-/// `GET /api/settings` — returns the subset of `Settings` the NavMenu UI
-/// needs (colour scheme, active profile name) as `SettingsDto`. CSS entries
-/// have their own dedicated list endpoint (`GET /api/settings/css`).
+/// CSS entries have their own endpoint (`GET /api/settings/css`).
async fn get_settings(State(state): State) -> impl IntoResponse {
let settings = state.settings.lock().unwrap();
Json(SettingsDto::from(&*settings)).into_response()
}
-/// `PATCH /api/settings` — updates process-wide settings. Body:
-/// `{ "color_scheme": "light" | "dark" | "system", "max_logs": number |
-/// null }` (omit a field to leave it unchanged; `max_logs: null` clears the
-/// cap back to unlimited). Persists to `settings_path`, pushes a changed
-/// `max_logs` to the engine so it takes effect on the next ingest without a
-/// restart, and returns the updated settings as `SettingsDto`.
+/// `max_logs: null` clears the cap; a changed value is pushed to the engine so it applies without a restart.
async fn update_settings(
State(state): State,
Json(body): Json,
@@ -588,9 +496,6 @@ async fn update_settings(
Json(new_max_logs.0).into_response()
}
-/// Maps a `ProfileError` to a response status/message: `NotFound` -> 404,
-/// `DuplicateName`/`CannotDeleteDefault` -> 400, `Io` -> 500. Mirrors
-/// `css_error_response` above.
fn profile_error_response(err: ProfileError) -> axum::response::Response {
match err {
ProfileError::NotFound => (StatusCode::NOT_FOUND, err.to_string()).into_response(),
@@ -600,10 +505,6 @@ fn profile_error_response(err: ProfileError) -> axum::response::Response {
}
}
-/// `GET /api/settings/profiles` — lists all known profiles with their
-/// param/metric declaration and dashboard counts, as `Vec`.
-/// Use `GET /api/settings`'s `active_profile` field to know which one (if
-/// any) is currently active.
async fn list_profiles(State(state): State) -> impl IntoResponse {
let settings = state.settings.lock().unwrap();
let profiles_dir = profile::resolve_profiles_dir(&settings, &state.settings_path);
@@ -612,10 +513,6 @@ async fn list_profiles(State(state): State) -> impl IntoResponse {
Json(summaries).into_response()
}
-/// `POST /api/settings/profiles` — creates a new, empty profile. Body:
-/// `{ "name": "..." }`. Returns `201 Created` with the new profile as
-/// `ProfileSummaryDto`, or `400 Bad Request` if a profile with that name
-/// already exists.
async fn create_profile(
State(state): State,
Json(body): Json,
@@ -633,11 +530,7 @@ async fn create_profile(
}
}
-/// `DELETE /api/settings/profiles/:name` — deletes a profile. If it was the
-/// active profile, `active_profile` is cleared (but the engine's currently
-/// declared params/metrics are left as-is — they simply won't be reloaded on
-/// the next restart). Returns `204 No Content`, or `404 Not Found` if no
-/// profile with that name exists.
+/// If it was the active profile, `active_profile` is cleared, but the engine's already-declared params/metrics keep running until the next restart.
async fn delete_profile(
State(state): State,
AxumPath(name): AxumPath,
@@ -652,12 +545,7 @@ async fn delete_profile(
}
}
-/// Removes every currently-declared param/metric from the running engine
-/// (via `EngineMessage::RemoveParamDeclaration`/`RemoveMetricDeclaration`),
-/// then replays `profile`'s stored declarations into it (see
-/// `profile::load_into_engine`). Used by `activate_profile` so switching the
-/// active profile takes effect on the running engine immediately, without
-/// requiring a process restart.
+/// Removes every currently-declared param/metric from the engine, then replays `profile`'s declarations into it.
async fn reload_engine_declarations(engine: &EngineHandle, profile: &Profile) {
if let Ok(EngineResponse::ParamDeclarations(params)) =
ask(engine, EngineQuery::ParamDeclarations).await
@@ -690,12 +578,6 @@ async fn reload_engine_declarations(engine: &EngineHandle, profile: &Profile) {
profile::load_into_engine(engine, profile).await;
}
-/// `POST /api/settings/profiles/:name/activate` — sets `name` as the active
-/// profile (persisted, see `ProfileStore::set_active`) and hot-swaps the
-/// running engine's declared params/metrics to match it (see
-/// `reload_engine_declarations`), so the switch is visible without a
-/// restart. Returns the updated settings as `SettingsDto`, or `404 Not
-/// Found` if no profile with that name exists.
async fn activate_profile(
State(state): State,
AxumPath(name): AxumPath,
@@ -723,9 +605,6 @@ async fn activate_profile(
}
}
-/// `GET /api/settings/profiles/:name/dashboards` — lists all dashboards saved
-/// on the profile `name`, as `Vec`. Returns `404 Not Found` if
-/// no profile with that name exists.
async fn list_dashboards(
State(state): State,
AxumPath(name): AxumPath,
@@ -742,10 +621,6 @@ async fn list_dashboards(
}
}
-/// `POST /api/settings/profiles/:name/dashboards` — creates a new, empty
-/// dashboard on profile `name`. Body: `{ "name": "..." }`. Returns
-/// `201 Created` with the new dashboard as `DashboardDto`, or `404 Not
-/// Found` if no profile with that name exists.
async fn create_dashboard(
State(state): State,
AxumPath(name): AxumPath,
@@ -761,9 +636,6 @@ async fn create_dashboard(
}
}
-/// `GET /api/settings/profiles/:name/dashboards/:dashboard_id` — fetches one
-/// dashboard by id. Returns `404 Not Found` if the profile or dashboard
-/// doesn't exist.
async fn get_dashboard(
State(state): State,
AxumPath((name, dashboard_id)): AxumPath<(String, String)>,
@@ -780,11 +652,6 @@ async fn get_dashboard(
}
}
-/// `PUT /api/settings/profiles/:name/dashboards/:dashboard_id` — replaces the
-/// dashboard's name/panels. Body: `{ "name": "...", "panels": [{ "id": "...",
-/// "title": "...", "promql": "...", "chart_type": "bar" | "line" | "pie" }] }`.
-/// Returns the updated dashboard as `DashboardDto`, or `404 Not Found` if the
-/// profile or dashboard doesn't exist.
async fn save_dashboard(
State(state): State,
AxumPath((name, dashboard_id)): AxumPath<(String, String)>,
@@ -805,9 +672,6 @@ async fn save_dashboard(
}
}
-/// `DELETE /api/settings/profiles/:name/dashboards/:dashboard_id` — deletes a
-/// dashboard. Returns `204 No Content`, or `404 Not Found` if the profile or
-/// dashboard doesn't exist.
async fn delete_dashboard(
State(state): State,
AxumPath((name, dashboard_id)): AxumPath<(String, String)>,
@@ -836,10 +700,7 @@ mod tests {
use tokio::sync::mpsc;
use tower::ServiceExt;
- /// Spins up a fresh engine + router pair, backed by a temp settings file,
- /// for one test. Returns the router, the engine handle (so a test can
- /// ingest lines directly without an HTTP route for it), and the
- /// `TempDir` guard (kept alive for the test's duration).
+ /// Spins up a fresh engine + router pair backed by a temp settings file.
fn test_app() -> (Router, EngineHandle, SettingsHandle, tempfile::TempDir) {
let (engine_tx, engine_rx) = mpsc::channel(128);
@@ -937,9 +798,7 @@ mod tests {
let status = delete_request(&app, "/api/settings/alerts/does-not-exist").await;
assert_eq!(status, StatusCode::NOT_FOUND);
- // `update_alert_rule`'s `AlertError::NotFound` maps to a plain-text
- // 404 body (mirrors `css_error_response`), not JSON, so this checks
- // the status directly rather than via `send_json`'s JSON parsing.
+ // AlertError::NotFound maps to a plain-text (not JSON) 404 body, so check status directly instead of via send_json.
let request = Request::builder()
.method("PUT")
.uri("/api/settings/alerts/does-not-exist")
@@ -1085,16 +944,11 @@ mod tests {
let values = body.as_array().unwrap();
assert_eq!(values.len(), 2);
for value in values {
- // `MetricPrimitive` is a Rust enum with derived `Serialize`, so its
- // JSON form is externally tagged: `{"String": "/a"}`.
+ // MetricPrimitive derives Serialize as an externally-tagged enum, so JSON is {"String": "/a"}.
assert_eq!(value["labels"]["path"]["String"], "/a");
}
- // Bucket values are grouped by label combination (ordered by the
- // labels' `BTreeMap` key, i.e. "/a" before "/b"), not globally by
- // sequence — so filtering to sequence > 0 leaves sequence 2 (from the
- // "/a" bucket) ahead of sequence 1 (from the "/b" bucket), and a
- // limit of 1 picks the former.
+ // Bucket values group by label (BTreeMap-ordered "/a" before "/b"), not by sequence, so limit=1 picks sequence 2 from "/a" ahead of sequence 1 from "/b".
let (status, body) = get_json(&app, "/api/metrics/requests?since_sequence=0&limit=1").await;
assert_eq!(status, StatusCode::OK);
let values = body.as_array().unwrap();
@@ -1159,10 +1013,7 @@ mod tests {
async fn declaring_param_persists_into_active_profile() {
let (app, _engine, settings_handle, dir) = test_app();
- // Set up an active profile against the *same* shared `settings`
- // handle the router was built with — mutating a separate `Settings`
- // loaded fresh from disk wouldn't be visible to the running app's
- // in-memory settings.
+ // Use the same shared settings handle the router was built with — a separately-loaded Settings wouldn't be visible to the running app.
let settings_path = dir.path().join("settings.json");
let profiles_dir = {
let settings = settings_handle.lock().unwrap();
@@ -1229,12 +1080,10 @@ mod tests {
assert!(body["panels"].as_array().unwrap().is_empty());
let dashboard_id = body["id"].as_str().unwrap().to_string();
- // Listing shows the new (empty) dashboard.
let (status, body) = get_json(&app, "/api/settings/profiles/web/dashboards").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body.as_array().unwrap().len(), 1);
- // Save adds a panel.
let (status, body) = send_json(
&app,
"PUT",
@@ -1253,13 +1102,11 @@ mod tests {
assert_eq!(panels.len(), 1);
assert_eq!(panels[0]["chart_type"], "line");
- // Get by id reflects the saved panel.
let (status, body) =
get_json(&app, &format!("/api/settings/profiles/web/dashboards/{dashboard_id}")).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body["panels"].as_array().unwrap().len(), 1);
- // Delete removes it.
let status = delete_request(&app, &format!("/api/settings/profiles/web/dashboards/{dashboard_id}")).await;
assert_eq!(status, StatusCode::NO_CONTENT);
let (status, body) = get_json(&app, "/api/settings/profiles/web/dashboards").await;
@@ -1308,8 +1155,7 @@ mod tests {
)
.await;
- // Simulate a restart: reload the profile store from disk directly,
- // independent of the running app/engine.
+ // Simulate a restart by reloading the profile store fresh from disk, independent of the running app/engine.
let settings_path = dir.path().join("settings.json");
let profiles_dir = {
let settings = settings_handle.lock().unwrap();
diff --git a/src/data_value.rs b/src/data_value.rs
index 4fa157b..731ecfb 100644
--- a/src/data_value.rs
+++ b/src/data_value.rs
@@ -3,19 +3,13 @@ use crate::timestamp::Timestamp;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
-/// A single parsed metric primitive extracted from a regex capture group.
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegexToMetricPrimitiveMatcher {
pub param_name: String,
pub capture_group: String,
pub kind: MetricPrimitiveKind,
- /// Ticket 022: whether this param contributes to the bucket's `labels`
- /// (the default) or is the bucket's per-event captured `value` (at most
- /// one `Value`-role param per metric — `declare_metric` rejects more
- /// than one). `#[serde(default)]` so profile files saved before this
- /// field existed still load, with every param defaulting to `Label` —
- /// i.e. unchanged behavior (the fixed occurrence-count-of-1 value).
+ /// Whether this param is a bucket label or its captured value (at most one Value-role param per metric); `#[serde(default)]` so profile files saved before this field existed still load as `Label`.
#[serde(default)]
pub role: ParamRole,
}
@@ -29,10 +23,7 @@ pub enum ParamRole {
Value,
}
-/// `rename_all = "lowercase"` matches every other `kind`-style enum exposed
-/// over the API/settings JSON (`ColorScheme`, `CssEntryKind`,
-/// `ProfileMetricPrimitiveKind`), so `POST /api/params`/`POST /api/metrics`
-/// bodies use `"string"`/`"number"`/`"timestamp"`.
+/// `rename_all = "lowercase"` matches other `kind`-style enums, so JSON bodies use "string"/"number"/"timestamp".
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
@@ -40,12 +31,7 @@ pub enum MetricPrimitiveKind {
String,
Number,
Timestamp,
- /// Ticket 023: milliseconds. On a metric's `Value`-role param, this
- /// doesn't parse the captured text at all — the value is instead derived
- /// as the elapsed time since the previous counter event with the same
- /// label set (see `crate::engine::apply_metric_to_log`). On any other
- /// param (a `Label`-role metric param, or a standalone `ParamDeclaration`),
- /// it parses the captured text as a literal `i64` millisecond count.
+ /// On a Value-role param this is derived as elapsed time since the previous event (not parsed from text); otherwise it's a literal i64 millisecond count.
Duration,
}
@@ -65,16 +51,16 @@ pub enum DataValue {
MetricDeclaration {
message_match_regex: String,
params: Vec,
- /// See `crate::engine::DeclareMetricRequest::matched_labels` (ticket 025).
+ /// See `crate::engine::DeclareMetricRequest::matched_labels`.
matched_labels: Option>,
- /// See `crate::engine::DeclareMetricRequest::unmatched_labels` (ticket 025).
+ /// See `crate::engine::DeclareMetricRequest::unmatched_labels`.
unmatched_labels: Option>,
},
/// Paired with `IndexKey::Bucket`.
BucketValue {
values: Vec<(u64, Timestamp, MetricPrimitive)>,
},
- /// Paired with `IndexKey::LastMetricEvent` (ticket 023).
+ /// Paired with `IndexKey::LastMetricEvent`.
LastEventTimestamp {
timestamp: Timestamp,
},
diff --git a/src/dto.rs b/src/dto.rs
index c0b122c..915d227 100644
--- a/src/dto.rs
+++ b/src/dto.rs
@@ -11,12 +11,7 @@ pub struct LogEntryDto {
pub sequence: u64,
pub timestamp_ms: i64,
pub text: String,
- /// Derived param values (ticket 001's `ParamDeclaration`s) for this line,
- /// keyed by param name. Only params whose regex actually matched this
- /// line's text (and whose captured group parsed as the declared `kind`)
- /// are present — a non-matching param is omitted rather than sent as
- /// null, so the frontend can render a blank/dash for anything missing
- /// from the map.
+ /// Only params whose regex matched (and parsed as the declared kind) are present — omitted rather than sent as null, so the frontend can render a blank for anything missing.
pub params: BTreeMap,
}
@@ -33,9 +28,9 @@ pub struct MetricDeclarationDto {
pub name: String,
pub message_match_regex: String,
pub params: Vec,
- /// See `crate::engine::DeclareMetricRequest::matched_labels` (ticket 025).
+ /// See `crate::engine::DeclareMetricRequest::matched_labels`.
pub matched_labels: Option>,
- /// See `crate::engine::DeclareMetricRequest::unmatched_labels` (ticket 025).
+ /// See `crate::engine::DeclareMetricRequest::unmatched_labels`.
pub unmatched_labels: Option>,
}
@@ -60,9 +55,7 @@ pub struct LogsParams {
pub start_ms: Option,
pub end_ms: Option,
pub limit: Option,
- /// Raw FilterBar query string (ticket 007), e.g. `regex("error") and
- /// sequence<5`. Parsed with `filter_parser::parse` and evaluated in the
- /// engine; a malformed string surfaces as `EngineResponse::Error`.
+ /// Raw FilterBar query string, e.g. `regex("error") and sequence<5`.
pub filter: Option,
}
@@ -71,14 +64,11 @@ pub struct TimelineParams {
pub start_ms: Option,
pub end_ms: Option,
pub bucket_ms: Option,
- /// Same FilterBar query string as `LogsParams::filter`: only logs
- /// matching it are counted into timeline buckets.
+ /// Same FilterBar query string as `LogsParams::filter`; only matching logs are counted into timeline buckets.
pub filter: Option,
}
-/// One entry from a metric's derived bucket timeseries: the label combination
-/// that bucket represents, plus one `(sequence, timestamp, value)` occurrence
-/// within it.
+/// One (sequence, timestamp, value) occurrence within a metric's bucket for a given label combination.
#[derive(Debug, Clone, Serialize)]
pub struct BucketValueDto {
pub labels: BTreeMap,
@@ -87,13 +77,7 @@ pub struct BucketValueDto {
pub value: MetricPrimitive,
}
-/// Query params for `GET /api/metrics/:name`. `labels` is a single
-/// comma-separated `key=value` list (e.g. `?labels=path=/a,duration=5`)
-/// rather than repeated `label.foo=bar` params, since axum's `Query`
-/// extractor needs a fixed/known set of field names — an arbitrary label set
-/// doesn't fit that without a custom deserializer. Every listed label must
-/// match (compared against each label value's `Display` form, see
-/// `MetricPrimitive`); unlisted labels on a bucket are ignored.
+/// `labels` is a comma-separated `key=value` list (not repeated params) since axum's `Query` extractor needs a fixed set of field names.
#[derive(Debug, Deserialize)]
pub struct BucketValuesParams {
pub labels: Option,
@@ -103,24 +87,13 @@ pub struct BucketValuesParams {
pub limit: Option,
}
-/// One chart-ready time series returned by `GET /api/query` (ticket 013's PromQL
-/// endpoint): a label combination plus its `(timestamp_ms, value)` points, sorted by
-/// timestamp. `labels` uses plain `String` values (not `MetricPrimitive`) since every
-/// value has already been through `MetricPrimitive`'s `Display` form — chart code
-/// doesn't need the typed value, just something to show in a legend/tooltip.
-///
-/// `value` is always `f64` (see `crate::engine::coerce_to_f64` for how a bucket's
-/// `MetricPrimitive` is coerced into one), since a chart needs a plottable number
-/// regardless of the underlying metric primitive's kind.
+/// `labels` uses plain `String` (already Display-formatted) since charts just need something to show; `value` is always `f64` for plottability.
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct PromqlSeriesDto {
pub labels: BTreeMap,
pub points: Vec<(i64, f64)>,
}
-/// Query params for `GET /api/query`. `promql` is the query string (see
-/// `crate::promql`); `start_ms`/`end_ms` bound which points are included, mirroring
-/// `/api/logs`/`/api/metrics/:name`.
#[derive(Debug, Deserialize)]
pub struct PromqlParams {
pub promql: String,
@@ -128,17 +101,13 @@ pub struct PromqlParams {
pub end_ms: Option,
}
-/// JSON error body returned by API handlers on a 4xx/5xx response, e.g.
-/// `{ "error": "invalid regex: ..." }`.
+/// JSON error body returned by API handlers on a 4xx/5xx response.
#[derive(Debug, Serialize)]
pub struct ErrorDto {
pub error: String,
}
-/// Request body for `POST /api/settings/css`: raw CSS content to import and
-/// store alongside the settings file. `is_theme` (ticket 033) marks this as
-/// a selectable colour-scheme theme rather than arbitrary CSS; defaults to
-/// `false` (arbitrary CSS) when omitted.
+/// `is_theme` marks this as a selectable colour-scheme theme rather than arbitrary CSS; defaults to false.
#[derive(Debug, Deserialize)]
pub struct ImportCssRequest {
pub filename: String,
@@ -147,9 +116,7 @@ pub struct ImportCssRequest {
pub is_theme: bool,
}
-/// Request body for `POST /api/settings/css/link`: a reference to an
-/// external CSS path/URL that ottyl doesn't copy or own. `is_theme` (ticket
-/// 033) marks this as a selectable theme rather than arbitrary CSS.
+/// A reference to an external CSS path/URL that ottyl doesn't copy or own.
#[derive(Debug, Deserialize)]
pub struct LinkCssRequest {
pub path_or_url: String,
@@ -157,14 +124,7 @@ pub struct LinkCssRequest {
pub is_theme: bool,
}
-/// Response shape for a single CSS entry, used both by the list endpoint and
-/// by mutation endpoints that return the affected entry. `id` is the entry's
-/// real id for `Imported` entries, or a stable id derived from the
-/// linked path/URL for `Linked` entries (see `settings::css_entry_id`) —
-/// this lets the frontend key/reference both kinds of entries uniformly,
-/// even though only `Imported` entries are deletable/content-fetchable.
-/// `is_theme` (ticket 033) distinguishes a theme entry (selectable from the
-/// theme picker) from arbitrary imported/linked CSS.
+/// `id` is the real id for `Imported` entries or a stable id derived from the path/URL for `Linked` ones, so the frontend can key both kinds uniformly (only `Imported` entries are deletable/content-fetchable).
#[derive(Debug, Clone, Serialize)]
pub struct CssEntryDto {
pub id: String,
@@ -202,19 +162,14 @@ impl From<&CssEntry> for CssEntryDto {
}
}
-/// Response shape for `GET /api/settings` and `PATCH /api/settings` (ticket
-/// 011): the subset of `Settings` the NavMenu UI needs to render a colour
-/// scheme picker and know which profile is active. `css_entries` is
-/// deliberately omitted here since it already has its own dedicated
-/// `GET /api/settings/css` endpoint (ticket 010).
+/// `css_entries` is omitted here since it has its own `GET /api/settings/css` endpoint.
#[derive(Debug, Clone, Serialize)]
pub struct SettingsDto {
pub color_scheme: ColorScheme,
pub active_profile: Option,
- /// `None` means unlimited (ticket 026).
+ /// `None` means unlimited.
pub max_logs: Option,
- /// Id of the selected custom theme entry, if any (ticket 033). `None`
- /// means `color_scheme` alone determines the active palette.
+ /// Id of the selected custom theme entry, if any. `None` means `color_scheme` alone determines the active palette.
pub custom_theme_id: Option,
}
@@ -229,11 +184,7 @@ impl From<&Settings> for SettingsDto {
}
}
-/// Request body for `PATCH /api/settings`. `color_scheme`: `None` leaves the
-/// current value untouched. `max_logs`/`custom_theme_id`: each distinguishes
-/// "field omitted" (leave unchanged, outer `None`) from "field present"
-/// (inner `Option` applied verbatim — `null` clears it, a value sets it), via
-/// `deserialize_double_option`.
+/// `max_logs`/`custom_theme_id` distinguish "omitted" (leave unchanged) from "present" (null clears, value sets), via `deserialize_double_option`.
#[derive(Debug, Deserialize)]
pub struct UpdateSettingsRequest {
pub color_scheme: Option,
@@ -243,10 +194,7 @@ pub struct UpdateSettingsRequest {
pub custom_theme_id: Option
Each param is either a label (the default) or the metric's single value{' '}
- param. Label params identify which bucket an occurrence belongs to; a value param's captured number becomes
- that occurrence's stored value instead of a label — e.g. a duration param marked{' '}
- value lets you query the distribution or latest reading of durations, not just how often they
- occurred. A metric with only label params still works exactly as before: every occurrence's value defaults
- to a fixed 1 (a pure occurrence count).
+ param. Labels identify which bucket an occurrence belongs to; a value param's captured number becomes the
+ occurrence's stored value instead of a label — e.g. a duration param marked value{' '}
+ lets you query the distribution or latest reading, not just how often it occurred. A metric with only
+ label params defaults every occurrence's value to a fixed 1 (a pure occurrence count).
- Declaring a metric backfills it against everything already ingested, then continues indexing new lines as
- they arrive — there's no need to restart ingestion or re-declare after the fact.
+ Declaring a metric backfills it against everything already ingested, then keeps indexing new lines as they
+ arrive — no need to restart ingestion or re-declare after the fact.
The Metrics panel lets you declare a metric, pick one from the declared list, and see its bucket values
grouped by label combination over the active time range — occurrence count per group, plus a small trend
- sparkline. That same data is what dashboard panels query via PromQL (see below), including two forms{' '}
+ sparkline. Dashboard panels query that same data via PromQL (see below), including two forms{' '}
derived from a counter's value param rather than stored separately:
@@ -188,7 +181,7 @@ const SECTIONS: DocSection[] = [
histogram(metric_name[width]){' '}
- — buckets every matching event's value into fixed-width buckets of size width, e.g.{' '}
+ — buckets each matching event's value into fixed-width buckets of size width, e.g.{' '}
histogram(durations[10]).
- Dashboards work like Grafana: each one is a named set of panels, and each panel is a PromQL query paired
- with a chart type — bar, line, or pie. Panels preview live as you edit the query, and a malformed query
- shows an inline error instead of breaking the page.
+ Dashboards work like Grafana: each is a named set of panels, and each panel is a PromQL query paired with a
+ chart type — bar, line, or pie. Panels preview live as you edit the query, and a malformed query shows an
+ inline error instead of breaking the page.
- Bar — the latest value per series (a snapshot of "where
- things are now").
+ Bar — the latest value per series (a snapshot of "now").
Pie — each series' share of the summed total across the
@@ -227,8 +219,8 @@ const SECTIONS: DocSection[] = [
- Dashboards are saved per profile, so they travel with a profile when you switch or copy it, and require an
- active profile to create or save — pick or create one from the settings menu first.
+ Dashboards are saved per profile, so they travel with it when you switch or copy it — an active profile is
+ required to create or save one; pick or create one from the settings menu first.
- An alert rule fires a notification when its condition matches a newly-ingested log line — evaluated per
- line as it arrives, not backfilled against history. There are four condition types:
+ An alert rule fires a notification when its condition matches a newly-ingested log line, evaluated per line
+ as it arrives rather than backfilled against history. There are four condition types:
- Immediate — fires once per line matching a query.
+ Immediate — fires once per matching line.
Threshold — fires when a query's match count within a
@@ -260,8 +252,8 @@ const SECTIONS: DocSection[] = [
- Query-based conditions (Immediate/Threshold/Ratio) reuse the filter bar's query syntax, but only the subset
- that evaluates against a single line's own fields — regex(...), sequence,{' '}
+ Query-based conditions (Immediate/Threshold/Ratio) reuse the filter bar's syntax, but only the subset that
+ evaluates a single line's own fields — regex(...), sequence,{' '}
timestamp, and and/or. A metric_name[param] comparison
isn't supported in an alert query.
- Every rule delivers to an ntfy sink: a topic (required) and an optional server override (defaults
- to https://ntfy.sh). "Send test notification" delivers to whatever topic/server is currently
- entered in the form, without needing to save the rule first — handy for verifying the sink before
- committing to it.
+ Every rule delivers to an ntfy sink: a topic (required) and an optional server override (default{' '}
+ https://ntfy.sh). "Send test notification" delivers to the form's current topic/server without
+ needing to save the rule first.
- The Settings panel controls process-wide state: colour scheme, the maximum number of logs kept in memory
- (blank means unlimited), and imported/linked CSS. These apply immediately and persist to the single
- settings file on disk — no save button needed.
+ The Settings panel controls process-wide state: colour scheme, the max number of logs kept in memory
+ (blank means unlimited), and imported/linked CSS. Changes apply immediately and persist to the settings
+ file on disk — no save button needed.
The Presets panel manages profiles: create, switch, and delete them. Switching activates a profile's
- params/metrics in the running engine immediately, with no restart needed — see the General idea{' '}
- section above for what a profile stores.
+ params/metrics in the running engine immediately, no restart needed — see General idea above for
+ what a profile stores.
),
},
]
-/** How long a flashed section stays highlighted before settling back to normal. */
const FLASH_DURATION_MS = 1200
export function DocsPanel({ target }: { target?: DocsTarget | null }) {
@@ -314,9 +304,7 @@ export function DocsPanel({ target }: { target?: DocsTarget | null }) {
const [flashId, setFlashId] = useState(null)
const sectionRefs = useRef>({})
- // Keyed on `target?.nonce` (not `target?.sectionId`) so clicking the same
- // "?" icon twice in a row re-triggers the flash rather than being a no-op
- // because the panel is already open to that section.
+ // Keyed on `target?.nonce`, not `target?.sectionId`, so clicking the same "?" icon twice re-triggers the flash instead of no-op'ing when already open.
useEffect(() => {
if (!target) return
setOpenId(target.sectionId)
diff --git a/webapp/src/components/FilterBar.tsx b/webapp/src/components/FilterBar.tsx
index 7b128cc..ac68359 100644
--- a/webapp/src/components/FilterBar.tsx
+++ b/webapp/src/components/FilterBar.tsx
@@ -2,19 +2,11 @@ import type { ReactNode } from 'react'
import { useState } from 'react'
import { HelpIcon } from './HelpIcon'
-/// Shared styling for a single FilterBar-syntax query input, reused by
-/// `Alerts.tsx`'s condition editor so query-based alert conditions look like
-/// the top-level filter bar without dragging in its own draft/submit state.
+// Reused by `Alerts.tsx`'s condition editor so alert conditions look like the filter bar without duplicating its draft/submit state.
export const queryInputClassName =
'flex-1 rounded border border-border-soft bg-surface-1 px-2 py-1 text-sm text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none'
-/**
- * Input for the syntax-aware pest-parsed filter language described in the
- * README (regex + metric comparisons combined with `and`/`or`). The raw
- * filter string is sent to the server (`GET /api/logs` / `GET /api/timeline`
- * `filter` param) and evaluated there; this component just manages the
- * draft text and surfaces any parse error the server reports back.
- */
+// The filter string is parsed and evaluated server-side (`filter` param on `GET /api/logs`/`GET /api/timeline`); this just manages draft text and surfaces the server's parse error.
export function FilterBar({
value,
onChange,
diff --git a/webapp/src/components/HelpIcon.tsx b/webapp/src/components/HelpIcon.tsx
index 2979494..4b48674 100644
--- a/webapp/src/components/HelpIcon.tsx
+++ b/webapp/src/components/HelpIcon.tsx
@@ -1,10 +1,6 @@
import { useDocsNav } from '../lib/docsNav'
-/**
- * A small "?" affordance that opens the Docs panel scrolled to (and
- * flashing) a specific section (ticket 021), instead of the user having to
- * find it themselves in the Docs accordion.
- */
+// Opens the Docs panel scrolled to (and flashing) sectionId, instead of the user hunting for it in the Docs accordion (ticket 021).
export function HelpIcon({ sectionId, label }: { sectionId: string; label?: string }) {
const { openSection } = useDocsNav()
return (
diff --git a/webapp/src/components/LogPreview.tsx b/webapp/src/components/LogPreview.tsx
index 5178b47..59b60e2 100644
--- a/webapp/src/components/LogPreview.tsx
+++ b/webapp/src/components/LogPreview.tsx
@@ -2,10 +2,7 @@ import { useEffect, useRef, useState } from 'react'
import { metricPrimitiveToDisplayString, type LogEntry, type ParamDeclaration } from '../lib/api'
import { HelpIcon } from './HelpIcon'
-/// localStorage key for LogPreview's selected param columns. Namespaced
-/// under `ottyl.` since this is the app's first localStorage usage — kept
-/// distinct enough that future per-viewer preferences elsewhere in the app
-/// won't collide with it.
+// Namespaced under `ottyl.` so future per-viewer preferences elsewhere in the app won't collide with this key.
const VISIBLE_PARAM_COLUMNS_STORAGE_KEY = 'ottyl.logPreview.visibleParamColumns'
function loadVisibleParamColumns(): string[] {
@@ -23,8 +20,7 @@ function saveVisibleParamColumns(columns: string[]) {
try {
localStorage.setItem(VISIBLE_PARAM_COLUMNS_STORAGE_KEY, JSON.stringify(columns))
} catch {
- // best-effort — a private browsing mode or full storage just means the
- // selection won't persist across reloads.
+ // best-effort — private browsing or full storage just means the selection won't persist across reloads
}
}
@@ -101,9 +97,7 @@ export function LogPreview({ logs, params }: { logs: LogEntry[]; params: ParamDe
})
}
- // Only render columns for params that are both selected and still known to
- // exist, so a stale localStorage entry for a since-removed param doesn't
- // leave a dangling column.
+ // Filter out stale localStorage entries for since-removed params, so they don't leave a dangling column.
const columns = visibleParamColumns.filter((name) => paramNames.includes(name))
return (
diff --git a/webapp/src/components/LogTimeline.tsx b/webapp/src/components/LogTimeline.tsx
index 79c825a..a8aab51 100644
--- a/webapp/src/components/LogTimeline.tsx
+++ b/webapp/src/components/LogTimeline.tsx
@@ -7,9 +7,7 @@ export interface TimeRange {
endMs: number
}
-/// A Grafana-style relative time-range preset (ticket 024). `'all'` clears
-/// the range (today's implicit default); the rest resolve to a window
-/// ending "now" at the moment they're (re-)computed — see `computePresetRange`.
+// `'all'` clears the range (the implicit default); the rest resolve to a window ending "now" at the moment they're (re-)computed — see `computePresetRange`.
export type TimeRangePresetId = 'all' | '5m' | '15m' | '1h' | '24h'
export const TIME_RANGE_PRESETS: { id: TimeRangePresetId; label: string; ms: number | null }[] = [
@@ -20,10 +18,7 @@ export const TIME_RANGE_PRESETS: { id: TimeRangePresetId; label: string; ms: num
{ id: '24h', label: '24h', ms: 24 * 60 * 60 * 1000 },
]
-/// Resolves a preset to a concrete `{startMs, endMs}` window as of *now* —
-/// callers re-invoke this on the same cadence they already poll on to keep a
-/// rolling preset's window moving with time (see `App.tsx`), rather than
-/// freezing it at selection time.
+// Callers re-invoke this on their existing poll cadence (see `App.tsx`) to keep a rolling preset's window moving with time, rather than freezing it at selection time.
export function computePresetRange(id: TimeRangePresetId): TimeRange | null {
const preset = TIME_RANGE_PRESETS.find((p) => p.id === id)
if (!preset || preset.ms == null) return null
@@ -131,9 +126,7 @@ export function LogTimeline({
))}
- {/* A dragged (custom, non-preset) selection — presets already show
- their own active state above, so this only appears for the
- "fixed range from a drag" case. */}
+ {/* Only shown for a dragged (non-preset) selection — presets already show their own active state above. */}
{range && activePreset === null && (