carlos / platform Public

console: environment beside every route, honest prod-wired badging

RouteView/LadderRoute both gain Environment (plumbed straight off
registry.Route.Environment via view.go's Assemble — Task 2's column, no
new registry read). Every route on the app page's ladder now shows the
environment it is bound to, beside the channel it is listed under, in a
new neutral .chip-env chip with a vendored Lucide "layers" icon —
"default" for an unbound route.

prodWiredCanaryHosts drops the apex-host guess (the placeholder marked
delete-me-when-environments-land) for spec §4a's real definition: a
canary/* route is prod-wired iff it binds the same environment as (any
of) the app's stable route(s) — both binding default counts as a match.
An app with no stable route flags nothing, since there is no production
environment to compare against. Still one function; callers unchanged.

Tests: environment renders for both a bound and an unbound route;
TestProdWiredCanaryIsBadgedDistinctly rewritten around the new same-env/
different-env definition (its old apex-host premise no longer holds);
a new no-stable-route case; a member sees the same environment and
prod-wired chrome an owner does (visibility, not permission).
Paul Campbell 293c90e4f67856a7590e6a284c29737599f88636 parent 7ca93b5
6 files changed, +160 −63
  • internal/console/icons.go +8 −0
  • internal/console/ladder.go +42 −47
  • internal/console/ladder_test.go +99 −16
  • internal/console/templates/ladder.html +1 −0
  • internal/console/templates/layout.html +4 −0
  • internal/console/view.go +6 −0
diff --git a/internal/console/icons.go b/internal/console/icons.go
index 0956bb164..6e5a22618 100644
--- a/internal/console/icons.go
+++ b/internal/console/icons.go
@@ -77,6 +77,13 @@ const (
iconTriangleAlert template.HTML = iconOpen +
`<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/>` +
`<path d="M12 9v4"/><path d="M12 17h.01"/></svg>`
+
+ // A route's bound environment, beside the rung it is listed under
+ // (Lucide: layers).
+ iconLayers template.HTML = iconOpen +
+ `<path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"/>` +
+ `<path d="m6.08 9.5-3.5 1.6a1 1 0 0 0 0 1.81l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83l-3.5-1.59"/>` +
+ `<path d="m6.08 14.5-3.5 1.6a1 1 0 0 0 0 1.81l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83l-3.5-1.59"/></svg>`
)
// icons maps each Lucide slug to its vendored markup. Slugs are the Lucide
@@ -88,6 +95,7 @@ var icons = map[string]template.HTML{
"arrow-down": iconArrowDown,
"flask-conical": iconFlask,
"triangle-alert": iconTriangleAlert,
+ "layers": iconLayers,
}
// icon renders one chrome icon by its Lucide slug: {{icon "chevron-down"}}.
diff --git a/internal/console/ladder.go b/internal/console/ladder.go
index cb05575a0..ee7a05749 100644
--- a/internal/console/ladder.go
+++ b/internal/console/ladder.go
@@ -9,16 +9,18 @@ package console
// reads — the app's channel pointers and its registry routes — so it can
// disagree with neither.
//
-// Two things it deliberately is not. It is not a second promote path: the
-// controls post to the app's one promote endpoint, which enforces the ladder
-// and the passkey gate for everybody, so a hand-written POST meets the same
-// doors as a button. And it is not an environment view — environments do not
-// exist yet (spec §4, deferred), which is why the one place this file guesses
-// at "production" is quarantined in prodWiredCanaryHosts below.
+// One thing it deliberately is not: a second promote path. The controls post
+// to the app's one promote endpoint, which enforces the ladder and the
+// passkey gate for everybody, so a hand-written POST meets the same doors as
+// a button.
+//
+// Every route also shows the environment it is wired to (spec §4a) beside
+// the channel it is listed under — "default" for a route with no binding —
+// and prodWiredCanaryHosts below answers "production" honestly from that:
+// no more inferring it from which host looks like the apex.
import (
"sort"
- "strings"
"time"
"github.com/carlosframework/platform/internal/release"
@@ -59,10 +61,14 @@ type LadderChannel struct {
// LadderRoute is one host under the channel it follows.
type LadderRoute struct {
Host string
+ // Environment is the named config bundle this route is wired to — ""
+ // (rendered "default" in ladder.html) means the app's default bundle
+ // alone. Independent of the channel it is listed under (spec §4a).
+ Environment string
// Canary is whether this host follows a canary channel at all.
Canary bool
- // ProdWired is the conspicuous case: a canary in front of everybody. See
- // prodWiredCanaryHosts for exactly how much this claim is worth.
+ // ProdWired is the conspicuous case: a canary bound to the same
+ // environment as the app's stable route. See prodWiredCanaryHosts.
ProdWired bool
}
@@ -104,9 +110,10 @@ func assembleLadder(channels []ChannelView, routes []RouteView) []LadderRung {
for _, rt := range routes {
rung, ok := release.Rung(rt.Channel)
byChannel[rt.Channel] = append(byChannel[rt.Channel], LadderRoute{
- Host: rt.Host,
- Canary: ok && rung == release.RungCanary,
- ProdWired: prod[rt.Host],
+ Host: rt.Host,
+ Environment: rt.Environment,
+ Canary: ok && rung == release.RungCanary,
+ ProdWired: prod[rt.Host],
})
}
entries := map[string][]LadderChannel{}
@@ -174,51 +181,39 @@ func stepUp(from, to string, chans []LadderChannel) *LadderStep {
return s
}
-// prodWiredCanaryHosts is THE INFERENCE, and it is alone in this file on
-// purpose.
-//
-// Spec §5 wants a canary wired to a production environment flagged
-// conspicuously, because an unreviewed build serving everybody must not be
-// discoverable only by reading the registry. But environments (spec §4) do
-// not exist yet — they are a subsystem with their own plan — so there is
-// nothing on a route that says "production" today.
+// prodWiredCanaryHosts is spec §4a's honest definition of "prod-wired",
+// replacing the apex-host guess this function used before named
+// environments existed (2026-08-02-pipelines-design.md §4a, last
+// paragraph): a canary/* route is prod-wired when it binds the SAME
+// ENVIRONMENT as the app's stable route — an unbound route binds "" (the
+// default bundle), so two unbound routes count as matching, exactly as the
+// spec says "or both bind default".
//
-// Until there is, production is inferred from the route being the app's
-// PRIMARY host: the apex it is reached at, picked as fewest labels, then
-// shortest, then alphabetical. A canary on `keymail.dev` is in front of
-// everybody; a canary on `rc.keymail.dev` is a side door somebody opened on
-// purpose. It is a heuristic and it is allowed to be wrong in both directions
-// — it flags, it never restricts.
+// An app can have more than one stable-channel route (e.g. two hosts both
+// following `stable`); this treats stable's set of bound environments as
+// "production", so a canary matching ANY of them is flagged. An app with no
+// stable route at all has no production environment to be wired in front
+// of, so nothing is flagged — that is the correct answer, not a gap.
//
-// WHEN ENVIRONMENTS LAND: delete the heuristic and answer from the route's
-// environment instead. This function is the whole of what has to change; its
-// callers only ever ask "is this host a prod-wired canary".
+// It flags, it never restricts: callers only ever ask "is this host a
+// prod-wired canary".
func prodWiredCanaryHosts(routes []RouteView) map[string]bool {
- primary := ""
- better := func(a, b string) bool {
- la, lb := strings.Count(a, "."), strings.Count(b, ".")
- if la != lb {
- return la < lb
- }
- if len(a) != len(b) {
- return len(a) < len(b)
- }
- return a < b
- }
+ stableEnvs := map[string]bool{}
for _, rt := range routes {
- if rt.Host == "" {
- continue
- }
- if primary == "" || better(rt.Host, primary) {
- primary = rt.Host
+ if rung, ok := release.Rung(rt.Channel); ok && rung == release.RungStable {
+ stableEnvs[rt.Environment] = true
}
}
out := map[string]bool{}
+ if len(stableEnvs) == 0 {
+ return out
+ }
for _, rt := range routes {
- if rt.Host != primary {
+ rung, ok := release.Rung(rt.Channel)
+ if !ok || rung != release.RungCanary {
continue
}
- if rung, ok := release.Rung(rt.Channel); ok && rung == release.RungCanary {
+ if stableEnvs[rt.Environment] {
out[rt.Host] = true
}
}
diff --git a/internal/console/ladder_test.go b/internal/console/ladder_test.go
index b9bac411a..165978ae7 100644
--- a/internal/console/ladder_test.go
+++ b/internal/console/ladder_test.go
@@ -209,33 +209,116 @@ func TestLadderRefusesASkip(t *testing.T) {
}
}
-// A canary is badged; a canary on the app's primary host is badged
-// unmissably, because that is an unreviewed build serving everybody
-// (spec §5). Both badges are chrome, so both are icons, not glyphs.
+// A canary is badged; a canary that binds the SAME ENVIRONMENT as the app's
+// stable route is badged unmissably, because that is an unreviewed build
+// wired to production glue (spec §4a, §5) — whichever host it happens to be
+// on. A canary bound to a genuinely different environment is the ordinary,
+// unremarkable case. Both badges are chrome, so both are icons, not glyphs.
func TestProdWiredCanaryIsBadgedDistinctly(t *testing.T) {
fx := newConsoleFixture(t)
- // The app's apex host follows a canary channel — and a side host follows
- // the same one, which is the ordinary, unremarkable case.
- fx.addRoute(t, registry.Route{Host: "kass.carlosapp.dev", Kind: "instance", Status: "ready",
- Version: "v2", App: "kass", Channel: "canary/x", Addr: "127.0.0.1:9001"})
+ // The fixture's own kass.carlosapp.dev is already on stable, unbound
+ // (Environment ""). A canary that is ALSO unbound binds the same
+ // environment as stable ("default" both times) and is prod-wired; one
+ // bound to a real, different environment is not — regardless of which
+ // host either is on.
fx.addRoute(t, registry.Route{Host: "rc.kass.carlosapp.dev", Kind: "instance", Status: "ready",
Version: "v2", App: "kass", Channel: "canary/x", Addr: "127.0.0.1:9002"})
+ fx.addRoute(t, registry.Route{Host: "side.kass.carlosapp.dev", Kind: "instance", Status: "ready",
+ Version: "v2", App: "kass", Channel: "canary/x", Environment: "sandbox", Addr: "127.0.0.1:9003"})
c := signInPaul(t, fx)
_, _, body := get(t, c, fx.u("/apps/kass"))
block := rungBlock(t, body, "canary")
- apex := routeLine(t, block, "kass.carlosapp.dev")
- if !strings.Contains(apex, "chip-prod") {
- t.Errorf("the app's primary host on a canary channel is not flagged:\n%s", apex)
+ sameEnv := routeLine(t, block, "rc.kass.carlosapp.dev")
+ if !strings.Contains(sameEnv, "chip-prod") {
+ t.Errorf("a canary binding the same (default) environment as stable is not flagged:\n%s", sameEnv)
}
- side := routeLine(t, block, "rc.kass.carlosapp.dev")
- if !strings.Contains(side, "chip-canary") {
- t.Errorf("a canary route carries no canary badge:\n%s", side)
+ diffEnv := routeLine(t, block, "side.kass.carlosapp.dev")
+ if !strings.Contains(diffEnv, "chip-canary") {
+ t.Errorf("a canary route carries no canary badge:\n%s", diffEnv)
}
- if strings.Contains(side, "chip-prod") {
- t.Errorf("a side canary is flagged as production-wired:\n%s", side)
+ if strings.Contains(diffEnv, "chip-prod") {
+ t.Errorf("a canary bound to a DIFFERENT environment than stable is flagged as production-wired:\n%s", diffEnv)
}
- if !strings.Contains(apex, "<svg") || !strings.Contains(side, "<svg") {
+ if !strings.Contains(sameEnv, "<svg") || !strings.Contains(diffEnv, "<svg") {
t.Error("the badges are text glyphs, not icons")
}
}
+
+// No stable route at all means no production environment to be wired in
+// front of — a canary is never flagged in that case, whatever it binds.
+func TestProdWiredCanaryNeedsAStableRouteToCompareAgainst(t *testing.T) {
+ fx := newConsoleFixture(t)
+ reg, err := registry.Open(fx.regPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Remove the fixture's two stable-channel kass routes so the app has no
+ // stable route left to compare a canary against.
+ if err := reg.Delete("kass.carlosapp.dev"); err != nil {
+ t.Fatal(err)
+ }
+ if err := reg.Delete("docs.kass.carlosapp.dev"); err != nil {
+ t.Fatal(err)
+ }
+ fx.addRoute(t, registry.Route{Host: "rc.kass.carlosapp.dev", Kind: "instance", Status: "ready",
+ Version: "v2", App: "kass", Channel: "canary/x", Addr: "127.0.0.1:9004"})
+
+ c := signInPaul(t, fx)
+ _, _, body := get(t, c, fx.u("/apps/kass"))
+ block := rungBlock(t, body, "canary")
+ line := routeLine(t, block, "rc.kass.carlosapp.dev")
+ if strings.Contains(line, "chip-prod") {
+ t.Errorf("a canary is flagged prod-wired on an app with no stable route:\n%s", line)
+ }
+}
+
+// Every route on the page shows the environment it is bound to, beside the
+// channel it is listed under — "default" for an unbound route, the bundle
+// name for a bound one (spec §4a, plan Task 5).
+func TestLadderShowsEachRoutesEnvironment(t *testing.T) {
+ fx := newConsoleFixture(t)
+ fx.addRoute(t, registry.Route{Host: "eu.kass.carlosapp.dev", Kind: "instance", Status: "ready",
+ Version: "v1", App: "kass", Channel: "stable", Environment: "eu-prod", Addr: "127.0.0.1:9011"})
+
+ c := signInPaul(t, fx)
+ _, _, body := get(t, c, fx.u("/apps/kass"))
+ block := rungBlock(t, body, "stable")
+
+ unbound := routeLine(t, block, "kass.carlosapp.dev")
+ if !strings.Contains(unbound, "chip-env") || !strings.Contains(unbound, "default") {
+ t.Errorf("an unbound route does not show \"default\" as its environment:\n%s", unbound)
+ }
+ bound := routeLine(t, block, "eu.kass.carlosapp.dev")
+ if !strings.Contains(bound, "chip-env") || !strings.Contains(bound, "eu-prod") {
+ t.Errorf("a bound route does not show its environment name:\n%s", bound)
+ }
+}
+
+// Visibility, not permission (spec §5): a member sees the same environment
+// and prod-wired chrome an owner does. Only the promote control is
+// owner-gated — see TestLadderPromoteControlIsOwnerOnly.
+func TestLadderEnvironmentAndProdWiredVisibleToNonOwner(t *testing.T) {
+ fx := newConsoleFixture(t)
+ fx.addRoute(t, registry.Route{Host: "rc.kass.carlosapp.dev", Kind: "instance", Status: "ready",
+ Version: "v2", App: "kass", Channel: "canary/x", Addr: "127.0.0.1:9002"})
+ if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ fx.setUser("sam@keymail.dev")
+ cm := newBrowser(t)
+ signUp(t, fx, cm, "code-sam-envs")
+ status, _, body := get(t, cm, fx.u("/apps/kass"))
+ if status != http.StatusOK {
+ t.Fatalf("member app page = %d, want 200", status)
+ }
+ canaryBlock := rungBlock(t, body, "canary")
+ line := routeLine(t, canaryBlock, "rc.kass.carlosapp.dev")
+ if !strings.Contains(line, "chip-prod") {
+ t.Errorf("a member does not see the prod-wired badge:\n%s", line)
+ }
+ stableBlock := rungBlock(t, body, "stable")
+ if !strings.Contains(stableBlock, "chip-env") {
+ t.Errorf("a member does not see route environment chrome:\n%s", stableBlock)
+ }
+}
diff --git a/internal/console/templates/ladder.html b/internal/console/templates/ladder.html
index b7ade3e01..2f8da0342 100644
--- a/internal/console/templates/ladder.html
+++ b/internal/console/templates/ladder.html
@@ -22,6 +22,7 @@ touching what any host is wired to.</p>
<ul class="routes">
{{range .Routes}}
<li><a href="https://{{.Host}}">{{.Host}}</a>
+ <span class="chip chip-env">{{icon "layers"}} {{if .Environment}}{{.Environment}}{{else}}default{{end}}</span>
{{if .ProdWired}}<span class="chip chip-prod">{{icon "triangle-alert"}} canary in front of everybody</span>
{{else if .Canary}}<span class="chip chip-canary">{{icon "flask-conical"}} canary</span>{{end}}
</li>
diff --git a/internal/console/templates/layout.html b/internal/console/templates/layout.html
index e79152697..8d4185ec2 100644
--- a/internal/console/templates/layout.html
+++ b/internal/console/templates/layout.html
@@ -146,6 +146,10 @@
every user is the one thing on this page a reader must not skim past. */
.chip-canary { border: 1px solid var(--accent); color: var(--accent); }
.chip-prod { background: var(--bad); color: var(--bg); }
+ /* A route's bound environment — a fact, not a warning, so it is bordered
+ like .chip-canary but in a neutral tone: it sits on every route, not
+ just the ones worth a second look. */
+ .chip-env { border: 1px solid var(--rule); color: var(--muted); }
.chip .icon { vertical-align: -0.1em; margin-right: 0.2em; }
.chip-up { background: var(--accent-soft); color: #2f7a4f; }
.chip-down { background: var(--accent-soft); color: var(--bad); }
diff --git a/internal/console/view.go b/internal/console/view.go
index 76fb54068..77e916929 100644
--- a/internal/console/view.go
+++ b/internal/console/view.go
@@ -107,6 +107,11 @@ type CanaryView struct {
type RouteView struct {
Host, Kind, Status, Version, Channel string
Hibernate bool
+ // Environment is the named config bundle this route is wired to — ""
+ // means the app's default bundle alone (registry.Route's own
+ // convention; see internal/appconfig.LoadMerged). Rendered as "default"
+ // rather than left blank, in ladder.html.
+ Environment string
}
// ChannelView is one row of the channels table.
@@ -199,6 +204,7 @@ func Assemble(regPath string, s release.Store, pub ed25519.PublicKey, acct strin
byApp[rt.App] = append(byApp[rt.App], RouteView{
Host: rt.Host, Kind: rt.Kind, Status: rt.Status,
Version: rt.Version, Channel: ch, Hibernate: rt.Hibernate,
+ Environment: rt.Environment,
})
domainsByApp[rt.App] = append(domainsByApp[rt.App], domainView(rt, edgeIPs))
}