From fb987836a81d07308ae29ee0147eef320ff9d244 Mon Sep 17 00:00:00 2001 From: RajMandaliya Date: Tue, 14 Jul 2026 10:07:47 -0700 Subject: [PATCH 1/2] feat(api-web): support exact health-alert filtering on managed-host page The existing health-alerts-filter only supported a binary healthy/ unhealthy toggle, checking whether health_probe_alerts was empty or not -- it never matched against a specific alert ID. Extends the filter to also support exact matching against HealthProbeAlert.id, with the dropdown dynamically populated from every distinct alert ID currently active across machines (same pattern already used for the vendor/model/GPU filters). Also adds a clear empty-state message ('No machines match the selected filters.') for the zero-results case, which previously rendered an empty table with no explanation for any filter. Addresses #3429. Signed-off-by: RajMandaliya --- crates/api-web/src/managed_host.rs | 17 ++++ crates/api-web/src/tests/managed_host.rs | 93 +++++++++++++++++++ .../managed_host_individual_table.html | 3 + .../api-web/templates/managed_host_show.html | 3 + 4 files changed, 116 insertions(+) diff --git a/crates/api-web/src/managed_host.rs b/crates/api-web/src/managed_host.rs index 0dba7c859c..295a757035 100644 --- a/crates/api-web/src/managed_host.rs +++ b/crates/api-web/src/managed_host.rs @@ -53,6 +53,7 @@ struct ManagedHostShow { active_state_filter: String, active_time_in_state_above_sla_filter: String, states: Vec, + health_alert_ids: Vec, gpus: Vec, active_gpu_filter: String, ibs: Vec, @@ -456,6 +457,7 @@ pub async fn show_html( let mut hosts = Vec::new(); let mut models_per_vendor: HashMap> = HashMap::new(); let mut states = HashSet::new(); + let mut health_alert_ids = HashSet::new(); let mut gpus = HashSet::new(); let mut ibs = HashSet::new(); let mut mems = HashSet::new(); @@ -481,6 +483,9 @@ pub async fn show_html( states.insert(short_state(&m.state).to_string()); gpus.insert(m.num_gpus); ibs.insert(m.num_ib_ifs); + for alert in &m.health_probe_alerts { + health_alert_ids.insert(alert.id.to_string()); + } let barry = mem_to_size(&m.host_memory); mems.insert((barry, format!("{barry} GiB"))); @@ -528,6 +533,15 @@ pub async fn show_html( if active_health_alerts_filter == "unhealthy" && m.health_probe_alerts.is_empty() { continue; } + if active_health_alerts_filter != "healthy" + && active_health_alerts_filter != "unhealthy" + && !m + .health_probe_alerts + .iter() + .any(|a| a.id.to_string() == active_health_alerts_filter) + { + continue; + } } if active_maintenance_filter != "all" { if active_maintenance_filter == "active" && !m.maintenance_reference.is_empty() { @@ -570,6 +584,8 @@ pub async fn show_html( let states: Vec = states.into_iter().sorted_unstable().collect(); + let health_alert_ids: Vec = health_alert_ids.into_iter().sorted_unstable().collect(); + let gpus: Vec = gpus .into_iter() .map(|x| x.to_string()) @@ -633,6 +649,7 @@ pub async fn show_html( active_state_filter, active_time_in_state_above_sla_filter, states, + health_alert_ids, gpus, active_gpu_filter, ibs, diff --git a/crates/api-web/src/tests/managed_host.rs b/crates/api-web/src/tests/managed_host.rs index 40453a3084..11a04481c0 100644 --- a/crates/api-web/src/tests/managed_host.rs +++ b/crates/api-web/src/tests/managed_host.rs @@ -337,3 +337,96 @@ async fn test_managed_host_html_uses_runtime_sla_config(pool: sqlx::PgPool) { assert!(body_str.contains("bubble warning")); assert!(body_str.contains("Assigned/BootingWithDiscoveryImage")); } + +#[crate::sqlx_test] +async fn test_managed_host_health_alert_exact_filter(pool: sqlx::PgPool) -> eyre::Result<()> { + let env = TestEnv::new(pool).await; + let mh_alerting = env.create_ready_managed_host(1).await.0; + let mh_healthy = env.create_ready_managed_host(2).await.0; + + let report = HealthReport { + source: "mock-bmc-intrusion".to_string(), + triggered_by: None, + observed_at: None, + successes: vec![], + alerts: vec![HealthProbeAlert { + id: HealthProbeId::from_str("IntrusionSensorTriggered")?, + target: Some("HostBMC".to_string()), + in_alert_since: None, + message: "Physical Chassis Intrusion Alert".to_string(), + tenant_message: None, + classifications: vec![ + HealthAlertClassification::hardware(), + HealthAlertClassification::sensor_critical(), + HealthAlertClassification::prevent_allocations(), + ], + }], + }; + + let mut txn = env.test_harness.db_txn().await; + db::machine::insert_health_report( + &mut txn, + &mh_alerting.host.id, + HealthReportApplyMode::Merge, + &report, + false, + ) + .await?; + txn.commit().await?; + + let app = make_test_app(&env.test_harness); + let response = app + .oneshot( + web_request_builder() + .uri("/admin/managed-host?health-alerts-filter=IntrusionSensorTriggered") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let body_bytes = response + .into_body() + .collect() + .await + .expect("Empty response body?") + .to_bytes(); + let body_str = std::str::from_utf8(&body_bytes).expect("Invalid UTF-8 in body"); + + // Only the alerting host should show up, not the healthy one. + assert!(body_str.contains(&mh_alerting.host.id.to_string())); + assert!(!body_str.contains(&mh_healthy.host.id.to_string())); + + Ok(()) +} + +#[crate::sqlx_test] +async fn test_managed_host_health_alert_filter_empty_state(pool: sqlx::PgPool) -> eyre::Result<()> { + let env = TestEnv::new(pool).await; + let _mh = env.create_ready_managed_host(1).await.0; + + let app = make_test_app(&env.test_harness); + let response = app + .oneshot( + web_request_builder() + .uri("/admin/managed-host?health-alerts-filter=SomeAlertNoOneHas") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let body_bytes = response + .into_body() + .collect() + .await + .expect("Empty response body?") + .to_bytes(); + let body_str = std::str::from_utf8(&body_bytes).expect("Invalid UTF-8 in body"); + + assert!(body_str.contains("No machines match the selected filters.")); + + Ok(()) +} \ No newline at end of file diff --git a/crates/api-web/templates/managed_host_individual_table.html b/crates/api-web/templates/managed_host_individual_table.html index bef4f23d61..3b58a448db 100644 --- a/crates/api-web/templates/managed_host_individual_table.html +++ b/crates/api-web/templates/managed_host_individual_table.html @@ -22,6 +22,9 @@ + {% if hosts.is_empty() %} + No machines match the selected filters. + {% endif %} {% for host in hosts %} {{ host.machine_id|machine_id_link|safe }} diff --git a/crates/api-web/templates/managed_host_show.html b/crates/api-web/templates/managed_host_show.html index baf592cdd4..3016c0eccb 100644 --- a/crates/api-web/templates/managed_host_show.html +++ b/crates/api-web/templates/managed_host_show.html @@ -23,6 +23,9 @@

All Managed Hosts ({{ page.total_items() }})

+ {% for a in health_alert_ids %} + + {% endfor %}
From 70b4bece1ec8c541e665dc77e05b3684edadb2c9 Mon Sep 17 00:00:00 2001 From: RajMandaliya Date: Tue, 14 Jul 2026 10:27:32 -0700 Subject: [PATCH 2/2] fix: address CodeRabbit review feedback - Match empty-state colspan to actual table column count (12 normally, 11 in maintenance mode) instead of a fixed 15. - Strengthen the exact-filter test to assert the dynamic alert ID actually renders as the selected dropdown option, not just that filtering excludes/includes the right hosts. Signed-off-by: RajMandaliya --- crates/api-web/src/tests/managed_host.rs | 6 ++++++ crates/api-web/templates/managed_host_individual_table.html | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/api-web/src/tests/managed_host.rs b/crates/api-web/src/tests/managed_host.rs index 11a04481c0..5cdbe4918e 100644 --- a/crates/api-web/src/tests/managed_host.rs +++ b/crates/api-web/src/tests/managed_host.rs @@ -398,6 +398,12 @@ async fn test_managed_host_health_alert_exact_filter(pool: sqlx::PgPool) -> eyre assert!(body_str.contains(&mh_alerting.host.id.to_string())); assert!(!body_str.contains(&mh_healthy.host.id.to_string())); + // The dynamic dropdown should include this alert ID as the selected option. + assert!( + body_str.contains(r#"