oficina / meet Public

The door's copy is estilo's: "Sign in", one help line, titles

Paul ruled on 2026-10-01 that the door's submit reads "Sign in", never
"Email me a link", and on 2026-09-23 that the hub is never named to
people. The r3 polish audit (SUMMARY items 6/7) found every app saying
the door's words its own way. estilo feat/door-copy (fbd14e7) now owns
them: chrome.SignIn has no Heading/AddressLabel/Help/Submit fields, and
SignInNotice.Sent / .SignedOut supply the other two states.

So Calls pins that estilo and deletes its own door strings: the
heading, label and "Email me a link" submit; the sent heading, body and
"Use a different email address" link; and the signed-out page's
connect copy ("You can sign in again with Home", "Sign in with Home"),
which now says the door's words while keeping connect's action as the
Sign in href. Tab titles come from the door: "Sign in · Calls",
"Check your email · Calls", "Signed out · Calls". The sign-in email's
subject is chrome.EmailSubject("Calls"); its body is still rastrillo's
until rastrillo auth grows Config.Body.

Must not merge before estilo feat/door-copy lands; re-pin to estilo
main after its squash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev pushed by paul@keymail.dev 6bc7c120d6486b0e7a205bf78bf880892f36a343 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	6.832s
?   	amadan.net/oficina/calls/cmd/calls	[no test files]
ok  	amadan.net/oficina/calls/internal/calls	1.467s
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.431s
?   	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.259s
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.958s
diff --git a/go.mod b/go.mod
index de3e184..d7ea5e2 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-20261001075355-fbd14e759027
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..b3f89ae 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-20261001075355-fbd14e759027 h1:+NFDWW5ROkLaFIsT9flDuAjNeQJJxj0Safk32UcRyn0=
+amadan.net/oficina/estilo.git v0.0.0-20261001075355-fbd14e759027/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")
+ }
+}