console: rename the supply-chain bake to 'safety delay' in every user-facing string
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
12 files changed,
+111
−76
internal/console/bakecontrols_test.go+11 −11internal/console/confirm.go+11 −11internal/console/confirm_test.go+2 −2internal/console/ladder_test.go+14 −13internal/console/server.go+17 −13internal/console/server_test.go+2 −2internal/console/templates/app_releases.html+7 −9internal/console/templates/app_settings.html+2 −2internal/console/templates/ladder.html+11 −11internal/console/templates/trash.html+2 −2internal/console/until_test.go+23 −0internal/console/view.go+9 −0
diff --git a/internal/console/bakecontrols_test.go b/internal/console/bakecontrols_test.go| index 7eaf8d748..cab5d35d7 100644 |
| --- a/internal/console/bakecontrols_test.go |
| +++ b/internal/console/bakecontrols_test.go |
| @@ -187,7 +187,7 @@ func TestFastTrackIsOwnerPasskeyGatedAndLands(t *testing.T) { |
| if len(p.Audit) == 0 || !p.Audit[0].FastTrack || p.Audit[0].Actor != "paul@keymail.dev" { |
| t.Fatalf("audit[0] = %+v, want the waiver recorded with its actor", p.Audit) |
| } |
| - auditedBy(t, fx, "kass", "fast-track", "paul@keymail.dev", "waived the bake window for v1 on stable") |
| + auditedBy(t, fx, "kass", "fast-track", "paul@keymail.dev", "skipped the safety delay for v1 on stable") |
| } |
| // Fast-tracking a channel that never bakes is refused. It looks inert — |
| @@ -209,7 +209,7 @@ func TestFastTrackRefusesAChannelWithNoBakeWindow(t *testing.T) { |
| t.Fatalf("fast-track of a bake-free channel = %d, want the flash redirect", status) |
| } |
| if _, _, body := get(t, c, fx.u("/apps/kass")); !strings.Contains(body, |
| - "Channel canary/x has no bake window — there is nothing to fast-track.") { |
| + "Channel canary/x has no safety delay running — there is nothing to skip.") { |
| t.Fatalf("no refusal flash:\n%s", body) |
| } |
| after := fx.channelVersion(t, "kass", "canary/x") |
| @@ -276,9 +276,9 @@ func TestPromoteEnableBakeFlipsTheFlag(t *testing.T) { |
| t.Fatalf("flag = %+v after enable_bake promote, want production on", f) |
| } |
| // The flash tells the truth about both halves of what just happened: the |
| - // flag went on in this action, so the bake note is now accurate. |
| - if _, _, body := get(t, c, fx.u("/apps/kass")); !strings.Contains(body, "Supply-chain bake enabled.") || |
| - !strings.Contains(body, "Adopts after the bake window") { |
| + // flag went on in this action, so the safety-delay note is now accurate. |
| + if _, _, body := get(t, c, fx.u("/apps/kass")); !strings.Contains(body, "Safety delay enabled.") || |
| + !strings.Contains(body, "It goes live on each machine after the safety delay") { |
| t.Fatalf("enable_bake flash missing its two halves:\n%s", body) |
| } |
| } |
| @@ -303,11 +303,11 @@ func TestPromoteEnableBakeSaysWhenItCouldNotRead(t *testing.T) { |
| t.Fatalf("stable = %s, want v3 — the promote itself must not depend on the flag", p.Version) |
| } |
| _, _, body := get(t, c, fx.u("/apps/kass")) |
| - if !strings.Contains(body, "could not be read, so the bake was not enabled") { |
| + if !strings.Contains(body, "could not be read, so the safety delay was not enabled") { |
| t.Fatalf("the flash is silent about the unhonored checkbox:\n%s", body) |
| } |
| - if strings.Contains(body, "Supply-chain bake enabled.") { |
| - t.Fatal("the flash claims the bake was enabled over an unreadable flag") |
| + if strings.Contains(body, "Safety delay enabled.") { |
| + t.Fatal("the flash claims the safety delay was enabled over an unreadable flag") |
| } |
| } |
| @@ -333,8 +333,8 @@ func TestPromoteBakeNoteFollowsTheFlag(t *testing.T) { |
| if p := fx.channelVersion(t, "kass", "beta"); p.Version != "v3" { |
| t.Fatalf("beta = %+v, want v3", p) |
| } |
| - if _, _, body := get(t, c, fx.u("/apps/kass")); strings.Contains(body, "Adopts after the bake window") { |
| - t.Fatalf("a flagless app was promised a bake window:\n%s", body) |
| + if _, _, body := get(t, c, fx.u("/apps/kass")); strings.Contains(body, "It goes live on each machine after the safety delay") { |
| + t.Fatalf("a flagless app was promised a safety delay:\n%s", body) |
| } |
| // Flag on: the same rung now says so. |
| @@ -348,7 +348,7 @@ func TestPromoteBakeNoteFollowsTheFlag(t *testing.T) { |
| url.Values{"csrf": {csrf}, "version": {"v4"}, "channel": {"beta"}, "confirmed": {"1"}}); status != http.StatusSeeOther { |
| t.Fatalf("beta promote of v4 = %d:\n%s", status, body) |
| } |
| - if _, _, body := get(t, c, fx.u("/apps/kass")); !strings.Contains(body, "Adopts after the bake window") { |
| + if _, _, body := get(t, c, fx.u("/apps/kass")); !strings.Contains(body, "It goes live on each machine after the safety delay") { |
| t.Fatalf("a production app's held promotion did not mention the window:\n%s", body) |
| } |
| } |
diff --git a/internal/console/confirm.go b/internal/console/confirm.go| index c088c7b76..9aa78f760 100644 |
| --- a/internal/console/confirm.go |
| +++ b/internal/console/confirm.go |
| @@ -134,12 +134,12 @@ func (s *Server) confirmPromote(w http.ResponseWriter, r *http.Request, addr, ra |
| flag, ferr := release.GetProduction(ctx, s.cfg.Releases, s.cfg.ReleasePub, acct.Sqid, app) |
| baking := ferr != nil || (flag != nil && flag.Production) |
| if hold := release.HoldFor(release.DefaultHolds, channel); hold > 0 && baking { |
| - cv.Lines = append(cv.Lines, "This app bakes: each host holds "+version+" for "+holdText(hold)+" on "+channel+" before adopting it.") |
| + cv.Lines = append(cv.Lines, "This app has the safety delay on: each machine waits "+holdText(hold)+" on "+channel+" before running "+version+".") |
| } |
| if stable && ferr == nil && !baking { |
| cv.CheckboxName = "enable_bake" |
| cv.CheckboxChecked = true |
| - cv.CheckboxLabel = "This app is in production — enable the supply-chain bake (recommended)" |
| + cv.CheckboxLabel = "This app is live for real users — keep new versions on the safety delay (recommended)" |
| } |
| if stable { |
| cv.Note = "You'll confirm it's really you with your passkey." |
| @@ -178,27 +178,27 @@ func (s *Server) confirmFastTrack(w http.ResponseWriter, r *http.Request, addr, |
| ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) |
| defer cancel() |
| cv := &ConfirmView{ |
| - Title: "Fast-track " + channel + "?", |
| - Warn: "The bake window is the supply-chain guard — waiving it ships a build that hasn't finished proving itself.", |
| - Note: "You'll confirm it's really you with your passkey. The pointer wears the fast-track badge permanently.", |
| + Title: "Skip the safety delay on " + channel + "?", |
| + Warn: "The safety delay is the window where a new build proves itself — skipping it puts an unproven build in front of users.", |
| + Note: "You'll confirm it's really you with your passkey. The version keeps a permanent 'delay skipped' badge.", |
| Action: "/a/" + url.PathEscape(acct.Sqid) + "/apps/" + url.PathEscape(app) + "/fasttrack", |
| Fields: [][2]string{ |
| {"csrf", raw}, {"channel", channel}, |
| }, |
| - Button: "Fast-track", |
| + Button: "Skip the delay", |
| Danger: true, |
| } |
| if cur, err := release.Channel(ctx, s.cfg.Releases, s.cfg.ReleasePub, acct.Sqid, app, channel); err == nil { |
| - cv.Title = "Fast-track " + cur.Version + " on " + channel + "?" |
| + cv.Title = "Skip the safety delay for " + cur.Version + " on " + channel + "?" |
| cv.Lines = append(cv.Lines, |
| - channel+" holds "+cur.Version+", promoted "+ago(cur.PromotedAt)+", still in its bake window.", |
| - "Fast-tracking waives the rest of the window: hosts adopt "+cur.Version+" on their next pass.") |
| - cv.Button = "Fast-track " + cur.Version |
| + channel+" holds "+cur.Version+", promoted "+ago(cur.PromotedAt)+", still in its safety delay.", |
| + "Skipping waives the rest of it: machines run "+cur.Version+" within a couple of minutes.") |
| + cv.Button = "Skip the delay for " + cur.Version |
| // Pin the version the card names — the waiver must not land on a |
| // build promoted after this was rendered. |
| cv.Fields = append(cv.Fields, [2]string{"version", cur.Version}) |
| } else { |
| - cv.Lines = append(cv.Lines, "Waives the rest of "+channel+"'s bake window: hosts adopt its current version on their next pass.") |
| + cv.Lines = append(cv.Lines, "Waives the rest of "+channel+"'s safety delay: machines run its current version within a couple of minutes.") |
| } |
| s.renderConfirm(w, r, addr, raw, acct, role, app, cv) |
| } |
diff --git a/internal/console/confirm_test.go b/internal/console/confirm_test.go| index e14ec9e6f..4a53ab2e8 100644 |
| --- a/internal/console/confirm_test.go |
| +++ b/internal/console/confirm_test.go |
| @@ -223,8 +223,8 @@ func TestConfirmFastTrackCardComesBeforeStepUp(t *testing.T) { |
| if strings.HasPrefix(loc, fx.fk.srv.URL) { |
| t.Fatalf("the owner met the passkey before the card (-> %q) — confirm asks \"do you mean it?\" first", loc) |
| } |
| - card := confirmCard(t, status, body, "Fast-track v1 on stable?", csrf) |
| - if !strings.Contains(card, "The bake window is the supply-chain guard") { |
| + card := confirmCard(t, status, body, "Skip the safety delay for v1 on stable?", csrf) |
| + if !strings.Contains(card, "The safety delay is the window where a new build proves itself") { |
| t.Fatalf("the fast-track card carries no warning:\n%s", card) |
| } |
| if p := fx.channelVersion(t, "kass", "stable"); p.FastTrack { |
diff --git a/internal/console/ladder_test.go b/internal/console/ladder_test.go| index 818be79a8..3d4ccc712 100644 |
| --- a/internal/console/ladder_test.go |
| +++ b/internal/console/ladder_test.go |
| @@ -635,8 +635,8 @@ func TestLadderShowsBakeStateAndControls(t *testing.T) { |
| csrf := sessionToken(t, fx, c) |
| _, _, body := get(t, c, fx.u("/apps/kass/releases")) |
| - if !strings.Contains(body, "Supply-chain bake") { |
| - t.Fatalf("no bake status line:\n%s", body) |
| + if !strings.Contains(body, "Safety delay") { |
| + t.Fatalf("no safety-delay status line:\n%s", body) |
| } |
| if !strings.Contains(body, `action="/a/bab/apps/kass/production"`) { |
| t.Fatal("owner is offered no production toggle") |
| @@ -678,8 +678,8 @@ func TestLadderShowsBakeStateAndControls(t *testing.T) { |
| } |
| _, _, body = get(t, c, fx.u("/apps/kass/releases")) |
| b := rungBlock(t, body, "beta") |
| - if !strings.Contains(b, "fast-tracked") { |
| - t.Errorf("no permanent fast-tracked badge:\n%s", b) |
| + if !strings.Contains(b, "delay skipped") { |
| + t.Errorf("no permanent delay-skipped badge:\n%s", b) |
| } |
| if strings.Contains(b, `action="/a/bab/apps/kass/fasttrack"`) { |
| t.Error("a waived head still offers the fast-track control") |
| @@ -709,8 +709,8 @@ func TestLadderBakeControlsMemberView(t *testing.T) { |
| signUp(t, fx, cm, "code-sam-baketpl") |
| _, _, mbody := get(t, cm, fx.u("/apps/kass/releases")) |
| - if !strings.Contains(mbody, "Supply-chain bake") { |
| - t.Fatal("member sees no bake status") |
| + if !strings.Contains(mbody, "Safety delay") { |
| + t.Fatal("member sees no safety-delay status") |
| } |
| if !strings.Contains(mbody, `name="production" value="on"`) { |
| t.Error("member is not offered the turn-on control") |
| @@ -760,14 +760,14 @@ func TestLadderUnreadableFlagOffersRepair(t *testing.T) { |
| t.Fatal(err) |
| } |
| _, _, body := get(t, c, fx.u("/apps/kass/releases")) |
| - if !strings.Contains(body, "Supply-chain bake: flag unreadable") { |
| + if !strings.Contains(body, "Safety delay: this app's setting can't be read right now, so the delay stays on.") { |
| t.Fatalf("no error line for an unverifiable flag:\n%s", body) |
| } |
| if !strings.Contains(body, `action="/a/bab/apps/kass/production"`) || |
| !strings.Contains(body, `name="production" value="on"`) { |
| t.Fatalf("an unreadable flag offers no repair form — the app bakes forever:\n%s", body) |
| } |
| - if !strings.Contains(body, "Re-declare production") { |
| + if !strings.Contains(body, "Set it again") { |
| t.Fatalf("the repair control is not named as a re-declaration:\n%s", body) |
| } |
| // The safe direction only: turning OFF over an unverifiable flag is still |
| @@ -812,9 +812,10 @@ func TestLadderTurningOffOffersTurnBackOn(t *testing.T) { |
| t.Fatalf("the turning-off state offers no way back on:\n%s", body) |
| } |
| // Not the default-off line: this app has an off-flag mid-ratchet, and |
| - // telling the operator hosts adopt immediately would be false for ~72h. |
| - if strings.Contains(body, "hosts adopt releases immediately") { |
| - t.Error("mid-ratchet, the page claims hosts already adopt immediately") |
| + // telling the operator new versions go live immediately would be false |
| + // for ~72h. |
| + if strings.Contains(body, "new versions go live as soon as they're promoted") { |
| + t.Error("mid-ratchet, the page claims new versions already go live immediately") |
| } |
| } |
| @@ -826,14 +827,14 @@ func TestLadderNudgesProdWiredBakeOff(t *testing.T) { |
| // The fixture's kass.carlosapp.dev is on stable and the flag is off (the |
| // default): that IS the nudge condition. |
| _, _, body := get(t, c, fx.u("/apps/kass/releases")) |
| - if !strings.Contains(body, "adopts releases immediately") { |
| + if !strings.Contains(body, "new versions go live immediately") { |
| t.Fatalf("no prod-wiring nudge:\n%s", body) |
| } |
| if err := release.SetProduction(context.Background(), fx.rel, fx.priv, |
| release.OpsAccount, "kass", true, "test"); err != nil { |
| t.Fatal(err) |
| } |
| - if _, _, body := get(t, c, fx.u("/apps/kass/releases")); strings.Contains(body, "adopts releases immediately") { |
| + if _, _, body := get(t, c, fx.u("/apps/kass/releases")); strings.Contains(body, "new versions go live immediately") { |
| t.Fatal("the nudge survived the flag turning on") |
| } |
| } |
diff --git a/internal/console/server.go b/internal/console/server.go| index 44a3c6734..963300ee5 100644 |
| --- a/internal/console/server.go |
| +++ b/internal/console/server.go |
| @@ -33,6 +33,10 @@ var tmplFS embed.FS |
| var templates = template.Must(template.New("").Funcs(template.FuncMap{ |
| "fmtTime": func(t time.Time) string { return t.UTC().Format("2006-01-02 15:04 UTC") }, |
| + // until renders a future horizon the way the cards speak: "~4h", never a |
| + // timestamp — the safety-delay strip's "goes live ~4h" and Task 6's |
| + // summary strip both read it this way. |
| + "until": untilText, |
| // ago renders a past instant the way a person scanning a dashboard reads |
| // one — "2h4m ago" — at humanAge's deliberately coarse resolution. The |
| // absolute stamp stays available where precision matters (title attrs, |
| @@ -1906,16 +1910,16 @@ func (s *Server) handlePromote(w http.ResponseWriter, r *http.Request) { |
| // re-declare control), not a promote's side effect. baking is true |
| // here only because an unreadable flag fails TOWARD baking — which |
| // settles nothing about what this app has declared. |
| - msg += " The production flag could not be read, so the bake was not enabled — use the toggle on the app page." |
| + msg += " The production flag could not be read, so the safety delay was not enabled — use the toggle on the app's Versions tab." |
| case baking: |
| // Already on: a stray checkbox on a repeat is a no-op, as before. |
| default: |
| if err := release.SetProduction(ctx, s.cfg.Releases, s.cfg.Key, acct.Sqid, app, true, addr); err != nil { |
| log.Printf("console: enable_bake %s: %v", app, err) |
| - msg += " Enabling the supply-chain bake failed — use the toggle on the app page." |
| + msg += " Turning the safety delay on failed — use the toggle on the app's Versions tab." |
| } else { |
| - s.audit(acct, app, addr, "production", "enabled the supply-chain bake (production: on, at stable promote)") |
| - msg += " Supply-chain bake enabled." |
| + s.audit(acct, app, addr, "production", "enabled the safety delay (production: on, at stable promote)") |
| + msg += " Safety delay enabled." |
| baking = true |
| } |
| } |
| @@ -1926,7 +1930,7 @@ func (s *Server) handlePromote(w http.ResponseWriter, r *http.Request) { |
| // checkbox was ticked, the production flag — in a single bump. |
| s.bumpSerial(ctx) |
| if baking && release.HoldFor(release.DefaultHolds, channel) > 0 { |
| - msg += " Adopts after the bake window on each host — rollback never bakes." |
| + msg += " It goes live on each machine after the safety delay — rollbacks skip it." |
| } |
| back("ok", msg) |
| } |
| @@ -2068,7 +2072,7 @@ func (s *Server) handleProduction(w http.ResponseWriter, r *http.Request) { |
| defer cancel() |
| if err := release.SetProduction(ctx, s.cfg.Releases, s.cfg.Key, acct.Sqid, app, on, addr); err != nil { |
| log.Printf("console: production %s -> %v: %v", app, on, err) |
| - back("err", "Could not update the production flag: "+err.Error()) |
| + back("err", "Could not change the safety delay: "+err.Error()) |
| return |
| } |
| // The flag is read on every box's adopt path (it decides whether a channel |
| @@ -2076,11 +2080,11 @@ func (s *Server) handleProduction(w http.ResponseWriter, r *http.Request) { |
| // releases held versions, and that is the direction people wait on. |
| s.bumpSerial(ctx) |
| if on { |
| - s.audit(acct, app, addr, "production", "enabled the supply-chain bake (production: on)") |
| - back("ok", "Supply-chain bake enabled — beta and stable now hold before hosts adopt.") |
| + s.audit(acct, app, addr, "production", "enabled the safety delay (production: on)") |
| + back("ok", "Safety delay enabled — new versions now wait on beta and stable before going live.") |
| } else { |
| - s.audit(acct, app, addr, "production", "disabled the supply-chain bake (production: off)") |
| - back("ok", "Supply-chain bake turning off — each host honors it after the stable window (~72h) on its own clock.") |
| + s.audit(acct, app, addr, "production", "disabled the safety delay (production: off)") |
| + back("ok", "Safety delay turning off — takes effect on each machine after ~72h.") |
| } |
| } |
| @@ -2138,7 +2142,7 @@ func (s *Server) handleFastTrack(w http.ResponseWriter, r *http.Request) { |
| // → owner 404 → requireFreshAuth LAST) then leaves only this spot. The |
| // cost is a passkey tap spent on a refusal; the alternative is an oracle. |
| if release.HoldFor(release.DefaultHolds, channel) == 0 { |
| - back("err", "Channel "+channel+" has no bake window — there is nothing to fast-track.") |
| + back("err", "Channel "+channel+" has no safety delay running — there is nothing to skip.") |
| return |
| } |
| ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) |
| @@ -2164,11 +2168,11 @@ func (s *Server) handleFastTrack(w http.ResponseWriter, r *http.Request) { |
| back("err", "Fast-track failed: "+err.Error()) |
| return |
| } |
| - s.audit(acct, app, addr, "fast-track", "waived the bake window for "+ver+" on "+channel) |
| + s.audit(acct, app, addr, "fast-track", "skipped the safety delay for "+ver+" on "+channel) |
| // A waiver re-signs the pointer to say "adopt this now" — the one write |
| // whose entire point is that boxes act on it immediately. |
| s.bumpSerial(ctx) |
| - back("ok", "Fast-tracked "+ver+" on "+channel+" — hosts adopt on their next pass.") |
| + back("ok", "Skipped the delay for "+ver+" on "+channel+" — machines pick it up within a couple of minutes.") |
| } |
| // ---- canaries --------------------------------------------------------------- |
diff --git a/internal/console/server_test.go b/internal/console/server_test.go| index da52c1982..098b950ae 100644 |
| --- a/internal/console/server_test.go |
| +++ b/internal/console/server_test.go |
| @@ -259,12 +259,12 @@ func TestConsoleE2E(t *testing.T) { |
| } |
| } |
| - // Releases tab: the Channels table, with the bake approximation labeled. |
| + // Releases tab: the Channels table, with the go-live estimate labeled. |
| status, _, rbody := get(t, c, fx.u("/apps/kass/releases")) |
| if status != 200 { |
| t.Fatalf("/apps/kass/releases = %d", status) |
| } |
| - for _, want := range []string{"canary/x", "v2", "bakes until ~", "<td>live</td>", "approximation"} { |
| + for _, want := range []string{"canary/x", "v2", "in ~", "<td>live now</td>", "estimates"} { |
| if !strings.Contains(rbody, want) { |
| t.Fatalf("/apps/kass/releases missing %q in:\n%s", want, rbody) |
| } |
diff --git a/internal/console/templates/app_releases.html b/internal/console/templates/app_releases.html| index 1d4d9e65f..d216bc232 100644 |
| --- a/internal/console/templates/app_releases.html |
| +++ b/internal/console/templates/app_releases.html |
| @@ -18,7 +18,7 @@ |
| {{if .Detail.Channels}} |
| <div class="tablewrap"> |
| <table> |
| - <tr><th>Channel</th><th>Version</th><th>Promoted</th><th>Bake</th><th></th></tr> |
| + <tr><th>Channel</th><th>Version</th><th>Promoted</th><th>Goes live</th><th></th></tr> |
| {{range .Detail.Channels}} |
| <tr> |
| <td>{{.Name}}</td> |
| @@ -27,7 +27,7 @@ |
| {{else}} |
| <td><code>{{.Version}}</code>{{if .Tag}} <span class="chip chip-env">{{.Tag}}</span>{{end}}</td> |
| <td><span title="{{fmtTime .PromotedAt}}">{{ago .PromotedAt}}</span></td> |
| - <td>{{if .BakedUntil.IsZero}}live{{else}}bakes until ~{{fmtTime .BakedUntil}}{{end}}{{if .FastTrack}} · fast-tracked{{end}}</td> |
| + <td>{{if .BakedUntil.IsZero}}live now{{else}}in {{until .BakedUntil}}{{end}}{{if .FastTrack}} · delay skipped{{end}}</td> |
| {{end}} |
| <td>{{if and $.CanPromote .HasHistory (not .Err)}} |
| <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/rollback"> |
| @@ -41,9 +41,7 @@ |
| {{end}} |
| </table> |
| </div> |
| -<p class="small">Bake times are an approximation from each channel's default |
| -hold; the authoritative clock is each box's own first-seen record. Rollback |
| -never bakes.</p> |
| +<p class="small">Go-live times are estimates — each machine keeps its own clock. Rollbacks skip the delay.</p> |
| {{else}}<p class="small">No channels yet — ship a release, then promote it below to create the first one.</p>{{end}} |
| <h2>Canaries</h2> |
| @@ -74,7 +72,7 @@ never bakes.</p> |
| {{end}} |
| </table> |
| </div> |
| -{{else}}<p class="small">No canaries — a canary is a fresh channel serving one chosen release immediately, with no bake window.</p>{{end}} |
| +{{else}}<p class="small">No test versions — a test version is a private copy serving one chosen build immediately, with no safety delay.</p>{{end}} |
| {{if and .CanPromote .Versions}} |
| <form class="signin" method="post" action="{{.Base}}/apps/{{.Detail.App}}/canary"> |
| <input type="hidden" name="csrf" value="{{.CSRF}}"> |
| @@ -85,12 +83,12 @@ never bakes.</p> |
| </select> |
| <button type="submit" class="btn-sm">Create canary</button> |
| </form> |
| -<p class="small">Creates channel <code>canary/<name></code> pointing at the chosen release — serving immediately, no bake. It gets its URL from a host that follows the channel:</p> |
| +<p class="small">Creates <code>canary/<name></code> — a test version serving the chosen build immediately, no delay. It gets its own URL from a machine that follows it:</p> |
| {{if .AttachEnable}}<pre><code>{{.AttachEnable}} |
| {{.AttachCmd}}</code></pre> |
| -<p class="small">This app has no instances manifest yet, so the first line enables one under this account's platform subdomain. The host comes live on its box's next provision pass.</p> |
| +<p class="small">This app has no machines yet, so the first line sets one up under this account's platform subdomain. It comes live within a couple of minutes.</p> |
| {{else}}<pre><code>{{.AttachCmd}}</code></pre> |
| -<p class="small">The host comes live on its box's next provision pass.</p> |
| +<p class="small">The address comes live within a couple of minutes.</p> |
| {{end}} |
| {{end}} |
diff --git a/internal/console/templates/app_settings.html b/internal/console/templates/app_settings.html| index 4e24ef77b..cc672ce26 100644 |
| --- a/internal/console/templates/app_settings.html |
| +++ b/internal/console/templates/app_settings.html |
| @@ -88,8 +88,8 @@ offers the final console-record step.</p> |
| <a href="{{.Nav}}/trash">trash</a>: hosts stop serving it on their next pass |
| (custom domains included), and it is restorable for |
| {{trashCooloffDays}} days before the platform destroys its releases for |
| -good.{{if .Detail.Production.On}} It is flagged production right now, which |
| -refuses deletion — turn the supply-chain bake off first.{{end}}</p> |
| +good.{{if .Detail.Production.On}} It is marked as production right now, which |
| +blocks deletion — turn the safety delay off on the Versions tab first.{{end}}</p> |
| <form method="post" action="{{.Base}}/apps/{{.Detail.App}}/delete" class="settings-row"> |
| <input type="hidden" name="csrf" value="{{.CSRF}}"> |
| <label class="visually-hidden" for="delete-confirm">Type the app's name to confirm</label> |
diff --git a/internal/console/templates/ladder.html b/internal/console/templates/ladder.html| index 26e69c263..5cd165b96 100644 |
| --- a/internal/console/templates/ladder.html |
| +++ b/internal/console/templates/ladder.html |
| @@ -5,29 +5,29 @@ promotion moves a signed pointer, and hosts follow their channel.</p> |
| {{with .Detail.Production}} |
| <div class="bake"> |
| {{if .Err}} |
| - <p class="small error">Supply-chain bake: flag unreadable: {{.Err}} — hosts keep baking. |
| + <p class="small error">Safety delay: this app's setting can't be read right now, so the delay stays on. |
| {{if $.CanPromote}} |
| <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/production" class="inline"> |
| <input type="hidden" name="csrf" value="{{$.CSRF}}"> |
| <input type="hidden" name="production" value="on"> |
| - <button type="submit" class="linkish">Re-declare production</button> |
| + <button type="submit" class="linkish">Set it again</button> |
| </form> |
| — writes a fresh signed flag over the unreadable one. |
| {{end}} |
| </p> |
| {{else if .On}} |
| - <p class="small">Supply-chain bake: <strong>on</strong> — beta holds 24h, stable 72h, on each host's own clock. |
| + <p class="small">Safety delay: <strong>on</strong> — new versions wait ~24h on beta and ~72h on live before machines pick them up. |
| {{if and $.CanPromote $.IsOwner}} |
| <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/production" class="inline"> |
| <input type="hidden" name="csrf" value="{{$.CSRF}}"> |
| <input type="hidden" name="production" value="off"> |
| <button type="submit" class="linkish">Turn off</button> |
| </form> |
| - <span title="Turning off confirms with your passkey; each host honors it after the stable window (~72h) on its own clock.">— passkey; honored after ~72h</span> |
| + <span class="small">Turning it off asks for your passkey and takes effect after ~72h.</span> |
| {{end}} |
| </p> |
| {{else if .TurningOff}} |
| - <p class="small">Supply-chain bake: <strong>turning off</strong> — hosts honor it from ~{{fmtTime .EffectiveAt}}. |
| + <p class="small">Safety delay: <strong>turning off</strong> — in effect until ~{{fmtTime .EffectiveAt}}. |
| {{if $.CanPromote}} |
| <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/production" class="inline"> |
| <input type="hidden" name="csrf" value="{{$.CSRF}}"> |
| @@ -37,7 +37,7 @@ promotion moves a signed pointer, and hosts follow their channel.</p> |
| {{end}} |
| </p> |
| {{else}} |
| - <p class="small">Supply-chain bake: off — hosts adopt promoted releases immediately. |
| + <p class="small">Safety delay: off — new versions go live as soon as they're promoted. |
| {{if $.CanPromote}} |
| <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/production" class="inline"> |
| <input type="hidden" name="csrf" value="{{$.CSRF}}"> |
| @@ -48,7 +48,7 @@ promotion moves a signed pointer, and hosts follow their channel.</p> |
| </p> |
| {{end}} |
| {{if .Nudge}} |
| - <p class="small nudge">{{icon "triangle-alert"}} This app looks production-wired but adopts releases immediately — consider enabling the supply-chain bake.</p> |
| + <p class="small nudge">{{icon "triangle-alert"}} This app looks like it's serving real users but new versions go live immediately — consider turning the safety delay on.</p> |
| {{end}} |
| </div> |
| {{end}} |
| @@ -64,15 +64,15 @@ promotion moves a signed pointer, and hosts follow their channel.</p> |
| {{else}} |
| <p><code>{{.Channel}}</code> holds <code>{{.Version}}</code> |
| <span class="small" title="{{fmtTime .PromotedAt}}">promoted {{ago .PromotedAt}}{{if .Actor}} by {{.Actor}}{{end}}</span> |
| - {{if .Hotfix}}<span class="chip chip-stale">{{icon "zap"}} hotfix — ladder bypassed</span>{{end}} |
| - {{if .FastTrack}}<span class="chip chip-stale">{{icon "fast-forward"}} fast-tracked</span>{{end}}</p> |
| + {{if .Hotfix}}<span class="chip chip-stale">{{icon "zap"}} published directly — skipped the pipeline</span>{{end}} |
| + {{if .FastTrack}}<span class="chip chip-stale">{{icon "fast-forward"}} delay skipped</span>{{end}}</p> |
| {{if and (not .BakedUntil.IsZero) $.CanPromote $.IsOwner}} |
| <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/fasttrack" class="inline"> |
| <input type="hidden" name="csrf" value="{{$.CSRF}}"> |
| <input type="hidden" name="channel" value="{{.Channel}}"> |
| - <button type="submit" class="btn-sm">Fast-track {{.Version}}</button> |
| + <button type="submit" class="btn-sm btn-danger">Skip the delay for {{.Version}}</button> |
| </form> |
| - <span class="small">baking until ~{{fmtTime .BakedUntil}}</span> |
| + <span class="small">goes live {{until .BakedUntil}}</span> |
| {{end}} |
| {{end}} |
| {{if .Routes}} |
diff --git a/internal/console/templates/trash.html b/internal/console/templates/trash.html| index 5e3f9be48..997c149aa 100644 |
| --- a/internal/console/templates/trash.html |
| +++ b/internal/console/templates/trash.html |
| @@ -35,8 +35,8 @@ |
| <p class="small">Deleting now does what the 30-day wait would eventually do, immediately and |
| permanently: every object the app owns is destroyed and its name is freed. There is no undo |
| and no copy anywhere. Type the app's name to confirm, then confirm with your passkey — a |
| - signed-in browser on its own isn't enough. An app that is still flagged production is |
| - refused whatever you type.</p> |
| + signed-in browser on its own isn't enough. An app still marked as production can't be |
| + permanently deleted, whatever you type — turn its safety delay off first.</p> |
| {{range .Trashed}} |
| <form class="signin" method="post" action="{{$.Base}}/trash/delete"> |
| <input type="hidden" name="csrf" value="{{$.CSRF}}"> |
diff --git a/internal/console/until_test.go b/internal/console/until_test.go| new file mode 100644 |
| index 000000000..16501cf25 |
| --- /dev/null |
| +++ b/internal/console/until_test.go |
| @@ -0,0 +1,23 @@ |
| +package console |
| + |
| +import ( |
| + "testing" |
| + "time" |
| +) |
| + |
| +func TestUntilHelper(t *testing.T) { |
| + // humanAge always renders both components in the 1-24h bucket |
| + // ("4h0m", never bare "4h"), and untilText's own time.Until call runs a |
| + // few microseconds after this Add — just enough for the floored minute |
| + // to round down once. "~4h1m" covers the (vanishingly unlikely) case |
| + // where the call lands before that rollover. |
| + if got := untilText(time.Now().Add(4*time.Hour + time.Minute)); got != "~4h0m" && got != "~4h1m" { |
| + t.Errorf("until ~4h = %q", got) |
| + } |
| + if got := untilText(time.Time{}); got != "now" { |
| + t.Errorf("until zero = %q", got) |
| + } |
| + if got := untilText(time.Now().Add(-time.Hour)); got != "now" { |
| + t.Errorf("until past = %q", got) |
| + } |
| +} |
diff --git a/internal/console/view.go b/internal/console/view.go| index 3bc79e6ca..58075ce25 100644 |
| --- a/internal/console/view.go |
| +++ b/internal/console/view.go |
| @@ -692,6 +692,15 @@ func humanAge(d time.Duration) string { |
| } |
| } |
| +// untilText renders a future horizon the way the cards speak: "~4h", never |
| +// a timestamp. Past or zero is "now" — the wait is over, not unknown. |
| +func untilText(t time.Time) string { |
| + if t.IsZero() || !t.After(time.Now()) { |
| + return "now" |
| + } |
| + return "~" + humanAge(time.Until(t)) |
| +} |
| + |
| // versionsCap bounds the app page's release list. |
| const versionsCap = 20 |