oficina / home Public

The door speaks the suite's copy: "Sign in", and never "Home"

id.oficina.works is the door most people see, and it said "Continue",
"Check your mail" and "fifteen minutes" while every app's door is moving
to estilo's one copy set (chrome.English, estilo fbd14e7). Paul ruled on
2026-10-01 that the submit reads "Sign in", and on 2026-09-23 that the hub
is never named "Home" to people.

- Pin estilo fbd14e7 (feat/door-copy). Home draws its own templates, so
  it reads the words and tab titles from estilo's types (chrome.SignIn,
  SignInNotice.Sent/SignedOut, chrome.Title via .Title) rather than
  rendering estilo-signin. The hub cannot drift from the apps.
- The form gains the door's help line; the sent page and the directory's
  signed-out page take the door's heading, body and link; titles are
  "Sign in · Oficina", "Check your email · Oficina", "Signed out · Oficina".
- The sign-in email keeps "Sign in to Oficina" (chrome.EmailSubject) and
  gains chrome.EmailBody, so it says 15 minutes, not "expires shortly".
- signin_failed says "15 minutes" from chrome.LinkLifetime.
- "Home" leaves user copy: "Back to Oficina", "Not connected to Oficina
  yet", and connect.English reworded on Docs' approved docs.home.*
  strings ("Connect your Oficina account", ...); its signed-out state
  uses the door's "You’re signed out" / "Sign in".

Tests pin the rendered strings, the email, and the absence of "Home".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev pushed by paul@keymail.dev 7f868ecc199828d7b19afb7e6c526f1a49e475d2 parent 7a0f19c
18 files changed, +233 −57
  • connect/view.go +9 −5
  • connect/view_test.go +31 −0
  • go.mod +1 −1
  • go.sum +2 −18
  • internal/home/calma.go +15 −0
  • internal/home/directory_catalog.go +4 −4
  • internal/home/directory_catalog_test.go +1 −1
  • internal/home/door_copy_test.go +135 −0
  • internal/home/handoff_failure_test.go +2 −2
  • internal/home/signin.go +6 −1
  • internal/home/templates/check_your_mail.html +6 −6
  • internal/home/templates/directory_signed_out.html +6 −5
  • internal/home/templates/docs_chooser.html +1 −1
  • internal/home/templates/docs_empty.html +1 −1
  • internal/home/templates/docs_unavailable.html +1 −1
  • internal/home/templates/handoff_failed.html +2 −2
  • internal/home/templates/signin.html +9 −8
  • internal/home/templates/signin_failed.html +1 −1

CI passed — run details

CI log
amadan: .amadan/timeout: this job's budget is 15m0s (this runner allows up to 30m0s)
=== vet ===
CGO_ENABLED=0 go vet ./...
cd handoff && CGO_ENABLED=0 go vet ./...
cd roster && CGO_ENABLED=0 go vet ./...
cd connect && CGO_ENABLED=0 go vet ./...
=== fmt ===
=== build ===
CGO_ENABLED=0 go build ./...
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ./cmd/home ./cmd/oficina ./internal/operatorlaunch
cd handoff && CGO_ENABLED=0 go build ./...
cd roster && CGO_ENABLED=0 go build ./...
cd connect && CGO_ENABLED=0 go build ./...
=== go-test ===
CGO_ENABLED=0 go test -count=1 ./...
?   	amadan.net/oficina/home/cmd/home	[no test files]
ok  	amadan.net/oficina/home/cmd/home-placeholder	0.603s
?   	amadan.net/oficina/home/cmd/oficina	[no test files]
ok  	amadan.net/oficina/home/internal/guardstest	0.033s
ok  	amadan.net/oficina/home/internal/home	8.844s
ok  	amadan.net/oficina/home/internal/home/directorydata	0.965s
ok  	amadan.net/oficina/home/internal/home/logtest	0.004s
ok  	amadan.net/oficina/home/internal/home/seal	0.003s
ok  	amadan.net/oficina/home/internal/home/sealtest	0.003s
ok  	amadan.net/oficina/home/internal/operator	5.076s
ok  	amadan.net/oficina/home/internal/operatorlaunch	4.139s
ok  	amadan.net/oficina/home/internal/provisioner	38.271s
cd handoff && CGO_ENABLED=0 go test -count=1 ./...
ok  	amadan.net/oficina/home/handoff	0.321s
ok  	amadan.net/oficina/home/handoff/seal	0.002s
cd roster && CGO_ENABLED=0 go test -count=1 ./...
ok  	amadan.net/oficina/home/roster	5.084s
cd connect && CGO_ENABLED=0 go test -count=1 ./...
ok  	amadan.net/oficina/home/connect	0.062s
ok  	amadan.net/oficina/home/connect/linkstore	0.193s
=== race ===
go test -race -count=1 ./internal/home/ ./internal/operator/ ./internal/operatorlaunch/ ./internal/provisioner/ ./internal/home/directorydata/
ok  	amadan.net/oficina/home/internal/home	96.855s
ok  	amadan.net/oficina/home/internal/operator	64.169s
ok  	amadan.net/oficina/home/internal/operatorlaunch	22.387s
ok  	amadan.net/oficina/home/internal/provisioner	273.305s
ok  	amadan.net/oficina/home/internal/home/directorydata	16.007s
cd handoff && CGO_ENABLED=1 go test -race -count=1 ./...
ok  	amadan.net/oficina/home/handoff	9.296s
ok  	amadan.net/oficina/home/handoff/seal	1.008s
cd roster && CGO_ENABLED=1 go test -race -count=1 ./...
ok  	amadan.net/oficina/home/roster	6.591s
cd connect && CGO_ENABLED=1 go test -race -count=1 ./...
ok  	amadan.net/oficina/home/connect	1.636s
ok  	amadan.net/oficina/home/connect/linkstore	3.765s
diff --git a/connect/view.go b/connect/view.go
index ba0eda2..c7736a2 100644
--- a/connect/view.go
+++ b/connect/view.go
@@ -46,10 +46,14 @@ func Page(state, label string, translate ...func(string) string) PageData {
return p
}
+// English is the copy the apps render for connecting an account. It
+// never names the hub: the account is "your Oficina account" (Paul,
+// 2026-09-23), worded as Docs' approved docs.home.* strings are. The
+// signed-out state speaks the door's words (estilo chrome.English).
var English = map[string]string{
- "signedout_title": "Signed out", "signedout_body": "You can sign in again with Home.", "signedout_action": "Sign in with Home",
- "start_title": "Connect your account to Home", "start_body": "Use Home to sign in to this account. Your content and sharing settings stay as they are.", "start_action": "Continue to Home", "cancel_action": "Back",
- "confirm_title": "Confirm account connection", "confirm_body": "Connect the account below to the Home account you just signed into.", "confirm_action": "Connect accounts",
- "unknown_title": "Home account not connected", "unknown_body": "Sign in to your existing account, then connect Home from the account menu.", "local_action": "Sign in to your existing account",
- "failed_title": "Could not connect accounts", "failed_body": "Return to the app and start again.", "success_title": "Accounts connected", "success_body": "You can now use Home to sign in to this account.", "documents_action": "Back to the app", "menu_action": "Connect Home account",
+ "signedout_title": "You’re signed out", "signedout_body": "You can sign in again with your Oficina account.", "signedout_action": "Sign in",
+ "start_title": "Connect your Oficina account", "start_body": "Use your Oficina account to sign in to this account. Your content and sharing settings stay as they are.", "start_action": "Connect", "cancel_action": "Back",
+ "confirm_title": "Confirm account connection", "confirm_body": "Connect the account below to the Oficina account you just signed in to.", "confirm_action": "Connect accounts",
+ "unknown_title": "Oficina account not connected", "unknown_body": "Sign in to your existing account, then connect your Oficina account from the account menu.", "local_action": "Sign in to your existing account",
+ "failed_title": "Could not connect accounts", "failed_body": "Return to the app and start again.", "success_title": "Accounts connected", "success_body": "You can now use your Oficina account to sign in to this account.", "documents_action": "Back to the app", "menu_action": "Connect your Oficina account",
}
diff --git a/connect/view_test.go b/connect/view_test.go
new file mode 100644
index 0000000..b48f1ca
--- /dev/null
+++ b/connect/view_test.go
@@ -0,0 +1,31 @@
+package connect
+
+import (
+ "strings"
+ "testing"
+)
+
+// The apps render this copy. It never names the hub (Paul, 2026-09-23):
+// the account is "your Oficina account".
+func TestEnglishNeverNamesTheHub(t *testing.T) {
+ for k, v := range English {
+ if strings.Contains(v, "Home") {
+ t.Errorf("%s = %q names Home", k, v)
+ }
+ }
+ for state, want := range map[string]string{
+ "signedout": "You’re signed out",
+ "start": "Connect your Oficina account",
+ "unknown": "Oficina account not connected",
+ } {
+ if got := Page(state, "").Title; got != want {
+ t.Errorf("%s title = %q, want %q", state, got, want)
+ }
+ }
+ if got := Page("signedout", "").ActionLabel; got != "Sign in" {
+ t.Errorf("signedout action = %q, want %q", got, "Sign in")
+ }
+ if got := English["menu_action"]; got != "Connect your Oficina account" {
+ t.Errorf("menu_action = %q", got)
+ }
+}
diff --git a/go.mod b/go.mod
index 989def4..762b8a4 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.25.0
toolchain go1.26.5
require (
- amadan.net/oficina/estilo.git v0.0.0-20260929172243-77346e82cb36
+ amadan.net/oficina/estilo.git v0.0.0-20261001075355-fbd14e759027
amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489
amadan.net/oficina/home/roster v0.0.0-20260908123429-74dda570e0cf
amadan.net/rastrillo/rastrillo v0.26.1-0.20260908075707-29e4491631bd
diff --git a/go.sum b/go.sum
index 8768f37..18b7781 100644
--- a/go.sum
+++ b/go.sum
@@ -1,23 +1,7 @@
amadan.net/carlos/platform.git/launchctx v0.0.0-20260911135504-fb577f96f7d1 h1:RxwIRojWcVtwSdNAjtdz7y1bxHLh3GUgqT1TrXVKpyM=
amadan.net/carlos/platform.git/launchctx v0.0.0-20260911135504-fb577f96f7d1/go.mod h1:6fBmMWgtESLFXZURzu/HyRXM0vyLuo4+XGGdKuZ+wao=
-amadan.net/oficina/estilo.git v0.0.0-20260911182719-0bcb9287988d h1:3HsB96tGs+P0tGf2VzqpqA8yqYBhJ+apBh2txAoz9qI=
-amadan.net/oficina/estilo.git v0.0.0-20260911182719-0bcb9287988d/go.mod h1:0R45/vCUAT+kpS7S6K/ZxBdE8f7YxFN4HT3HyDJ3nPY=
-amadan.net/oficina/estilo.git v0.0.0-20260929065402-0f830bc12f2c h1:XE8qYLSGKYLN+SNJv+/BMx0rCnHFUJusJcQ0/JLS+IM=
-amadan.net/oficina/estilo.git v0.0.0-20260929065402-0f830bc12f2c/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
-amadan.net/oficina/estilo.git v0.0.0-20260929065843-683ccd534ead h1:t5QdNWBWWIJonOPdyD6CuwZ64XdahP7j8U+ewlTg1e8=
-amadan.net/oficina/estilo.git v0.0.0-20260929065843-683ccd534ead/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
-amadan.net/oficina/estilo.git v0.0.0-20260929070257-7a1a376ad2ab h1:ypKDilZG0lxfXyPrEPVPmWFHnDNDsl1NE+IndxiukkY=
-amadan.net/oficina/estilo.git v0.0.0-20260929070257-7a1a376ad2ab/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
-amadan.net/oficina/estilo.git v0.0.0-20260929070530-5b2ce835e773 h1:9DIAhG3TaGG+LE2+8LvU73ZEhfK+oNn2y2p4kFjAAi0=
-amadan.net/oficina/estilo.git v0.0.0-20260929070530-5b2ce835e773/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
-amadan.net/oficina/estilo.git v0.0.0-20260929082849-4d41ba2100ff h1:eGeR7E6hcL8b1z6FR1v0LkoY58lVyVLju7xvz39gJkQ=
-amadan.net/oficina/estilo.git v0.0.0-20260929082849-4d41ba2100ff/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
-amadan.net/oficina/estilo.git v0.0.0-20260929171000-db2000f8275c h1:RPB7vypc+8AFIl+vMAxPCwFbEp2o+G3Dop+68klwn2M=
-amadan.net/oficina/estilo.git v0.0.0-20260929171000-db2000f8275c/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
-amadan.net/oficina/estilo.git v0.0.0-20260929171256-065aca0402c2 h1:zPP+L/nK+PpXsg237Y5nAGP6RaAmoUBcp5DKY5kzNAY=
-amadan.net/oficina/estilo.git v0.0.0-20260929171256-065aca0402c2/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
-amadan.net/oficina/estilo.git v0.0.0-20260929172243-77346e82cb36 h1:Gu1R9LtvUQjn7HsUcJxowYHgIB9SdKq8yrDQs6SWnWg=
-amadan.net/oficina/estilo.git v0.0.0-20260929172243-77346e82cb36/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/handoff v0.0.0-20260908102137-481d2d57f489 h1:RPKtyry/71i06o8L+gjAaUyexbmG56kO1YPs601URAM=
amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489/go.mod h1:YISpzFJ+iMgrM0rUL032OSzsAQbP4/nozvXLCCOcn1k=
amadan.net/oficina/home/roster v0.0.0-20260908123429-74dda570e0cf h1:SRKjCUgtk42WwR7VSn+V0eLNk20mfiY8dQmzdw13Sbo=
diff --git a/internal/home/calma.go b/internal/home/calma.go
index 631387a..92557f3 100644
--- a/internal/home/calma.go
+++ b/internal/home/calma.go
@@ -4,6 +4,7 @@ import (
"amadan.net/oficina/estilo.git/busy"
_ "embed"
+ "amadan.net/oficina/estilo.git/chrome"
"amadan.net/oficina/estilo.git/sky"
"amadan.net/oficina/estilo.git/theme"
"github.com/go-chi/chi/v5"
@@ -25,8 +26,22 @@ var homeFuncs = map[string]any{
"suiteSky": func() sky.View { return sky.For(sky.SuiteSeed) },
// Onest's stylesheet, under estilo's versioned prefix.
"fontsCSS": func() string { return theme.FontPrefix + "fonts.css" },
+ // The door's words and tab titles are estilo's (chrome.English), the
+ // one copy set every app's door speaks, so the hub's door cannot
+ // drift from theirs. Home draws its own templates, so it reads the
+ // strings from estilo's types rather than rendering estilo-signin.
+ "signinDoor": func() chrome.SignIn { return chrome.SignIn{App: suiteName} },
+ "sentDoor": func() chrome.SignInNotice { return chrome.SignInNotice{App: suiteName}.Sent("/signin") },
+ "signedOutDoor": func() chrome.SignInNotice {
+ return chrome.SignInNotice{App: suiteName}.SignedOut("/auth/start")
+ },
+ "linkLifetime": func() int { return int(chrome.LinkLifetime.Minutes()) },
}
+// suiteName is what the hub calls itself to people: the suite, never
+// "Home" (Paul, 2026-09-23).
+const suiteName = "Oficina"
+
// doorPages are the pages at the door — signing in, and everything on the
// way in or out. The layout draws them as one card on the sky with the
// Oficina mark, as Calma's sign-in does; every other page keeps the full
diff --git a/internal/home/directory_catalog.go b/internal/home/directory_catalog.go
index b417778..0e89eb0 100644
--- a/internal/home/directory_catalog.go
+++ b/internal/home/directory_catalog.go
@@ -16,11 +16,11 @@ func directoryCatalog(cfg Runtime) []catalogApp {
}
apps := []catalogApp{
docs,
- {Name: "Sheets", Icon: "/static/brand/sheets.svg", Status: "Not connected to Home yet"},
- {Name: "Slides", Icon: "/static/brand/slides.svg", Status: "Not connected to Home yet"},
- {Name: "Calendar", Icon: "/static/brand/calendar.svg", Status: "Not connected to Home yet"},
+ {Name: "Sheets", Icon: "/static/brand/sheets.svg", Status: "Not connected to Oficina yet"},
+ {Name: "Slides", Icon: "/static/brand/slides.svg", Status: "Not connected to Oficina yet"},
+ {Name: "Calendar", Icon: "/static/brand/calendar.svg", Status: "Not connected to Oficina yet"},
{Name: "Calls", Icon: "/static/brand/calls.svg", Status: "Coming soon"},
- {Name: "Memoria", Icon: "/static/brand/memoria.svg", Status: "Not connected to Home yet"},
+ {Name: "Memoria", Icon: "/static/brand/memoria.svg", Status: "Not connected to Oficina yet"},
{Name: "Mail", Icon: "/static/brand/mail.svg", Status: "Separate sign-in"},
{Name: "Files", Icon: "/static/brand/files.svg", Status: "Separate sign-in"},
}
diff --git a/internal/home/directory_catalog_test.go b/internal/home/directory_catalog_test.go
index e103859..58db5be 100644
--- a/internal/home/directory_catalog_test.go
+++ b/internal/home/directory_catalog_test.go
@@ -53,7 +53,7 @@ func TestDirectoryCatalogRequiresSigninAndDoesNotInventLaunchDestinations(t *tes
if strings.Count(body, "<a ") != 7 || strings.Count(body, `href="/help/open-source"`) != 1 || !strings.Contains(body, `href="/apps/docs"`) || !strings.Contains(body, "Choose a team") || hasOtherScript(body) {
t.Fatal("catalog advertised unsupported launch", body)
}
- if strings.Count(body, "Not connected to Home yet") != 0 || strings.Count(body, "Coming soon") != 0 || strings.Count(body, "Separate sign-in") != 2 {
+ if strings.Count(body, "Not connected to Oficina yet") != 0 || strings.Count(body, "Coming soon") != 0 || strings.Count(body, "Separate sign-in") != 2 {
t.Fatal("catalog readiness missing", body)
}
entry := f.request("GET", docsEntryPath, "", "", cookie)
diff --git a/internal/home/door_copy_test.go b/internal/home/door_copy_test.go
new file mode 100644
index 0000000..7c5552a
--- /dev/null
+++ b/internal/home/door_copy_test.go
@@ -0,0 +1,135 @@
+package home
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+// The door's words are estilo's chrome.English, the one copy set every
+// app's door renders (Paul, 2026-10-01: the submit reads "Sign in", never
+// "Continue" or "Email me a link"). They are pinned here as literals, not
+// read back from chrome.English, so a change upstream shows up as a red
+// test on the hub rather than as silently different copy.
+const (
+ doorHelp = "We’ll email you a link that signs you in. It works once and expires in 15 minutes."
+ doorSentBody = "If that address can sign in here, we’ve sent it a link. It works once and expires in 15 minutes."
+)
+
+func getCredential(t *testing.T, app *testApp, path string) string {
+ t.Helper()
+ req := httptest.NewRequest(http.MethodGet, path, nil)
+ req.Host = credentialHost
+ w := httptest.NewRecorder()
+ app.Router().ServeHTTP(w, req)
+ if w.Code != http.StatusOK {
+ t.Fatalf("GET %s: %d", path, w.Code)
+ }
+ return w.Body.String()
+}
+
+func mustContain(t *testing.T, page, body string, wants ...string) {
+ t.Helper()
+ for _, want := range wants {
+ if !strings.Contains(body, want) {
+ t.Errorf("%s: missing %q", page, want)
+ }
+ }
+}
+
+func mustNotContain(t *testing.T, page, body string, nots ...string) {
+ t.Helper()
+ for _, not := range nots {
+ if strings.Contains(body, not) {
+ t.Errorf("%s: still says %q", page, not)
+ }
+ }
+}
+
+func TestDoorSpeaksTheSuiteCopy(t *testing.T) {
+ app := newTestApp(t)
+
+ form := getCredential(t, app, "/signin")
+ mustContain(t, "/signin", form,
+ "<title>Sign in · Oficina</title>",
+ "<h1>Sign in</h1>",
+ ">Email address</label>",
+ doorHelp,
+ `<button type="submit" rst-btn="primary">Sign in</button>`)
+ mustNotContain(t, "/signin", form, ">Continue<", "Email me a link", "Home")
+
+ sent := getCredential(t, app, "/signin/sent")
+ mustContain(t, "/signin/sent", sent,
+ "<title>Check your email · Oficina</title>",
+ "<h1>Check your email</h1>",
+ doorSentBody,
+ `<a href="/signin">Use a different address</a>`)
+ mustNotContain(t, "/signin/sent", sent, "fifteen", "Check your mail", "Home")
+
+ failed := redeem(t, app, "not-a-token").Body.String()
+ mustContain(t, "signin failed", failed, "Sign-in links work once and expire after 15 minutes.")
+ mustNotContain(t, "signin failed", failed, "fifteen")
+
+ f := newMountedChooser(t)
+ out := f.request("GET", "/signed-out", "", "", nil)
+ if out.Code != http.StatusOK {
+ t.Fatalf("GET /signed-out: %d", out.Code)
+ }
+ mustContain(t, "/signed-out", out.Body.String(),
+ "<title>Signed out · Oficina</title>",
+ "<h1>You’re signed out</h1>",
+ "You’ve signed out of Oficina on this device.",
+ `rst-btn="primary">Sign in</a>`)
+ mustNotContain(t, "/signed-out", out.Body.String(), "You are signed out", "Home")
+}
+
+// The hub builds its own signin.Flow, so it sets the email itself: the
+// suite's subject, and a body that says how long the link lasts rather
+// than keymaildev's default "expires shortly".
+func TestSigninEmailSpeaksTheSuiteCopy(t *testing.T) {
+ app := newTestApp(t)
+ signIn(t, app, "someone@example.com")
+ mail, ok := app.mailer.last()
+ if !ok {
+ t.Fatal("no mail was sent")
+ }
+ if mail.Subject != "Sign in to Oficina" {
+ t.Errorf("subject = %q", mail.Subject)
+ }
+ for _, want := range []string{"Here’s your link to sign in to Oficina:", Origin + linkPath + "?token=", "It works once and expires in 15 minutes."} {
+ if !strings.Contains(mail.Body, want) {
+ t.Errorf("body missing %q:\n%s", want, mail.Body)
+ }
+ }
+ if strings.Contains(mail.Body, "shortly") || strings.Contains(mail.Body, "Home") {
+ t.Errorf("body is not the suite's copy:\n%s", mail.Body)
+ }
+}
+
+// Never name the hub to people (Paul, 2026-09-23): the suite is Oficina.
+func TestHubNeverCallsItselfHome(t *testing.T) {
+ f := newMountedChooser(t)
+ page := f.request("GET", "/", "", "", f.session(t, chooserPerson))
+ mustNotContain(t, "directory", page.Body.String(), "to Home")
+ for _, app := range directoryCatalog(Runtime{}) {
+ if strings.Contains(app.Status, "Home") {
+ t.Errorf("%s: status %q names Home", app.Name, app.Status)
+ }
+ }
+ files, err := templatesFS.ReadDir("templates")
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, e := range files {
+ b, err := templatesFS.ReadFile("templates/" + e.Name())
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, bad := range []string{"Back to Home", "Return to Home", "to Home"} {
+ if strings.Contains(string(b), bad) {
+ t.Errorf("%s says %q", e.Name(), bad)
+ }
+ }
+ }
+}
diff --git a/internal/home/handoff_failure_test.go b/internal/home/handoff_failure_test.go
index 092f393..254164c 100644
--- a/internal/home/handoff_failure_test.go
+++ b/internal/home/handoff_failure_test.go
@@ -37,7 +37,7 @@ func TestHandoffFailuresHaveFixedPrivateDiagnostics(t *testing.T) {
t.Fatal(response.Code, response.Header())
}
body := response.Body.String()
- if !strings.Contains(body, "Could not finish signing in") || strings.Contains(body, "fifteen minutes") || !strings.Contains(body, `href="`+directoryOrigin+`"`) || strings.Contains(body, secret) {
+ if !strings.Contains(body, "Could not finish signing in") || strings.Contains(body, "15 minutes") || !strings.Contains(body, `href="`+directoryOrigin+`"`) || strings.Contains(body, secret) {
t.Fatal("wrong handoff failure page", body)
}
log := p.logs.String()
@@ -52,7 +52,7 @@ func TestHandoffFailuresHaveFixedPrivateDiagnostics(t *testing.T) {
}
p.logs.Reset()
failedLink := exchangeRequest(p.credential.App, "POST", Origin+linkPath, Origin, url.Values{"token": {secret}})
- if failedLink.Code != 400 || !strings.Contains(failedLink.Body.String(), "Sign-in links work once and expire after fifteen minutes") || strings.Contains(p.logs.String(), "home handoff failed") {
+ if failedLink.Code != 400 || !strings.Contains(failedLink.Body.String(), "Sign-in links work once and expire after 15 minutes") || strings.Contains(p.logs.String(), "home handoff failed") {
t.Fatal("magic-link failure changed", failedLink.Code)
}
}
diff --git a/internal/home/signin.go b/internal/home/signin.go
index d66be07..1888483 100644
--- a/internal/home/signin.go
+++ b/internal/home/signin.go
@@ -7,6 +7,7 @@ import (
"strings"
"time"
+ "amadan.net/oficina/estilo.git/chrome"
"github.com/carlosframework/rastrillo/sessions"
"github.com/keymaildev/signin"
)
@@ -94,7 +95,11 @@ func newFlow(secrets Secrets, links signin.LinkStore, origin string) *signin.Flo
Key: secrets.SigninKey,
Origin: origin,
LinkPath: linkPath,
- Subject: "Sign in to Oficina",
+ // The suite's subject and body (estilo chrome.English), so the
+ // email says how long the link lasts rather than keymaildev's
+ // default "expires shortly", and names Oficina, never "Home".
+ Subject: chrome.EmailSubject(suiteName),
+ Body: chrome.EmailBody(suiteName),
}
}
diff --git a/internal/home/templates/check_your_mail.html b/internal/home/templates/check_your_mail.html
index 8d38667..517f714 100644
--- a/internal/home/templates/check_your_mail.html
+++ b/internal/home/templates/check_your_mail.html
@@ -1,6 +1,6 @@
-{{define "title"}}Check your mail · Oficina{{end}}
-{{define "content"}}
-<h1>Check your mail</h1>
-<p>If that address can sign in here, a link is on its way. It works once, and expires in fifteen minutes.</p>
-<p><a href="/signin">Use a different address</a></p>
-{{end}}
+{{define "title"}}{{sentDoor.Title}}{{end}}
+{{define "content"}}{{with sentDoor}}
+<h1>{{.Heading}}</h1>
+<p>{{.Body}}</p>
+<p><a href="{{.Link.Href}}">{{.Link.Label}}</a></p>
+{{end}}{{end}}
diff --git a/internal/home/templates/directory_signed_out.html b/internal/home/templates/directory_signed_out.html
index ace69eb..19d79fe 100644
--- a/internal/home/templates/directory_signed_out.html
+++ b/internal/home/templates/directory_signed_out.html
@@ -1,5 +1,6 @@
-{{define "title"}}Oficina{{end}}
-{{define "content"}}
-<h1>You are signed out</h1>
-<a href="/auth/start" rst-btn="primary">Sign in</a>
-{{end}}
+{{define "title"}}{{signedOutDoor.Title}}{{end}}
+{{define "content"}}{{with signedOutDoor}}
+<h1>{{.Heading}}</h1>
+<p>{{.Body}}</p>
+<a href="{{.Link.Href}}" rst-btn="primary">{{.Link.Label}}</a>
+{{end}}{{end}}
diff --git a/internal/home/templates/docs_chooser.html b/internal/home/templates/docs_chooser.html
index f8de751..eec274c 100644
--- a/internal/home/templates/docs_chooser.html
+++ b/internal/home/templates/docs_chooser.html
@@ -7,5 +7,5 @@
<button type="submit" name="team" value="{{.SQID}}" rst-btn>{{.Name}}</button>
{{end}}
</form>
-<p><a href="/">Back to Home</a></p>
+<p><a href="/">Back to Oficina</a></p>
{{end}}
diff --git a/internal/home/templates/docs_empty.html b/internal/home/templates/docs_empty.html
index 483356e..d3df80d 100644
--- a/internal/home/templates/docs_empty.html
+++ b/internal/home/templates/docs_empty.html
@@ -2,5 +2,5 @@
{{define "content"}}
<h1>No teams available</h1>
<p>This account has no team with {{.AppName}} available.</p>
-<p><a href="/">Back to Home</a></p>
+<p><a href="/">Back to Oficina</a></p>
{{end}}
diff --git a/internal/home/templates/docs_unavailable.html b/internal/home/templates/docs_unavailable.html
index 51e2bcd..f8ff90b 100644
--- a/internal/home/templates/docs_unavailable.html
+++ b/internal/home/templates/docs_unavailable.html
@@ -2,5 +2,5 @@
{{define "content"}}
<h1>Could not load your teams</h1>
<p>Try again in a moment.</p>
-<p><a href="/">Back to Home</a></p>
+<p><a href="/">Back to Oficina</a></p>
{{end}}
diff --git a/internal/home/templates/handoff_failed.html b/internal/home/templates/handoff_failed.html
index 0017972..fa7ddcd 100644
--- a/internal/home/templates/handoff_failed.html
+++ b/internal/home/templates/handoff_failed.html
@@ -1,6 +1,6 @@
{{define "title"}}Could not finish signing in · Oficina{{end}}
{{define "content"}}
<h1>Could not finish signing in</h1>
-<p>Return to Home and open the app again.</p>
-<p><a href="{{.HomeURL}}">Back to Home</a></p>
+<p>Return to Oficina and open the app again.</p>
+<p><a href="{{.HomeURL}}">Back to Oficina</a></p>
{{end}}
diff --git a/internal/home/templates/signin.html b/internal/home/templates/signin.html
index 94b0e20..373cdff 100644
--- a/internal/home/templates/signin.html
+++ b/internal/home/templates/signin.html
@@ -1,14 +1,15 @@
-{{define "title"}}Sign in · Oficina{{end}}
-{{define "content"}}
-<h1>Sign in</h1>
-{{if .Error}}<p role="alert" rst-callout rst-tone="negative"><span rst-callout-body>{{.Error}}</span></p>{{end}}
+{{define "title"}}{{signinDoor.Title}}{{end}}
+{{define "content"}}{{with signinDoor}}
+<h1>{{.Heading}}</h1>
+{{if $.Error}}<p role="alert" rst-callout rst-tone="negative"><span rst-callout-body>{{$.Error}}</span></p>{{end}}
<form method="post" action="/signin" rst-form>
<div rst-field>
- <label rst-field-label for="address">Email address</label>
- <input id="address" name="address" type="email" autocomplete="email" required autofocus rst-input>
+ <label rst-field-label for="address">{{.AddressLabel}}</label>
+ <input id="address" name="address" type="email" autocomplete="email" required autofocus rst-input aria-describedby="address-help">
+ <p id="address-help" rst-field-help>{{.Help}}</p>
</div>
<div rst-form-foot>
- <button type="submit" rst-btn="primary">Continue</button>
+ <button type="submit" rst-btn="primary">{{.Submit}}</button>
</div>
</form>
-{{end}}
+{{end}}{{end}}
diff --git a/internal/home/templates/signin_failed.html b/internal/home/templates/signin_failed.html
index 74929f8..d8494b5 100644
--- a/internal/home/templates/signin_failed.html
+++ b/internal/home/templates/signin_failed.html
@@ -1,6 +1,6 @@
{{define "title"}}That link did not work · Oficina{{end}}
{{define "content"}}
<h1>That link did not work</h1>
-<p>Sign-in links work once and expire after fifteen minutes. Ask for a new one.</p>
+<p>Sign-in links work once and expire after {{linkLifetime}} minutes. Ask for a new one.</p>
<p><a href="/signin">Start again</a></p>
{{end}}