Trim comments across Rust and webapp source to direct one-liners (ticket 030)
Pass over src/*.rs, webapp/src/**, and the in-app "?" docs prose: condensed multi-paragraph doc comments to single lines that state the non-obvious why, deleted comments that only restated adjacent code, and tightened the Docs.tsx help-panel wording. No logic, structure, or code formatting changed — verified via cargo test (143 passed) and tsc -b && vite build. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3cef6dc8ae
commit
00c21c8032
+17
-171
@@ -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<PathBuf>,
|
||||
}
|
||||
|
||||
@@ -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<EngineResponse
|
||||
response.await.map_err(|_| StatusCode::SERVICE_UNAVAILABLE)
|
||||
}
|
||||
|
||||
/// `GET /api/logs` — also accepts a `filter` query param (ticket 007): a
|
||||
/// FilterBar query string, parsed and evaluated inside the engine. A
|
||||
/// malformed filter string comes back as `EngineResponse::Error` and is
|
||||
/// surfaced here as `400 Bad Request` with `{ "error": "..." }`, matching
|
||||
/// ticket 004's `declare_param`/`declare_metric` error-mapping pattern.
|
||||
async fn get_logs(
|
||||
State(state): State<ApiState>,
|
||||
Query(params): Query<LogsParams>,
|
||||
@@ -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<ApiState>,
|
||||
Query(params): Query<TimelineParams>,
|
||||
@@ -210,10 +194,6 @@ async fn get_stats(State(state): State<ApiState>) -> impl IntoResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/// `GET /api/params` — lists all declared params (name, regex, capture
|
||||
/// group, kind) as `Vec<ParamDeclarationDto>`. 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<ApiState>) -> 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<ApiState>) -> 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<ApiState>,
|
||||
Json(body): Json<DeclareParamRequest>,
|
||||
@@ -260,8 +232,6 @@ async fn declare_param(
|
||||
}
|
||||
}
|
||||
|
||||
/// `GET /api/metrics` — lists all declared metrics (name, regex, params) as
|
||||
/// `Vec<MetricDeclarationDto>`.
|
||||
async fn list_metrics(State(state): State<ApiState>) -> 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<ApiState>) -> 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<ApiState>,
|
||||
Json(body): Json<DeclareMetricRequest>,
|
||||
@@ -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<String, String> {
|
||||
raw.split(',')
|
||||
.filter_map(|pair| pair.split_once('='))
|
||||
@@ -316,9 +279,6 @@ fn parse_labels_filter(raw: &str) -> BTreeMap<String, String> {
|
||||
.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<ApiState>,
|
||||
AxumPath(metric_name): AxumPath<String>,
|
||||
@@ -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<PromqlSeriesDto>` — 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<ApiState>,
|
||||
Query(params): Query<PromqlParams>,
|
||||
@@ -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<CssEntryDto>`.
|
||||
async fn list_css(State(state): State<ApiState>) -> impl IntoResponse {
|
||||
let settings = state.settings.lock().unwrap();
|
||||
let entries: Vec<CssEntryDto> = 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<ApiState>,
|
||||
Json(body): Json<ImportCssRequest>,
|
||||
@@ -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<ApiState>,
|
||||
Json(body): Json<LinkCssRequest>,
|
||||
@@ -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<ApiState>,
|
||||
AxumPath(id): AxumPath<String>,
|
||||
@@ -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<ApiState>,
|
||||
AxumPath(id): AxumPath<String>,
|
||||
@@ -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<AlertRule>`.
|
||||
async fn list_alert_rules(State(state): State<ApiState>) -> 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<ApiState>,
|
||||
Json(body): Json<CreateAlertRuleRequest>,
|
||||
@@ -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<ApiState>,
|
||||
AxumPath(id): AxumPath<String>,
|
||||
@@ -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<ApiState>, AxumPath(id): AxumPath<String>) -> 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<ApiState>, 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<TestAlertNotificationRequest>) -> 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<TestAlertNotificationRequest>)
|
||||
}
|
||||
}
|
||||
|
||||
/// `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<ApiState>) -> 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<ApiState>,
|
||||
Json(body): Json<UpdateSettingsRequest>,
|
||||
@@ -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<ProfileSummaryDto>`.
|
||||
/// Use `GET /api/settings`'s `active_profile` field to know which one (if
|
||||
/// any) is currently active.
|
||||
async fn list_profiles(State(state): State<ApiState>) -> 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<ApiState>) -> 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<ApiState>,
|
||||
Json(body): Json<CreateProfileRequest>,
|
||||
@@ -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<ApiState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
@@ -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<ApiState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
@@ -723,9 +605,6 @@ async fn activate_profile(
|
||||
}
|
||||
}
|
||||
|
||||
/// `GET /api/settings/profiles/:name/dashboards` — lists all dashboards saved
|
||||
/// on the profile `name`, as `Vec<DashboardDto>`. Returns `404 Not Found` if
|
||||
/// no profile with that name exists.
|
||||
async fn list_dashboards(
|
||||
State(state): State<ApiState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
@@ -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<ApiState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
@@ -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<ApiState>,
|
||||
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<ApiState>,
|
||||
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<ApiState>,
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user