carlos / platform Public

console: production flag, fast-track and bake state plumbed into the app view

Adds ProductionView (AppView.Production) and ChannelView/LadderChannel
FastTrack, assembled from release.GetProduction and the pointer's own
fast_track waiver: an app with no production flag (or flag off) shows no
bake estimate at all, since nothing bakes without it; a waived channel's
BakedUntil is zeroed the same way, since fast_track means nothing to wait
out. The badge reads the pointer's own FastTrack field, never the
unverified audit trail. assembleProduction also derives the turning-off
ratchet estimate (flag flipped off, still within the stable hold, still
holding channels) and the prod-wiring nudge (flag off but routes look
production-wired) — display only, per spec §7.

TestAssemble and TestConsoleE2E now seed the production flag before
asserting BakedUntil text, since both exercise a fixture app that
previously baked by default and now needs the flag to keep doing so.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VioXzogWf1Xi4Rswz2B6EK
Paul Campbell aa9e3acd92ec04b9bd6cb78da68fa580720972f4 parent 829c387
4 files changed, +103 −1
  • internal/console/ladder.go +5 −0
  • internal/console/server_test.go +8 −0
  • internal/console/view.go +82 −0
  • internal/console/view_test.go +8 −1
diff --git a/internal/console/ladder.go b/internal/console/ladder.go
index ceadd0ab7..a96c45bb9 100644
--- a/internal/console/ladder.go
+++ b/internal/console/ladder.go
@@ -57,6 +57,10 @@ type LadderChannel struct {
Actor string
// Hotfix records that this promotion bypassed the ladder.
Hotfix bool
+ // FastTrack and BakedUntil carry the channel's waiver and its ~hold
+ // estimate into the rung row, where the fast-track control lives.
+ FastTrack bool
+ BakedUntil time.Time
// Err carries a pointer that would not read or verify, and a channel that
// hosts follow but that nothing has ever been published to. Both are one
// line of bad news on one rung, never a failed page.
@@ -146,6 +150,7 @@ func assembleLadder(channels []ChannelView, routes []RouteView, now time.Time) [
entries[rung] = append(entries[rung], LadderChannel{
Channel: cv.Name, Version: cv.Version, PromotedAt: cv.PromotedAt,
Actor: cv.Actor, Hotfix: cv.Hotfix, Err: cv.Err,
+ FastTrack: cv.FastTrack, BakedUntil: cv.BakedUntil,
Routes: byChannel[cv.Name],
})
delete(byChannel, cv.Name)
diff --git a/internal/console/server_test.go b/internal/console/server_test.go
index 7eda6b806..3f078f8ae 100644
--- a/internal/console/server_test.go
+++ b/internal/console/server_test.go
@@ -234,6 +234,14 @@ func TestConsoleE2E(t *testing.T) {
t.Fatal("welcome flash survived a second page view")
}
+ // Bake estimates only render for an app the production flag marks on
+ // (2026-08-04 console bake controls spec §7) — seed it so the "bakes
+ // until ~" assertion below still exercises the baking path rather than
+ // the new "nothing bakes without the flag" zeroing.
+ if err := release.SetProduction(context.Background(), fx.rel, fx.priv, release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
+
// Detail page: routes and channels, with the bake approximation labeled.
status, _, body := get(t, c, fx.u("/apps/kass"))
if status != 200 {
diff --git a/internal/console/view.go b/internal/console/view.go
index 0e9c7054c..02c56fa03 100644
--- a/internal/console/view.go
+++ b/internal/console/view.go
@@ -31,6 +31,9 @@ type AppView struct {
// channels and routes as the fields above, arranged as the pipeline they
// actually are rather than as two alphabetical lists.
Ladder []LadderRung
+ // Production is the app's production-flag state (release.ProductionFlag)
+ // as the console shows it — see ProductionView.
+ Production ProductionView
// RouteAccount is the account the app's ROUTES say it belongs to, when
// that disagrees with the account whose page this is; "" when they agree
// or there are no routes. It is the half-migrated state — see
@@ -142,11 +145,35 @@ type ChannelView struct {
// missing information, not an error, and the page says so.
Actor string
Hotfix bool
+ // FastTrack is the pointer's bake waiver (release.ChannelPointer) — the
+ // badge is permanent, like the hotfix badge.
+ FastTrack bool
// Err degrades a missing or invalid channel pointer to one row's error
// text instead of a failed page.
Err string
}
+// ProductionView is the app's production-flag state as the console shows it
+// (2026-08-04 console bake controls spec §7).
+type ProductionView struct {
+ // On is the flag as the bucket has it; the default (no object) is off.
+ On bool
+ // TurningOff marks an off-flag younger than the stable window while the
+ // app still has held channels: hosts may still be ratcheting.
+ // EffectiveAt is the ~estimate shown (set_at + the stable hold) — each
+ // box's own clock and jitter govern, exactly as BakedUntil is only ever
+ // an approximation.
+ TurningOff bool
+ EffectiveAt time.Time
+ SetAt time.Time
+ // Nudge: the app looks production-wired while bake is off. Detection
+ // recommends; it never flips the flag.
+ Nudge bool
+ // Err is a flag object that exists but will not verify — shown, and
+ // boxes treat it as production (they keep baking).
+ Err string
+}
+
// VersionView is one shipped release on the app page.
type VersionView struct {
Version, Kind, BuiltAt string
@@ -244,6 +271,19 @@ func Assemble(regPath string, s release.Store, pub ed25519.PublicKey, acct strin
if ra := routeAccount[app]; ra != "" && ra != acct {
mismatch = ra
}
+ pv := assembleProduction(ctx, s, pub, acct, app, channels, byApp[app], now)
+ if pv.Err == "" && !pv.On {
+ // No production flag (or off): nothing bakes, so no estimate. An
+ // Err keeps the estimate — boxes fail toward baking.
+ for i := range channels {
+ channels[i].BakedUntil = time.Time{}
+ }
+ }
+ for i := range channels {
+ if channels[i].FastTrack {
+ channels[i].BakedUntil = time.Time{} // waived — nothing to wait out
+ }
+ }
out = append(out, AppView{
App: app,
Routes: byApp[app], // List orders by host already
@@ -251,6 +291,7 @@ func Assemble(regPath string, s release.Store, pub ed25519.PublicKey, acct strin
Channels: channels,
Canaries: assembleCanaries(channels, byApp[app]),
Ladder: assembleLadder(channels, byApp[app], now),
+ Production: pv,
RouteAccount: mismatch,
})
}
@@ -312,6 +353,7 @@ func assembleChannels(ctx context.Context, s release.Store, pub ed25519.PublicKe
cv := ChannelView{
Name: name, Version: p.Version, PromotedAt: p.PromotedAt,
HasHistory: len(p.History) > 0,
+ FastTrack: p.FastTrack,
}
// The trail is newest first, but a rollback carries it forward
// without appending: the entry that explains the CURRENT version is
@@ -334,6 +376,46 @@ func assembleChannels(ctx context.Context, s release.Store, pub ed25519.PublicKe
return out
}
+// assembleProduction reads the app's flag and derives the console's view of
+// it: on/off, the turning-off ratchet estimate, and the prod-wiring nudge.
+func assembleProduction(ctx context.Context, s release.Store, pub ed25519.PublicKey, acct, app string, channels []ChannelView, routes []RouteView, now time.Time) ProductionView {
+ pv := ProductionView{}
+ flag, err := release.GetProduction(ctx, s, pub, acct, app)
+ switch {
+ case err != nil:
+ pv.Err = err.Error()
+ pv.On = true // what the boxes will do: fail toward baking
+ return pv
+ case flag != nil:
+ pv.On = flag.Production
+ pv.SetAt = flag.SetAt
+ }
+ held := false
+ for _, cv := range channels {
+ if cv.Err == "" && release.HoldFor(release.DefaultHolds, cv.Name) > 0 {
+ held = true
+ break
+ }
+ }
+ if flag != nil && !flag.Production && held {
+ hold := release.HoldFor(release.DefaultHolds, release.RungStable)
+ if until := flag.SetAt.Add(hold); now.Before(until) {
+ pv.TurningOff, pv.EffectiveAt = true, until
+ }
+ }
+ if !pv.On {
+ stableRoute := false
+ for _, rt := range routes {
+ if rung, ok := release.Rung(rt.Channel); ok && rung == release.RungStable {
+ stableRoute = true
+ break
+ }
+ }
+ pv.Nudge = stableRoute || len(prodWiredCanaryHosts(routes)) > 0
+ }
+ return pv
+}
+
// FleetView reads status/fleet.json off the console's release.Store handle
// and decodes it into status.Fleet — the exact shape internal/status's
// public page renders, so the operator view and the public page never
diff --git a/internal/console/view_test.go b/internal/console/view_test.go
index 4fea7b3b4..c93b651b2 100644
--- a/internal/console/view_test.go
+++ b/internal/console/view_test.go
@@ -107,7 +107,14 @@ func findChannel(t *testing.T, chans []ChannelView, name string) ChannelView {
}
func TestAssemble(t *testing.T) {
- regPath, store, _, pub := buildFixtures(t)
+ regPath, store, priv, pub := buildFixtures(t)
+ // Bake estimates only render for an app the flag marks as production
+ // (2026-08-04 console bake controls spec §7) — seed it so the
+ // BakedUntil assertions below still exercise the "still baking" path
+ // rather than the new "nothing bakes without the flag" zeroing.
+ if err := release.SetProduction(context.Background(), store, priv, release.OpsAccount, "kass", true, "test"); err != nil {
+ t.Fatal(err)
+ }
views, err := Assemble(regPath, store, pub, release.OpsAccount, []string{"kass"}, nil)
if err != nil {
t.Fatal(err)