Skip to main content

max / makenotwork

Say how long each version has been the live one The roll-up said what is running and when it was last checked, which leaves out the question usually asked next: since when. PoM cannot know when something was deployed, nothing tells it, so it reports the honest neighbour of that, the first check in the unbroken run reporting the current version. The column is SINCE rather than DEPLOYED for exactly that reason, and it is bounded by retention: a version older than the prune window looks first-seen at the oldest row still held. A failed check reports no version at all, so it is skipped rather than counted as a change. Otherwise every blip of unreachable would read as a fresh deploy, which is the one wrong answer this column could give. Also stops blaming a target for a check that never ran: a configured repo and no health history reported "target reports no git_sha", when what had happened was nothing at all.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-29 18:55 UTC
Signed with PGP, not checked
Commit: b288f01c491439a4cf2974914e051d53a965292e
Parent: 6db7719
6 files changed, +154 insertions, -10 deletions
@@ -155,7 +155,7 @@
155 155 | `/api/status` | GET | JSON summary of all targets (latest health, uptime, latency, TLS, staleness, incidents) |
156 156 | `/api/status/{target}` | GET | Same as above for a single target |
157 157 | `/api/trends/{target}` | GET | Latency trend data with configurable window and bucket size (`?hours=24&bucket_minutes=60`) |
158 - | `/api/versions` | GET | What each target is running: version, git sha, when it was seen, commits behind this host's checkout |
158 + | `/api/versions` | GET | What each target is running: version, git sha, when that version was first seen, when it was last checked, commits behind this host's checkout |
159 159 | `/api/peer/info` | GET | This instance's identity (id, name, version, targets, started_at) |
160 160 | `/api/peer/status` | GET | This instance's full view: identity + target statuses + peer summaries |
161 161 | `/api/mesh` | GET | Aggregated mesh view: self + each peer's cached status |
@@ -372,20 +372,24 @@
372 372 return "No targets configured.\n".to_string();
373 373 }
374 374
375 - let cells: Vec<[String; 5]> = rows
375 + let cells: Vec<[String; 6]> = rows
376 376 .iter()
377 377 .map(|r| {
378 378 [
379 379 scrub(&r.target),
380 380 r.version.as_deref().map_or_else(dash, scrub),
381 381 r.git_sha.as_deref().map_or_else(dash, short_sha),
382 + r.version_since.as_deref().map_or_else(dash, minute_stamp),
382 383 r.checked_at.as_deref().map_or_else(dash, minute_stamp),
383 384 r.commits_behind.map_or_else(dash, |n| n.to_string()),
384 385 ]
385 386 })
386 387 .collect();
387 388
388 - const HEADERS: [&str; 5] = ["TARGET", "VERSION", "SHA", "CHECKED", "BEHIND"];
389 + // SINCE is when this version was first seen, not when it was deployed:
390 + // nothing tells PoM about a deploy, and the column header should not
391 + // promise more than a poller can know.
392 + const HEADERS: [&str; 6] = ["TARGET", "VERSION", "SHA", "SINCE", "CHECKED", "BEHIND"];
389 393 let widths: Vec<usize> = (0..HEADERS.len())
390 394 .map(|i| {
391 395 cells
@@ -412,7 +416,7 @@
412 416 }
413 417
414 418 /// Write one table row, every column but the last padded to its width.
415 - fn write_row(out: &mut String, cells: &[String; 5], widths: &[usize]) {
419 + fn write_row(out: &mut String, cells: &[String; 6], widths: &[usize]) {
416 420 for (i, cell) in cells.iter().enumerate() {
417 421 if i + 1 == cells.len() {
418 422 writeln!(out, "{cell}").unwrap();
@@ -1586,6 +1590,7 @@
1586 1590 version: version.map(String::from),
1587 1591 git_sha: sha.map(String::from),
1588 1592 checked_at: Some("2026-07-29T18:04:37.123456+00:00".to_string()),
1593 + version_since: Some("2026-07-28T09:00:00+00:00".to_string()),
1589 1594 commits_behind: Some(8),
1590 1595 behind_error: None,
1591 1596 }
@@ -1623,6 +1628,7 @@
1623 1628 version: None,
1624 1629 git_sha: None,
1625 1630 checked_at: None,
1631 + version_since: None,
1626 1632 commits_behind: None,
1627 1633 behind_error: None,
1628 1634 }];
@@ -1630,7 +1636,7 @@
1630 1636 let row = out.lines().nth(1).unwrap();
1631 1637 assert_eq!(
1632 1638 row.split_whitespace().collect::<Vec<_>>(),
1633 - ["mt", "-", "-", "-", "-"]
1639 + ["mt", "-", "-", "-", "-", "-"]
1634 1640 );
1635 1641 }
1636 1642
@@ -245,6 +245,11 @@
245 245 /// When that health check ran, RFC 3339. `None` if the target has never
246 246 /// been checked, or has no health endpoint at all.
247 247 pub checked_at: Option<String>,
248 + /// When the live version was first seen, RFC 3339. The closest honest
249 + /// answer to "when was this deployed": PoM observes versions, not deploys,
250 + /// and only as far back as retention keeps the checks.
251 + #[serde(default)]
252 + pub version_since: Option<String>,
248 253 /// Commits in the configured repo between the live sha and local HEAD.
249 254 /// `None` when there is no repo configured, no live sha to anchor on, or
250 255 /// the count could not be taken: the column degrades to blank rather than
@@ -31,12 +31,15 @@
31 31
32 32 let (commits_behind, behind_error) = match (&target.repo, &git_sha) {
33 33 (None, _) => (None, None),
34 - // A repo is configured but the target never reported a commit, so
35 - // there is nothing to anchor the count to. Say which of the two is
36 - // missing: the fix is on the target, not in pom.toml.
34 + // A repo is configured but there is no commit to anchor the count
35 + // to. A target that has been checked and did not send one is a gap
36 + // on the target; a target that has never been checked has nothing
37 + // to say yet, and the blank row already says it.
37 38 (Some(_), None) => (
38 39 None,
39 - Some("target reports no git_sha in its health body".to_string()),
40 + health
41 + .as_ref()
42 + .map(|_| "target reports no git_sha in its health body".to_string()),
40 43 ),
41 44 (Some(repo), Some(sha)) => match commits_behind(repo, sha).await {
42 45 Ok(n) => (Some(n), None),
@@ -44,12 +47,19 @@
44 47 },
45 48 };
46 49
50 + let version = details.and_then(|d| d.version.clone());
51 + let version_since = match &version {
52 + Some(v) => db::get_version_first_seen(pool, &name, v).await?,
53 + None => None,
54 + };
55 +
47 56 rows.push(VersionRow {
48 57 target: name,
49 58 label: target.label.clone(),
50 - version: details.and_then(|d| d.version.clone()),
59 + version,
51 60 git_sha,
52 61 checked_at: health.as_ref().map(|h| h.checked_at.clone()),
62 + version_since,
53 63 commits_behind,
54 64 behind_error,
55 65 });
@@ -3938,6 +3938,32 @@
3938 3938 assert_eq!(rows[0].version.as_deref(), Some("0.11.0"));
3939 3939 }
3940 3940
3941 + #[tokio::test]
3942 + async fn versions_stays_quiet_about_a_target_that_has_never_been_checked() {
3943 + // A repo is configured, but no health check has ever run. There is nothing
3944 + // to report yet, and claiming the target "reports no git_sha" would blame
3945 + // it for a check that never happened.
3946 + let (path, _) = scratch_repo("nocheck", 1, None);
3947 + let pool = db::connect_in_memory().await.unwrap();
3948 + let config: pom::config::Config = toml::from_str(&format!(
3949 + r#"
3950 + [targets.mnw]
3951 + label = "MakeNotWork"
3952 + [targets.mnw.repo]
3953 + path = "{}"
3954 + "#,
3955 + path.display()
3956 + ))
3957 + .unwrap();
3958 +
3959 + let rows = pom::versions::collect(&pool, &config).await.unwrap();
3960 + assert!(rows[0].checked_at.is_none());
3961 + assert!(rows[0].commits_behind.is_none());
3962 + assert!(rows[0].behind_error.is_none(), "{:?}", rows[0].behind_error);
3963 +
3964 + std::fs::remove_dir_all(&path).unwrap();
3965 + }
3966 +
3941 3967 #[tokio::test]
3942 3968 async fn versions_says_when_a_target_reports_no_sha_to_anchor_on() {
3943 3969 let (path, _) = scratch_repo("nosha", 1, None);
@@ -4271,3 +4297,66 @@
4271 4297 assert_eq!(json[0]["version"], "0.11.0");
4272 4298 assert_eq!(json[0]["git_sha"], "6402bf4e");
4273 4299 }
4300 +
4301 + #[tokio::test]
4302 + async fn versions_reports_when_the_live_version_was_first_seen() {
4303 + let pool = db::connect_in_memory().await.unwrap();
4304 + let config: pom::config::Config = toml::from_str(
4305 + r#"
4306 + [targets.mnw]
4307 + label = "MakeNotWork"
4308 + "#,
4309 + )
4310 + .unwrap();
4311 +
4312 + insert_version_health(&pool, "mnw", Some("0.10.0"), None, "2026-07-20T00:00:00Z").await;
4313 + insert_version_health(&pool, "mnw", Some("0.11.0"), None, "2026-07-27T00:00:00Z").await;
4314 + // An unreachable check reports no version at all. It must not read as a
4315 + // version change, or every blip would look like a fresh deploy.
4316 + let blip = HealthSnapshot {
4317 + id: None,
4318 + target: "mnw".to_string(),
4319 + status: HealthStatus::Unreachable,
4320 + checked_at: "2026-07-28T00:00:00Z".to_string(),
4321 + response_time_ms: 0,
4322 + details: None,
4323 + error: Some("timeout".to_string()),
4324 + };
4325 + db::insert_health_check(&pool, &blip).await.unwrap();
4326 + insert_version_health(&pool, "mnw", Some("0.11.0"), None, "2026-07-29T00:00:00Z").await;
4327 +
4328 + let rows = pom::versions::collect(&pool, &config).await.unwrap();
4329 + assert_eq!(rows[0].version.as_deref(), Some("0.11.0"));
4330 + assert_eq!(
4331 + rows[0].version_since.as_deref(),
4332 + Some("2026-07-27T00:00:00Z")
4333 + );
4334 + assert_eq!(rows[0].checked_at.as_deref(), Some("2026-07-29T00:00:00Z"));
4335 + }
4336 +
4337 + #[tokio::test]
4338 + async fn versions_first_seen_moves_when_the_version_does() {
4339 + let pool = db::connect_in_memory().await.unwrap();
4340 + let config: pom::config::Config = toml::from_str(
4341 + r#"
4342 + [targets.mnw]
4343 + label = "MakeNotWork"
4344 + "#,
4345 + )
4346 + .unwrap();
4347 +
4348 + insert_version_health(&pool, "mnw", Some("0.11.0"), None, "2026-07-27T00:00:00Z").await;
4349 + let before = pom::versions::collect(&pool, &config).await.unwrap();
4350 + assert_eq!(
4351 + before[0].version_since.as_deref(),
4352 + Some("2026-07-27T00:00:00Z")
4353 + );
4354 +
4355 + insert_version_health(&pool, "mnw", Some("0.11.1"), None, "2026-07-29T12:00:00Z").await;
4356 + let after = pom::versions::collect(&pool, &config).await.unwrap();
4357 + assert_eq!(after[0].version.as_deref(), Some("0.11.1"));
4358 + assert_eq!(
4359 + after[0].version_since.as_deref(),
4360 + Some("2026-07-29T12:00:00Z")
4361 + );
4362 + }
@@ -75,6 +75,40 @@
75 75 Ok(row.map(HealthCheckRow::into_snapshot))
76 76 }
77 77
78 + /// When the target's *current* version was first seen, i.e. the earliest check
79 + /// in the unbroken run of checks reporting it.
80 + ///
81 + /// The closest PoM can honestly get to "when was this deployed": it observes
82 + /// versions, never deployments. Two consequences worth knowing at the call
83 + /// site. A failed check reports no version at all, and is skipped rather than
84 + /// treated as a version change, so an outage does not read as a redeploy. And
85 + /// the answer is bounded by retention: a version older than the prune window
86 + /// looks like it was first seen at the oldest row still held.
87 + #[instrument(skip_all)]
88 + pub async fn get_version_first_seen(
89 + pool: &SqlitePool,
90 + target: &str,
91 + version: &str,
92 + ) -> Result<Option<String>> {
93 + let row = sqlx::query_as::<_, (Option<String>,)>(
94 + "SELECT MIN(checked_at) FROM health_checks
95 + WHERE target = ?1
96 + AND json_extract(details_json, '$.version') = ?2
97 + AND id > COALESCE(
98 + (SELECT MAX(id) FROM health_checks
99 + WHERE target = ?1
100 + AND json_extract(details_json, '$.version') IS NOT NULL
101 + AND json_extract(details_json, '$.version') != ?2),
102 + 0)",
103 + )
104 + .bind(target)
105 + .bind(version)
106 + .fetch_optional(pool)
107 + .await?;
108 +
109 + Ok(row.and_then(|r| r.0))
110 + }
111 +
78 112 /// Calculate uptime percentage for a target over the given number of hours.
79 113 /// Returns the percentage of health checks with "operational" status.
80 114 #[instrument(skip_all)]