| 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 |