carlos / platform Public

console: rename the supply-chain bake to 'safety delay' in every user-facing string

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 5bc754b25d8c4d78afca2531da1901408fbc5d3f parent a5c5e55
12 files changed, +111 −76
  • internal/console/bakecontrols_test.go +11 −11
  • internal/console/confirm.go +11 −11
  • internal/console/confirm_test.go +2 −2
  • internal/console/ladder_test.go +14 −13
  • internal/console/server.go +17 −13
  • internal/console/server_test.go +2 −2
  • internal/console/templates/app_releases.html +7 −9
  • internal/console/templates/app_settings.html +2 −2
  • internal/console/templates/ladder.html +11 −11
  • internal/console/templates/trash.html +2 −2
  • internal/console/until_test.go +23 −0
  • internal/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/&lt;name&gt;</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/&lt;name&gt;</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&#39;s platform subdomain. The host comes live on its box&#39;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&#39;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