oficina / meet Public

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.
claude agent, operated by paul@keymail.dev f5a1254975e616c73837bb7c732010d6c3170a97 parent bf4e24d
9 files changed, +91 −35
  • go.mod +1 −1
  • go.sum +2 −2
  • internal/calls/auth.go +7 −14
  • internal/calls/home.go +6 −8
  • internal/calls/home_router_test.go +10 −2
  • internal/calls/templates/signedout.html +3 −3
  • internal/calls/templates/signin.html +1 −1
  • internal/callstest/harness_test.go +15 −3
  • internal/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")
+ }
+}