Skip to main content

max / pom

Fix DNS checks, prune stale route/DNS data from DB - Remove hardcoded expected IPs from DNS config (empty = any resolved IP is OK) - Add prune_stale_routes() and prune_stale_dns() to clean DB rows for records no longer in config, run at task startup - Filter API status responses to only include configured routes/DNS records - Bind hetzner listener to 0.0.0.0:9100 instead of Tailscale IP - Update integration tests to use explicit configs matching new filtering Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-03-25 21:49 UTC
Commit: ee37659e407148c8037a33e919501c971236380e
Parent: 69503fb
7 files changed, +118 insertions, -12 deletions
@@ -17,17 +17,17 @@ expected_routes = ["/", "/discover", "/login", "/docs"]
17 17 [[targets.mnw.dns]]
18 18 name = "makenot.work"
19 19 record_type = "A"
20 - expected = ["5.78.144.244"]
20 + expected = []
21 21
22 22 [[targets.mnw.dns]]
23 23 name = "forums.makenot.work"
24 24 record_type = "A"
25 - expected = ["5.78.144.244"]
25 + expected = []
26 26
27 27 [[targets.mnw.dns]]
28 28 name = "git.makenot.work"
29 29 record_type = "A"
30 - expected = ["5.78.144.244"]
30 + expected = []
31 31
32 32 [targets.mnw.whois]
33 33 domain = "makenot.work"
@@ -81,7 +81,7 @@ label = "htpy.app"
81 81 [[targets.htpy.dns]]
82 82 name = "htpy.app"
83 83 record_type = "A"
84 - expected = ["5.78.135.189"]
84 + expected = []
85 85
86 86 [targets.htpy.whois]
87 87 domain = "htpy.app"
@@ -1,7 +1,7 @@
1 1 [serve]
2 2 interval_secs = 300
3 3 prune_days = 30
4 - listen = "100.120.174.96:9100"
4 + listen = "0.0.0.0:9100"
5 5 peer_heartbeat_secs = 60
6 6 route_check_interval_secs = 300
7 7 dashboard = false
@@ -17,17 +17,17 @@ expected_routes = ["/", "/discover", "/login", "/docs"]
17 17 [[targets.mnw.dns]]
18 18 name = "makenot.work"
19 19 record_type = "A"
20 - expected = ["5.78.144.244"]
20 + expected = []
21 21
22 22 [[targets.mnw.dns]]
23 23 name = "forums.makenot.work"
24 24 record_type = "A"
25 - expected = ["5.78.144.244"]
25 + expected = []
26 26
27 27 [[targets.mnw.dns]]
28 28 name = "git.makenot.work"
29 29 record_type = "A"
30 - expected = ["5.78.144.244"]
30 + expected = []
31 31
32 32 [targets.mnw.whois]
33 33 domain = "makenot.work"
@@ -81,7 +81,7 @@ label = "htpy.app"
81 81 [[targets.htpy.dns]]
82 82 name = "htpy.app"
83 83 record_type = "A"
84 - expected = ["5.78.135.189"]
84 + expected = []
85 85
86 86 [targets.htpy.whois]
87 87 domain = "htpy.app"
M src/api.rs +9 -1
@@ -1,6 +1,6 @@
1 1 //! HTTP API for serve mode — exposes health check data to consumers like MNW.
2 2
3 - use std::collections::HashMap;
3 + use std::collections::{HashMap, HashSet};
4 4 use std::sync::Arc;
5 5 use std::sync::atomic::{AtomicU64, Ordering};
6 6
@@ -336,8 +336,12 @@ async fn build_target_status(
336 336 let route_checks = db::get_latest_route_checks(pool, name)
337 337 .await
338 338 .unwrap_or_default();
339 + let expected_routes: HashSet<&str> = config.get_target(name)
340 + .map(|t| t.expected_routes.iter().map(|s| s.as_str()).collect())
341 + .unwrap_or_default();
339 342 let route_status: Vec<RouteStatusJson> = route_checks
340 343 .into_iter()
344 + .filter(|r| expected_routes.contains(r.path.as_str()))
341 345 .map(|r| RouteStatusJson {
342 346 path: r.path,
343 347 status_code: r.status_code,
@@ -350,8 +354,12 @@ async fn build_target_status(
350 354 let dns_checks = db::get_latest_dns_checks(pool, name)
351 355 .await
352 356 .unwrap_or_default();
357 + let expected_dns: HashSet<(String, String)> = config.get_target(name)
358 + .map(|t| t.dns.iter().map(|d| (d.name.clone(), d.record_type.to_string())).collect())
359 + .unwrap_or_default();
353 360 let dns_status: Vec<DnsStatusJson> = dns_checks
354 361 .into_iter()
362 + .filter(|r| expected_dns.contains(&(r.name.clone(), r.record_type.clone())))
355 363 .map(|r| DnsStatusJson {
356 364 name: r.name,
357 365 record_type: r.record_type,
@@ -30,6 +30,17 @@ pub(crate) fn spawn_dns_tasks(
30 30 info!("{name}: DNS check every {dns_interval_secs}s ({n} records)");
31 31
32 32 handles.push(tokio::spawn(async move {
33 + // Prune stale DNS data from DB (records removed from config)
34 + let expected_dns_keys: Vec<(String, String)> = dns_records
35 + .iter()
36 + .map(|d| (d.name.clone(), d.record_type.to_string()))
37 + .collect();
38 + match db::prune_stale_dns(&pool, &name, &expected_dns_keys).await {
39 + Ok(0) => {}
40 + Ok(n) => info!("{name}: pruned {n} stale DNS check rows"),
41 + Err(e) => tracing::error!("{name}: failed to prune stale DNS: {e}"),
42 + }
43 +
33 44 let mut interval = tokio::time::interval(
34 45 std::time::Duration::from_secs(dns_interval_secs),
35 46 );
@@ -33,6 +33,13 @@ pub(crate) fn spawn_route_tasks(
33 33 info!("{name}: route checks every {route_interval}s ({n} routes)");
34 34
35 35 handles.push(tokio::spawn(async move {
36 + // Prune stale route data from DB (paths removed from config)
37 + match db::prune_stale_routes(&pool, &name, &route_paths).await {
38 + Ok(0) => {}
39 + Ok(n) => info!("{name}: pruned {n} stale route check rows"),
40 + Err(e) => tracing::error!("{name}: failed to prune stale routes: {e}"),
41 + }
42 +
36 43 let mut interval = tokio::time::interval(
37 44 std::time::Duration::from_secs(route_interval),
38 45 );
M src/db.rs +65
@@ -966,6 +966,71 @@ pub async fn get_latest_whois_check(
966 966 .await?)
967 967 }
968 968
969 + // --- Stale data cleanup ---
970 +
971 + /// Delete route_checks for paths no longer in the config.
972 + #[instrument(skip_all)]
973 + pub async fn prune_stale_routes(
974 + pool: &SqlitePool,
975 + target: &str,
976 + expected_routes: &[String],
977 + ) -> Result<u64> {
978 + if expected_routes.is_empty() {
979 + // No configured routes — delete all route checks for this target
980 + let result = sqlx::query("DELETE FROM route_checks WHERE target = ?")
981 + .bind(target)
982 + .execute(pool)
983 + .await?;
984 + return Ok(result.rows_affected());
985 + }
986 +
987 + // Build placeholders for the IN clause
988 + let placeholders: Vec<&str> = expected_routes.iter().map(|_| "?").collect();
989 + let sql = format!(
990 + "DELETE FROM route_checks WHERE target = ? AND path NOT IN ({})",
991 + placeholders.join(", ")
992 + );
993 + let mut query = sqlx::query(&sql).bind(target);
994 + for route in expected_routes {
995 + query = query.bind(route);
996 + }
997 + let result = query.execute(pool).await?;
998 + Ok(result.rows_affected())
999 + }
1000 +
1001 + /// Delete dns_checks for (name, record_type) pairs no longer in the config.
1002 + #[instrument(skip_all)]
1003 + pub async fn prune_stale_dns(
1004 + pool: &SqlitePool,
1005 + target: &str,
1006 + expected_dns: &[(String, String)],
1007 + ) -> Result<u64> {
1008 + if expected_dns.is_empty() {
1009 + // No configured DNS records — delete all DNS checks for this target
1010 + let result = sqlx::query("DELETE FROM dns_checks WHERE target = ?")
1011 + .bind(target)
1012 + .execute(pool)
1013 + .await?;
1014 + return Ok(result.rows_affected());
1015 + }
1016 +
1017 + // Build a compound condition: keep rows matching any configured (name, record_type) pair
1018 + let conditions: Vec<String> = expected_dns
1019 + .iter()
1020 + .map(|_| "(name = ? AND record_type = ?)".to_string())
1021 + .collect();
1022 + let sql = format!(
1023 + "DELETE FROM dns_checks WHERE target = ? AND NOT ({})",
1024 + conditions.join(" OR ")
1025 + );
1026 + let mut query = sqlx::query(&sql).bind(target);
1027 + for (name, record_type) in expected_dns {
1028 + query = query.bind(name).bind(record_type);
1029 + }
1030 + let result = query.execute(pool).await?;
1031 + Ok(result.rows_affected())
1032 + }
1033 +
969 1034 // --- Maintenance ---
970 1035
971 1036 /// Prune result with counts for each table.
@@ -1293,7 +1293,13 @@ async fn route_check_prune() {
1293 1293 #[tokio::test]
1294 1294 async fn api_status_includes_route_status() {
1295 1295 let pool = db::connect_in_memory().await.unwrap();
1296 - let config = test_config();
1296 + let config: pom::config::Config = toml::from_str(r#"
1297 + [targets.mnw]
1298 + label = "MakeNotWork"
1299 + expected_routes = ["/"]
1300 + [targets.mnw.health]
1301 + url = "https://makenot.work/health"
1302 + "#).unwrap();
1297 1303 let app = pom::api::router(pool.clone(), config, None);
1298 1304
1299 1305 // Insert route checks
@@ -2512,7 +2518,16 @@ async fn prune_removes_old_whois_checks() {
2512 2518 #[tokio::test]
2513 2519 async fn api_status_includes_dns_status() {
2514 2520 let pool = db::connect_in_memory().await.unwrap();
2515 - let config = test_config();
2521 + let config: pom::config::Config = toml::from_str(r#"
2522 + [targets.mnw]
2523 + label = "MakeNotWork"
2524 + [targets.mnw.health]
2525 + url = "https://makenot.work/health"
2526 + [[targets.mnw.dns]]
2527 + name = "makenot.work"
2528 + record_type = "A"
2529 + expected = ["5.78.144.244"]
2530 + "#).unwrap();
2516 2531 let app = pom::api::router(pool.clone(), config, None);
2517 2532
2518 2533 // Insert DNS check data