carlos / platform Public

console: Running tile separates not-tracked copies; Web addresses says what it measures

OverviewStats gains InstanceNoReadings and InstanceCounted (Expected
minus the unknowable). assembleOverview's route loop uses Task 2's
expectsReadings to classify silent instance-kind routes as no-readings
rather than folding them into the shortfall count, and sets
InstanceCounted before healthVerdict runs. healthVerdict's shortfall
case now compares against InstanceCounted, not InstanceExpected, so a
route the sweep was never told how to sample (the 2026-08-10 tito
false alarm) can't trip "Needs attention".

Running tile shows "N of Counted" and names the no-readings copies
instead of silently baking them into a denominator nobody could reach.
Web addresses tile's "all working" becomes "DNS & certs OK" — says
what it measures instead of a vague absolute.

TestOverviewSeparatesNoReadings and TestVerdictIgnoresNoReadings pin
both. TestOverviewVerdict's existing struct-literal cases gain
InstanceCounted (equal to InstanceExpected, since none of those cases
have no-readings rows) now that the shortfall case reads from it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell e56552c88ed404d9ed71f1f9eb23e17cb9ae7aff parent 2c55fba
3 files changed, +52 −8
  • internal/console/overview.go +12 −1
  • internal/console/overview_test.go +37 −4
  • internal/console/templates/app_overview.html +3 −3
diff --git a/internal/console/overview.go b/internal/console/overview.go
index abadbea46..5a635f281 100644
--- a/internal/console/overview.go
+++ b/internal/console/overview.go
@@ -33,6 +33,14 @@ type OverviewStats struct {
InstanceTotal int
InstanceReporting int
InstanceAsleep int
+ // InstanceNoReadings counts instance-kind routes nobody could sample: not
+ // provisioned, no carlos-app@ backing, no stamped unit. They are honest
+ // unknowns — the tile names them separately and the verdict must not
+ // treat them as down (the 2026-08-10 tito false alarm).
+ InstanceNoReadings int
+ // InstanceCounted is the Running tile's denominator: InstanceExpected
+ // minus the unknowable. Templates render facts, never compute them.
+ InstanceCounted int
// MetricsUnavailable: the sweep itself failed (nil metrics), which is
// "readings unavailable", not "zero instances reporting".
@@ -129,9 +137,12 @@ func assembleOverview(view *AppView, acct string, metrics []InstanceMetrics, ver
st.InstanceReporting++
} else if rt.Hibernate {
st.InstanceAsleep++
+ } else if !expectsReadings(rt.Provisioned, rt.Unit, rt.Backing) {
+ st.InstanceNoReadings++
}
}
}
+ st.InstanceCounted = st.InstanceExpected - st.InstanceNoReadings
// Routes and Domains are parallel slices — Assemble appends one of each
// per registry route, in the same loop — which is what lets the domain
// rows be account-filtered here at all (DomainView carries no account of
@@ -181,7 +192,7 @@ func healthVerdict(st OverviewStats) (string, string) {
return "attention", "Needs attention"
case st.MetricsUnavailable && st.InstanceExpected > 0:
return "unknown", "Can't tell right now"
- case st.InstanceReporting+st.InstanceAsleep < st.InstanceExpected:
+ case st.InstanceReporting+st.InstanceAsleep < st.InstanceCounted:
return "attention", "Needs attention"
default:
return "ok", "All good"
diff --git a/internal/console/overview_test.go b/internal/console/overview_test.go
index ad25bede5..d58d08d09 100644
--- a/internal/console/overview_test.go
+++ b/internal/console/overview_test.go
@@ -141,6 +141,39 @@ func TestOverviewSumsOnlyReportingInstances(t *testing.T) {
}
}
+// TestOverviewSeparatesNoReadings pins InstanceNoReadings/InstanceCounted:
+// a silent instance route the sweep was never told how to sample (no
+// Provisioned, Unit or Backing) is honest ignorance, not an outage, and
+// must not count against the Running tile's denominator.
+func TestOverviewSeparatesNoReadings(t *testing.T) {
+ view := &AppView{Routes: []RouteView{
+ {Host: "a.x", Kind: "instance", Account: "acct1", Provisioned: true}, // reporting
+ {Host: "b.x", Kind: "instance", Account: "acct1", Provisioned: true}, // silent, expected
+ {Host: "c.x", Kind: "instance", Account: "acct1"}, // silent, bespoke-unstamped
+ {Host: "d.x", Kind: "instance", Account: "acct1", Hibernate: true}, // asleep
+ }, Domains: []DomainView{{}, {}, {}, {}}}
+ metrics := []InstanceMetrics{{Host: "a.x", Box: "box1"}}
+ st := assembleOverview(view, "acct1", metrics, nil)
+ if st.InstanceExpected != 4 || st.InstanceReporting != 1 || st.InstanceAsleep != 1 ||
+ st.InstanceNoReadings != 1 || st.InstanceCounted != 3 {
+ t.Fatalf("counts: %+v", st)
+ }
+}
+
+// TestVerdictIgnoresNoReadings pins that healthVerdict's shortfall case
+// compares against InstanceCounted, not InstanceExpected — a no-readings
+// row must not trip "attention" (the 2026-08-10 tito false alarm).
+func TestVerdictIgnoresNoReadings(t *testing.T) {
+ // 1 reporting + 1 asleep + 1 no-readings out of 3 counted... construct an
+ // OverviewStats where Reporting+Asleep == InstanceCounted and assert the
+ // verdict is NOT "attention" on account of the no-readings row.
+ st := OverviewStats{HasStable: true, InstanceExpected: 3, InstanceCounted: 2,
+ InstanceReporting: 1, InstanceAsleep: 1, InstanceNoReadings: 1}
+ if v, _ := healthVerdict(st); v != "ok" {
+ t.Fatalf("no-readings rows must not trip the verdict: %s", v)
+ }
+}
+
// PrimaryHost is the apex of this account's OWN routes — Task 2's apexHost
// reused here, so the headline's "live at" link names the same host
// summarize's account table does (same fixture shape as
@@ -487,10 +520,10 @@ func TestOverviewVerdict(t *testing.T) {
verdict string
label string
}{
- {"all good", OverviewStats{HasStable: true, InstanceExpected: 2, InstanceReporting: 2}, "ok", "All good"},
- {"asleep counts as accounted for", OverviewStats{HasStable: true, InstanceExpected: 2, InstanceReporting: 1, InstanceAsleep: 1}, "ok", "All good"},
- {"silent instance", OverviewStats{HasStable: true, InstanceExpected: 2, InstanceReporting: 1}, "attention", "Needs attention"},
- {"domain attention", OverviewStats{HasStable: true, InstanceExpected: 1, InstanceReporting: 1, DomainAttention: 1}, "attention", "Needs attention"},
+ {"all good", OverviewStats{HasStable: true, InstanceExpected: 2, InstanceCounted: 2, InstanceReporting: 2}, "ok", "All good"},
+ {"asleep counts as accounted for", OverviewStats{HasStable: true, InstanceExpected: 2, InstanceCounted: 2, InstanceReporting: 1, InstanceAsleep: 1}, "ok", "All good"},
+ {"silent instance", OverviewStats{HasStable: true, InstanceExpected: 2, InstanceCounted: 2, InstanceReporting: 1}, "attention", "Needs attention"},
+ {"domain attention", OverviewStats{HasStable: true, InstanceExpected: 1, InstanceCounted: 1, InstanceReporting: 1, DomainAttention: 1}, "attention", "Needs attention"},
{"not deployed", OverviewStats{}, "notdeployed", "Not deployed yet"},
{"unreadable stable pointer", OverviewStats{LiveErr: "boom"}, "unknown", "Can't tell right now"},
{"sweep failed with instances expected", OverviewStats{HasStable: true, InstanceExpected: 1, MetricsUnavailable: true}, "unknown", "Can't tell right now"},
diff --git a/internal/console/templates/app_overview.html b/internal/console/templates/app_overview.html
index 81689f40d..c2212bd7a 100644
--- a/internal/console/templates/app_overview.html
+++ b/internal/console/templates/app_overview.html
@@ -36,8 +36,8 @@
routinely share a box. */}}
<a class="tile" href="{{$.Nav}}/apps/{{$.AppName}}/instances">
<h2>Running</h2>
- <p class="tile-value">{{if eq .InstanceTotal 0}}—{{else if eq .InstanceExpected 0}}Served{{else if .MetricsUnavailable}}?{{else}}{{.InstanceReporting}} of {{.InstanceExpected}}{{end}}</p>
- <p class="tile-detail">{{if eq .InstanceTotal 0}}nothing running yet{{else if eq .InstanceExpected 0}}static — the platform serves it, nothing to run{{else if .MetricsUnavailable}}no data right now{{else}}copies responding{{if .InstanceAsleep}} · {{.InstanceAsleep}} asleep{{end}}{{end}}</p>
+ <p class="tile-value">{{if eq .InstanceTotal 0}}—{{else if eq .InstanceExpected 0}}Served{{else if .MetricsUnavailable}}?{{else if eq .InstanceCounted 0}}?{{else}}{{.InstanceReporting}} of {{.InstanceCounted}}{{end}}</p>
+ <p class="tile-detail">{{if eq .InstanceTotal 0}}nothing running yet{{else if eq .InstanceExpected 0}}static — the platform serves it, nothing to run{{else if .MetricsUnavailable}}no data right now{{else if eq .InstanceCounted 0}}no readings for {{.InstanceNoReadings}} copies — stamp their units to see health{{else}}copies reporting{{if .InstanceAsleep}} · {{.InstanceAsleep}} asleep{{end}}{{if .InstanceNoReadings}} · {{.InstanceNoReadings}} no readings{{end}}{{end}}</p>
</a>
<a class="tile" href="{{$.Nav}}/apps/{{$.AppName}}/instances">
<h2>Resources</h2>
@@ -47,7 +47,7 @@
<a class="tile" href="{{$.Nav}}/apps/{{$.AppName}}/settings">
<h2>Web addresses</h2>
<p class="tile-value">{{.DomainTotal}}</p>
- <p class="tile-detail">{{if eq .DomainTotal 0}}one appears after your first deploy{{else if .DomainAttention}}<span class="error">{{.DomainAttention}} need{{if eq .DomainAttention 1}}s{{end}} attention</span>{{else}}all working{{end}}</p>
+ <p class="tile-detail">{{if eq .DomainTotal 0}}one appears after your first deploy{{else if .DomainAttention}}<span class="error">{{.DomainAttention}} need{{if eq .DomainAttention 1}}s{{end}} attention</span>{{else}}DNS &amp; certs OK{{end}}</p>
</a>
<a class="tile" href="{{$.Nav}}/apps/{{$.AppName}}/releases">
<h2>Versions</h2>