carlos / platform Public

console: overview leads with URL + health verdict, four plain tiles

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell a5c5e55d9ed07840571d6db21c98924e3c646887 parent 1737fb5
4 files changed, +76 −39
  • internal/console/overview.go +11 −0
  • internal/console/overview_test.go +38 −6
  • internal/console/server_test.go +2 −2
  • internal/console/templates/app_overview.html +25 −31
diff --git a/internal/console/overview.go b/internal/console/overview.go
index 65d6ede18..eb802cb93 100644
--- a/internal/console/overview.go
+++ b/internal/console/overview.go
@@ -51,6 +51,10 @@ type OverviewStats struct {
Verdict string
VerdictLabel string
+ // PrimaryHost is the apex host, for the headline's "live at" link; empty
+ // when no routes.
+ PrimaryHost string
+
// Current resource use summed across every reporting instance. Nil when
// nothing reports — the tiles render the em-dash, never zeros.
CPUSum *float64
@@ -149,6 +153,13 @@ func assembleOverview(view *AppView, acct string, metrics []InstanceMetrics, ver
st.LatestVersion = versions[0].Version
}
st.Verdict, st.VerdictLabel = healthVerdict(st)
+ var mine []RouteView
+ for _, rt := range view.Routes {
+ if rt.Account == acct {
+ mine = append(mine, rt)
+ }
+ }
+ st.PrimaryHost = apexHost(mine)
return st
}
diff --git a/internal/console/overview_test.go b/internal/console/overview_test.go
index 84de7f5e4..25d7c6aef 100644
--- a/internal/console/overview_test.go
+++ b/internal/console/overview_test.go
@@ -139,6 +139,28 @@ func TestOverviewSumsOnlyReportingInstances(t *testing.T) {
}
}
+// 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
+// TestSummarizeVerdictAndURL in server_test.go).
+func TestOverviewPrimaryHost(t *testing.T) {
+ view := &AppView{App: "kass", Routes: []RouteView{
+ {Host: "kass.bab.oncarlos.com", Account: "bab", Kind: "instance"},
+ {Host: "kass.example.com", Account: "bab", Kind: "instance"},
+ // Another account's same-named app: not this dashboard's host.
+ {Host: "kass.theirs.oncarlos.com", Account: "cdef", Kind: "instance"},
+ }}
+ st := assembleOverview(view, "bab", nil, nil)
+ if st.PrimaryHost != "kass.example.com" {
+ t.Fatalf("PrimaryHost = %q, want the apex host kass.example.com", st.PrimaryHost)
+ }
+
+ empty := assembleOverview(&AppView{App: "kass"}, "bab", nil, nil)
+ if empty.PrimaryHost != "" {
+ t.Fatalf("PrimaryHost = %q, want empty with no routes", empty.PrimaryHost)
+ }
+}
+
// Attention means someone has to do something: DNS that is not pointing here,
// or issuance that failed. A cert mid-issuance is patience, not attention —
// badging it would make every new domain look broken for its first minute.
@@ -282,13 +304,23 @@ func TestOverviewPageRendersTiles(t *testing.T) {
if !strings.Contains(body, `class="tile"`) {
t.Fatalf("no tiles on the Overview:\n%s", body)
}
- // The fixture's stable holds v1 — the number the "Live now" tile exists
- // to show.
- if !strings.Contains(body, "Live now") || !strings.Contains(body, "<code>v1</code>") {
- t.Fatalf("the Live now tile does not show what stable serves:\n%s", body)
+ // The fixture's stable holds v1 — the number the headline exists to show.
+ if !strings.Contains(body, "<code>v1</code>") {
+ t.Fatalf("the headline does not show what stable serves:\n%s", body)
+ }
+ // The fixture's two domains need attention, which outranks everything
+ // else in healthVerdict — the headline verdict this page exists to show.
+ if !strings.Contains(body, "Needs attention") {
+ t.Fatalf("the headline verdict is missing:\n%s", body)
+ }
+ if !strings.Contains(body, "Machines") {
+ t.Fatalf("no Machines tile on the Overview:\n%s", body)
+ }
+ if strings.Contains(body, "stable pointer unreadable") {
+ t.Fatalf("stale tile copy still rendered:\n%s", body)
}
- if !strings.Contains(body, "on stable · promoted") {
- t.Fatalf("the Live now tile does not say when it was promoted:\n%s", body)
+ if strings.Contains(body, "Supply-chain bake") {
+ t.Fatalf("the Overview still says Supply-chain bake:\n%s", body)
}
if strings.Contains(body, `id="rung-`) {
t.Fatalf("the ladder is still rendered on the Overview:\n%s", body)
diff --git a/internal/console/server_test.go b/internal/console/server_test.go
index 15427667b..da52c1982 100644
--- a/internal/console/server_test.go
+++ b/internal/console/server_test.go
@@ -534,7 +534,7 @@ func TestOverviewIsALadderNotAnEverythingPage(t *testing.T) {
t.Fatalf("overview still renders %q", gone)
}
}
- for _, want := range []string{`class="tiles stat-tiles"`, "Releases", "Domains", "Instances"} {
+ for _, want := range []string{`class="tiles stat-tiles"`, "Machines", "Web addresses", "Versions"} {
if !strings.Contains(body, want) {
t.Fatalf("overview missing %q", want)
}
@@ -1207,7 +1207,7 @@ func TestPromotionsDisabledWithoutKey(t *testing.T) {
csrf := sessionToken(t, fx, c)
_, _, body := get(t, c, fx.u("/apps/kass"))
- if !strings.Contains(body, "Promotions disabled: no signing key on this host.") {
+ if !strings.Contains(body, "Deploys are temporarily unavailable on this console.") {
t.Fatal("keyless console is missing the disabled note")
}
// The Domains section is a read — keyless (read-only) mode still renders it.
diff --git a/internal/console/templates/app_overview.html b/internal/console/templates/app_overview.html
index 2afa9ea83..8091f6edd 100644
--- a/internal/console/templates/app_overview.html
+++ b/internal/console/templates/app_overview.html
@@ -14,42 +14,36 @@
</p>
{{end}}
{{with .Overview}}
+<section class="pagehead" aria-label="App status">
+ <p><span class="chip chip-health-{{.Verdict}}">{{.VerdictLabel}}</span></p>
+ {{if .HasStable}}
+ <p>{{$.AppName}} is live{{if .PrimaryHost}} at <a href="https://{{.PrimaryHost}}">{{.PrimaryHost}}</a>{{end}} —
+ running <code>{{.LiveVersion}}</code>, published {{ago .LivePromotedAt}}{{if .LiveActor}} by {{.LiveActor}}{{end}}.</p>
+ {{else if .LiveErr}}
+ <p>We can't tell what's live right now — the record of what's live didn't read. It usually recovers on its own; check again shortly.</p>
+ {{else}}
+ <p>Nothing is live yet. Publish a first version from your terminal — the command is on the
+ <a href="{{$.Nav}}/apps/{{$.AppName}}/releases">Versions</a> tab.</p>
+ {{end}}
+</section>
<section class="tiles stat-tiles" aria-label="Vital statistics">
- <a class="tile" href="{{$.Nav}}/apps/{{$.AppName}}/releases">
- <h2>Live now</h2>
- {{if .HasStable}}
- <p class="tile-value"><code>{{.LiveVersion}}</code></p>
- <p class="tile-detail">on stable · promoted {{ago .LivePromotedAt}}{{if .LiveActor}} by {{.LiveActor}}{{end}}</p>
- {{else if .LiveErr}}
- <p class="tile-value">—</p>
- <p class="tile-detail error">stable pointer unreadable</p>
- {{else}}
- <p class="tile-value">not live</p>
- <p class="tile-detail">nothing on stable yet</p>
- {{end}}
- </a>
<a class="tile" href="{{$.Nav}}/apps/{{$.AppName}}/instances">
- <h2>Instances</h2>
+ <h2>Machines</h2>
<p class="tile-value">{{.InstanceTotal}}</p>
- <p class="tile-detail">{{if eq .InstanceTotal 0}}no hosts yet{{else if .MetricsUnavailable}}readings unavailable{{else}}{{.InstanceReporting}} reporting{{if .InstanceAsleep}} · {{.InstanceAsleep}} asleep{{end}}{{end}}</p>
- </a>
- <a class="tile" href="{{$.Nav}}/apps/{{$.AppName}}/instances">
- <h2>CPU</h2>
- <p class="tile-value">{{pct .CPUSum}}</p>
- <p class="tile-detail">{{if .CPUSum}}across {{.InstanceReporting}} reporting{{else if .MetricsUnavailable}}readings unavailable{{else}}no recent readings{{end}}</p>
+ <p class="tile-detail">{{if eq .InstanceTotal 0}}none yet{{else if .MetricsUnavailable}}no data right now{{else if eq .InstanceExpected 0}}served by the platform{{else}}{{.InstanceReporting}} of {{.InstanceExpected}} responding{{if .InstanceAsleep}} · {{.InstanceAsleep}} asleep{{end}}{{end}}</p>
</a>
<a class="tile" href="{{$.Nav}}/apps/{{$.AppName}}/instances">
- <h2>Memory</h2>
- <p class="tile-value">{{bytesOrDash .MemSum}}</p>
- <p class="tile-detail">{{if .MemSum}}across {{.InstanceReporting}} reporting{{else if .MetricsUnavailable}}readings unavailable{{else}}no recent readings{{end}}</p>
+ <h2>Resources</h2>
+ <p class="tile-value">{{pct .CPUSum}} · {{bytesOrDash .MemSum}}</p>
+ <p class="tile-detail">{{if .CPUSum}}CPU · memory, from the machines responding{{else if .MetricsUnavailable}}no data right now{{else}}no recent data{{end}}</p>
</a>
<a class="tile" href="{{$.Nav}}/apps/{{$.AppName}}/settings">
- <h2>Domains</h2>
+ <h2>Web addresses</h2>
<p class="tile-value">{{.DomainTotal}}</p>
- <p class="tile-detail">{{if eq .DomainTotal 0}}the first route brings one{{else if .DomainAttention}}<span class="error">{{.DomainAttention}} need attention</span>{{else}}all pointing, TLS ready{{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}}all working{{end}}</p>
</a>
<a class="tile" href="{{$.Nav}}/apps/{{$.AppName}}/releases">
- <h2>Releases</h2>
+ <h2>Versions</h2>
<p class="tile-value">{{.ReleaseCount}}{{if .ReleasesCapped}}+{{end}}</p>
<p class="tile-detail">{{if .LatestVersion}}latest <code>{{.LatestVersion}}</code>{{else}}nothing shipped yet{{end}}</p>
</a>
@@ -57,13 +51,13 @@
{{end}}
{{with .Detail.Production}}
{{if .Err}}
-<p class="small error">Supply-chain bake: flag unreadable ({{.Err}}) — hosts keep baking. Repair it on the <a href="{{$.Nav}}/apps/{{$.AppName}}/releases">Releases</a> page.</p>
+<p class="small error">Safety delay: this app's setting can't be read right now, so the delay stays on. Repair it on the <a href="{{$.Nav}}/apps/{{$.AppName}}/releases">Versions</a> tab.</p>
{{else if .On}}
-<p class="small">Supply-chain bake: <strong>on</strong> — releases hold on beta and stable before hosts adopt. Managed on <a href="{{$.Nav}}/apps/{{$.AppName}}/releases">Releases</a>.</p>
+<p class="small">Safety delay: <strong>on</strong> — new versions wait before going live. Managed on <a href="{{$.Nav}}/apps/{{$.AppName}}/releases">Versions</a>.</p>
{{else if .TurningOff}}
-<p class="small">Supply-chain bake: <strong>turning off</strong> — hosts honor it from ~{{fmtTime .EffectiveAt}}, each on its own clock.</p>
+<p class="small">Safety delay: <strong>turning off</strong> — in effect until ~{{fmtTime .EffectiveAt}}.</p>
{{else}}
-<p class="small">Supply-chain bake: off — hosts adopt promoted releases immediately.{{if .Nudge}} {{icon "triangle-alert"}} This app looks production-wired — consider turning it on, on <a href="{{$.Nav}}/apps/{{$.AppName}}/releases">Releases</a>.{{end}}</p>
+<p class="small">Safety delay: off — new versions go live as soon as they're promoted.{{if .Nudge}} {{icon "triangle-alert"}} This app looks like it's serving real users — consider turning it on, on <a href="{{$.Nav}}/apps/{{$.AppName}}/releases">Versions</a>.{{end}}</p>
{{end}}
{{end}}
{{if .Events}}
@@ -75,5 +69,5 @@
</ul>
<p class="small"><a href="{{.Nav}}/apps/{{.AppName}}/activity">All activity</a></p>
{{end}}
-{{if not .CanPromote}}<p class="small">Promotions disabled: no signing key on this host.</p>{{end}}
+{{if not .CanPromote}}<p class="small">Deploys are temporarily unavailable on this console.</p>{{end}}
{{template "bottom" .}}{{end}}