Speak the suite's sign-in door copy in Calls
The door's button reads 'Sign in'; the sent page, signed-out page and tab titles come from estilo's one copy set; the email subject is 'Sign in to Calls'. Retires 'A sign in link should be on its way with a link to click.', 'Use a different email address' and 'Sign in with Home'. Reviewed by GPT-6-Astra via Codex (#23). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Squashed from chore/door-copy into main.
9 files changed,
+91
−35
go.mod+1 −1go.sum+2 −2internal/calls/auth.go+7 −14internal/calls/home.go+6 −8internal/calls/home_router_test.go+10 −2internal/calls/templates/signedout.html+3 −3internal/calls/templates/signin.html+1 −1internal/callstest/harness_test.go+15 −3internal/callstest/signin_test.go+46 −1
CI passed — run details
CI log
=== vet === go vet ./... === fmt === === test === go test ./... ok amadan.net/oficina/calls/callscore (cached) ? amadan.net/oficina/calls/cmd/calls [no test files] ok amadan.net/oficina/calls/internal/calls 1.486s ok amadan.net/oficina/calls/internal/calls/apns (cached) ok amadan.net/oficina/calls/internal/calls/hub (cached) ? amadan.net/oficina/calls/internal/calls/icons [no test files] ok amadan.net/oficina/calls/internal/calls/relay (cached) ok amadan.net/oficina/calls/internal/calls/seal (cached) ok amadan.net/oficina/calls/internal/callstest 8.445s ? amadan.net/oficina/calls/protocol [no test files] ? amadan.net/oficina/calls/scripts/writeestilo [no test files] === migration-check === go run amadan.net/rastrillo/rastrillo/cmd/rastrillo migration check rastrillo migration check: models and migrations agree === race === CGO_ENABLED=1 go test -race ./internal/... ok amadan.net/oficina/calls/internal/calls 9.263s ok amadan.net/oficina/calls/internal/calls/apns (cached) ok amadan.net/oficina/calls/internal/calls/hub (cached) ? amadan.net/oficina/calls/internal/calls/icons [no test files] ok amadan.net/oficina/calls/internal/calls/relay (cached) ok amadan.net/oficina/calls/internal/calls/seal (cached) ok amadan.net/oficina/calls/internal/callstest 23.968s
diff --git a/go.mod b/go.mod| index de3e184..5cc6c80 100644 |
| --- a/go.mod |
| +++ b/go.mod |
| @@ -3,7 +3,7 @@ module amadan.net/oficina/calls |
| go 1.26.0 |
| require ( |
| - amadan.net/oficina/estilo.git v0.0.0-20260929231226-cce11010ece0 |
| + amadan.net/oficina/estilo.git v0.0.0-20261001084202-0b4d92c3dc16 |
| amadan.net/oficina/home/connect v0.0.0-20260923190447-01166fcfc0a9 |
| amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489 |
| amadan.net/rastrillo/rastrillo v0.26.1-0.20260908075707-29e4491631bd |
diff --git a/go.sum b/go.sum| index 04b1901..3542d64 100644 |
| --- a/go.sum |
| +++ b/go.sum |
| @@ -1,5 +1,5 @@ |
| -amadan.net/oficina/estilo.git v0.0.0-20260929231226-cce11010ece0 h1:Qww+xGIYJaj+6TIX4Knr71ghxeYJjtgFm6WukX6NkGg= |
| -amadan.net/oficina/estilo.git v0.0.0-20260929231226-cce11010ece0/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw= |
| +amadan.net/oficina/estilo.git v0.0.0-20261001084202-0b4d92c3dc16 h1:F5v3x25dEj1rJNnBm71G7shN+fM+T2actUlaGPvoeH8= |
| +amadan.net/oficina/estilo.git v0.0.0-20261001084202-0b4d92c3dc16/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw= |
| amadan.net/oficina/home/connect v0.0.0-20260923190447-01166fcfc0a9 h1:K/PaOVdQqRMwifPD6ipiwLeGR5nbFSxm9FxjrDJlZcg= |
| amadan.net/oficina/home/connect v0.0.0-20260923190447-01166fcfc0a9/go.mod h1:YroavXe43Wietec+po8mtSk4a9Bir2h31Q3iAf4MWxI= |
| amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489 h1:RPKtyry/71i06o8L+gjAaUyexbmG56kO1YPs601URAM= |
diff --git a/internal/calls/auth.go b/internal/calls/auth.go| index 60804e6..cffd033 100644 |
| --- a/internal/calls/auth.go |
| +++ b/internal/calls/auth.go |
| @@ -32,6 +32,7 @@ func (a *app) newAuthPlugin(writer *sql.DB) (*rauth.Auth, error) { |
| Origin: a.cfg.Origin, |
| InstanceKey: a.cfg.instanceKey, |
| Mailer: a.cfg.mailer, |
| + Subject: chrome.EmailSubject("Calls"), |
| Authorize: a.authorize, |
| SigninPath: signinPath, |
| SignedInPath: landing, |
| @@ -192,13 +193,10 @@ func (a *app) requireMember(next http.Handler) http.Handler { |
| func (a *app) localSignin(w http.ResponseWriter, r *http.Request) { |
| door := chrome.SignIn{ |
| - Icon: assets.Path("static/brand/icon.svg"), |
| - App: "Calls", |
| - Sky: sky.For(r.Host), |
| - Heading: "Sign in", |
| - Action: signinPath, |
| - AddressLabel: "Email address", |
| - Submit: "Email me a link", |
| + Icon: assets.Path("static/brand/icon.svg"), |
| + App: "Calls", |
| + Sky: sky.For(r.Host), |
| + Action: signinPath, |
| } |
| switch r.URL.Query().Get("err") { |
| case "address": |
| @@ -215,13 +213,8 @@ func (a *app) localSignin(w http.ResponseWriter, r *http.Request) { |
| "Sent": r.URL.Query().Get("sent") == "1", |
| "Door": door, |
| "Notice": chrome.SignInNotice{ |
| - Icon: door.Icon, |
| - App: door.App, |
| - Sky: door.Sky, |
| - Heading: "Check your email", |
| - Body: "A sign in link should be on its way with a link to click.", |
| - Link: &chrome.Link{Href: signinPath, Label: "Use a different email address"}, |
| - }, |
| + Icon: door.Icon, App: door.App, Sky: door.Sky, |
| + }.Sent(signinPath), |
| }) |
| } |
diff --git a/internal/calls/home.go b/internal/calls/home.go| index f5264d6..8744887 100644 |
| --- a/internal/calls/home.go |
| +++ b/internal/calls/home.go |
| @@ -123,16 +123,14 @@ func (a *app) signin(w http.ResponseWriter, r *http.Request) { |
| func (a *app) renderHome(w http.ResponseWriter, r *http.Request, state, label string, status int) { |
| name, data := "home", a.page(r, map[string]any{"Home": connect.Page(state, label)}) |
| if state == "signedout" { |
| - // Where Sign out lands: the Oficina door, in connect's own words. |
| + // Where Sign out lands: the Oficina door, in the door's words |
| + // (connect's name the hub); its Sign in goes to connect's action. |
| home := connect.Page(state, label) |
| name, data = "signedout", map[string]any{"Notice": chrome.SignInNotice{ |
| - Icon: assets.Path("static/brand/icon.svg"), |
| - App: "Calls", |
| - Sky: sky.For(r.Host), |
| - Heading: home.Title, |
| - Body: home.Body, |
| - Link: &chrome.Link{Href: home.Action, Label: home.ActionLabel}, |
| - }} |
| + Icon: assets.Path("static/brand/icon.svg"), |
| + App: "Calls", |
| + Sky: sky.For(r.Host), |
| + }.SignedOut(home.Action)} |
| } |
| var body bytes.Buffer |
| if err := pages[name].ExecuteTemplate(&body, "layout", data); err != nil { |
diff --git a/internal/calls/home_router_test.go b/internal/calls/home_router_test.go| index 7972b06..10ac041 100644 |
| --- a/internal/calls/home_router_test.go |
| +++ b/internal/calls/home_router_test.go |
| @@ -347,7 +347,8 @@ func TestHomeSigninClearsOnlyDefinitivelyStaleSession(t *testing.T) { |
| } |
| // With Home connected, Sign out lands on the Oficina door's signed-out |
| -// page, in connect's own words, and its one link goes back to /signin. |
| +// page, in the door's own words (not connect's, which name the hub), and |
| +// its one link, "Sign in", goes back to connect's sign-in action. |
| func TestHomeSignedOutIsTheOficinaDoor(t *testing.T) { |
| f := newHomeRouterFixture(t) |
| w := httptest.NewRecorder() |
| @@ -356,9 +357,16 @@ func TestHomeSignedOutIsTheOficinaDoor(t *testing.T) { |
| if w.Code != http.StatusOK || w.Header().Get("Cache-Control") != "no-store" { |
| t.Fatalf("GET /signed-out: %d %q", w.Code, w.Header().Get("Cache-Control")) |
| } |
| - for _, want := range []string{"<div estilo-door", "<span>Calls</span>", "estilo-sky-layer", connect.English["signedout_title"], `<a rst-btn="primary" href="/signin">` + connect.English["signedout_action"]} { |
| + for _, want := range []string{"<div estilo-door", "<span>Calls</span>", "estilo-sky-layer", |
| + "<title>Signed out · Calls</title>", "<h1>You’re signed out</h1>", "You’ve signed out of Calls on this device.", |
| + `<a rst-btn="primary" href="/signin">Sign in</a>`} { |
| if !strings.Contains(body, want) { |
| t.Errorf("signed-out page is missing %q:\n%s", want, body) |
| } |
| } |
| + for _, gone := range []string{connect.English["signedout_body"], connect.English["signedout_action"], "Home"} { |
| + if strings.Contains(body, gone) { |
| + t.Errorf("signed-out page still carries %q:\n%s", gone, body) |
| + } |
| + } |
| } |
diff --git a/internal/calls/templates/signedout.html b/internal/calls/templates/signedout.html| index a275f71..f967279 100644 |
| --- a/internal/calls/templates/signedout.html |
| +++ b/internal/calls/templates/signedout.html |
| @@ -1,5 +1,5 @@ |
| -{{define "title"}}Signed out · Calls{{end}} |
| +{{define "title"}}{{.Notice.Title}}{{end}} |
| {{/* Where Sign out lands with Home connected: the Oficina door's |
| - signed-out page. home.go's renderHome builds it from connect's own |
| - signed-out strings. */}} |
| + signed-out page, in the door's own words. home.go's renderHome |
| + builds it. */}} |
| {{define "content"}}{{template "estilo-signed-out" .Notice}}{{end}} |
diff --git a/internal/calls/templates/signin.html b/internal/calls/templates/signin.html| index 7f49e4c..e45f2a2 100644 |
| --- a/internal/calls/templates/signin.html |
| +++ b/internal/calls/templates/signin.html |
| @@ -1,4 +1,4 @@ |
| -{{define "title"}}{{if .Sent}}Check your email{{else}}Sign in{{end}}{{end}} |
| +{{define "title"}}{{if .Sent}}{{.Notice.Title}}{{else}}{{.Door.Title}}{{end}}{{end}} |
| {{/* The Oficina door (estilo chrome): one card on the suite's sky, Calls' |
| mark and name at its head. The form, or once a link has been asked |
| for, the check-your-email notice. auth.go's localSignin builds both. */}} |
diff --git a/internal/callstest/harness_test.go b/internal/callstest/harness_test.go| index 22305b1..fc4aedf 100644 |
| --- a/internal/callstest/harness_test.go |
| +++ b/internal/callstest/harness_test.go |
| @@ -159,9 +159,10 @@ func (x *harness) pendingJoins(t *testing.T) int { |
| } |
| type recordingMailer struct { |
| - mu sync.Mutex |
| - to []string |
| - links []string |
| + mu sync.Mutex |
| + to []string |
| + subjects []string |
| + links []string |
| } |
| var magicLink = regexp.MustCompile(`https?://\S+/auth/verify\?token=\S+`) |
| @@ -170,6 +171,7 @@ func (m *recordingMailer) Send(ctx context.Context, to, subject, body string) er |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| m.to = append(m.to, to) |
| + m.subjects = append(m.subjects, subject) |
| m.links = append(m.links, magicLink.FindString(body)) |
| return nil |
| } |
| @@ -183,6 +185,16 @@ func (m *recordingMailer) last() (string, string) { |
| return m.to[len(m.to)-1], m.links[len(m.links)-1] |
| } |
| +func (m *recordingMailer) lastSubject(t *testing.T) string { |
| + t.Helper() |
| + m.mu.Lock() |
| + defer m.mu.Unlock() |
| + if len(m.subjects) == 0 { |
| + t.Fatal("no mail sent") |
| + } |
| + return m.subjects[len(m.subjects)-1] |
| +} |
| + |
| func (m *recordingMailer) count() int { |
| m.mu.Lock() |
| defer m.mu.Unlock() |
diff --git a/internal/callstest/signin_test.go b/internal/callstest/signin_test.go| index 1f44264..38c5951 100644 |
| --- a/internal/callstest/signin_test.go |
| +++ b/internal/callstest/signin_test.go |
| @@ -160,7 +160,7 @@ func TestSigninPageIsTheOficinaDoor(t *testing.T) { |
| body := rec.Body.String() |
| for _, want := range []string{ |
| "<div estilo-door", "<span>Calls</span>", `<img estilo-sky-layer="placeholder"`, |
| - `<img estilo-sky-layer="grain"`, `action="/signin"`, `name="address"`, "Email me a link", |
| + `<img estilo-sky-layer="grain"`, `action="/signin"`, `name="address"`, |
| } { |
| if !strings.Contains(body, want) { |
| t.Errorf("sign-in page is missing %q:\n%s", want, body) |
| @@ -196,3 +196,48 @@ func TestSigninPageIsTheOficinaDoor(t *testing.T) { |
| t.Errorf("err=rate is not a page error:\n%s", page) |
| } |
| } |
| + |
| +// The door's words are estilo's, the same in every Oficina app: the |
| +// submit reads "Sign in" (never "Email me a link", Paul 2026-10-01), one |
| +// help line says what happens, the sent state says the one body and links |
| +// back with "Use a different address", and each tab title is |
| +// "<Page> · Calls". |
| +func TestSigninDoorSpeaksTheSuitesCopy(t *testing.T) { |
| + x := newHarness(t, "paul@example.com") |
| + c := x.browser(t) |
| + form := c.get("/signin").Body.String() |
| + for _, want := range []string{ |
| + "<title>Sign in · Calls</title>", "<h1>Sign in</h1>", ">Email address<", |
| + "We’ll email you a link that signs you in. It works once and expires in 15 minutes.", |
| + `<button rst-btn="primary" type="submit">Sign in</button>`, |
| + } { |
| + if !strings.Contains(form, want) { |
| + t.Errorf("sign-in form is missing %q:\n%s", want, form) |
| + } |
| + } |
| + if strings.Contains(form, "Email me a link") { |
| + t.Errorf("sign-in form still says \"Email me a link\":\n%s", form) |
| + } |
| + sent := c.get("/signin?sent=1").Body.String() |
| + for _, want := range []string{ |
| + "<title>Check your email · Calls</title>", "<h1>Check your email</h1>", |
| + "If that address can sign in here, we’ve sent it a link. It works once and expires in 15 minutes.", |
| + `<a href="/signin">Use a different address</a>`, |
| + } { |
| + if !strings.Contains(sent, want) { |
| + t.Errorf("sent page is missing %q:\n%s", want, sent) |
| + } |
| + } |
| +} |
| + |
| +// The sign-in email's subject names the app: "Sign in to Calls". |
| +func TestSigninEmailSubjectNamesCalls(t *testing.T) { |
| + x := newHarness(t, "paul@example.com") |
| + x.browser(t).post("/signin", url.Values{"address": {"paul@example.com"}}) |
| + if x.mail.count() != 1 { |
| + t.Fatalf("sent %d mails, want 1", x.mail.count()) |
| + } |
| + if got := x.mail.lastSubject(t); got != "Sign in to Calls" { |
| + t.Errorf("sign-in email subject %q, want %q", got, "Sign in to Calls") |
| + } |
| +} |