Add Alerts webapp UI (ticket 020)

New Alerts panel: lists rules with condition summaries and an enabled
toggle, and a condition-type-specific editor (Immediate/Threshold/NewValue/
Ratio) with client-side validation before save, reusing FilterBar's query
input styling for query-based conditions. Adds a test-notification endpoint
(POST /api/settings/alerts/test) so an ntfy topic/server can be verified
without saving a rule first, plus a docs section and help icons matching
ticket 021's pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
Hugh Kaznowski
2026-09-05 16:34:14 +01:00
co-authored by Claude Sonnet 5
parent dd3320812c
commit 7bf323dd50
8 changed files with 644 additions and 44 deletions
+16 -2
View File
@@ -6,13 +6,13 @@ use crate::dto::{
BucketValuesParams, CreateAlertRuleRequest, CreateDashboardRequest, CreateProfileRequest,
CssEntryDto, DashboardDto, ErrorDto, ImportCssRequest, LinkCssRequest, MetricDeclarationDto,
ParamDeclarationDto, LogsParams, ProfileSummaryDto, PromqlParams, SaveDashboardRequest,
SettingsDto, TimelineParams, UpdateAlertRuleRequest, UpdateSettingsRequest,
SettingsDto, TestAlertNotificationRequest, TimelineParams, UpdateAlertRuleRequest, UpdateSettingsRequest,
};
use crate::engine::{
DeclareMetricRequest, DeclareParamRequest, EngineHandle, EngineMessage, EngineQuery,
EngineResponse,
};
use crate::alerts::AlertError;
use crate::alerts::{deliver_ntfy, AlertError};
use crate::profile::{self, Profile, ProfileError, ProfileStore};
use crate::settings::{CssError, SettingsHandle};
use axum::extract::{Path as AxumPath, Query, State};
@@ -110,6 +110,7 @@ pub fn router(engine: EngineHandle, settings: SettingsHandle, settings_path: Pat
"/api/settings/alerts/{id}",
put(update_alert_rule).delete(delete_alert_rule),
)
.route("/api/settings/alerts/test", post(test_alert_notification))
.route("/api/settings/profiles", get(list_profiles).post(create_profile))
.route("/api/settings/profiles/{name}", delete(delete_profile))
.route("/api/settings/profiles/{name}/activate", post(activate_profile))
@@ -523,6 +524,19 @@ 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.
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 {
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(err) => (StatusCode::BAD_GATEWAY, err).into_response(),
}
}
/// `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`).
+10
View File
@@ -259,6 +259,16 @@ pub struct UpdateAlertRuleRequest {
pub sink: AlertSink,
}
/// Request body for `POST /api/settings/alerts/test` (ticket 020): delivers
/// a test notification to an ntfy topic/server without requiring a saved
/// rule. `server` defaults to `https://ntfy.sh` when absent, same as
/// `AlertSink::Ntfy`.
#[derive(Debug, Deserialize)]
pub struct TestAlertNotificationRequest {
pub topic: String,
pub server: Option<String>,
}
/// Summary of one profile for `GET /api/settings/profiles` (ticket 011):
/// just the name and declaration/dashboard counts the Presets panel needs to
/// render a list, rather than the full param/metric declaration bodies.