console: overview leads with URL + health verdict, four plain tiles
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 files changed,
+76
−39
internal/console/overview.go+11 −0internal/console/overview_test.go+38 −6internal/console/server_test.go+2 −2internal/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}} |