oficina / sheets Public

feat: serve a root page instead of 404

sheets.bds.oncarlos.com/ answered "404 page not found" because the app
genuinely has no "/" route — it is a headless engine. A 404 is honest
about the route and misleading about the deploy: it looks like a broken
instance rather than a working one with no UI.

The page says what this address is, that it has no UI deliberately, and
which two routes do answer. Those two are registered by rastrillo.Serve,
not by this package, so both were verified against the deployed instance
before being named here (/healthz -> ok, /api/version -> 0ccd7ab).

Unstyled. estilo exists and is pinned to day, but this app does not
consume it and nobody decided that it should.

The "/" route is exact, not a catch-all: a catch-all would serve this
page at every URL, so a mistyped path would look like a working page
instead of a 404. TestIndexIsNotACatchAll pins that, and all three new
tests were watched failing against a deliberately broken page before
being kept.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APtNStXDZ5H2fAc1ZuGqtS
Paul Campbell pushed by paul@keymail.dev aa3d7f26bc1e180663d8fb8ce085f480b1f317a0 parent 0ccd7ab
3 files changed, +113 −0
  • internal/sheets/app.go +4 −0
  • internal/sheets/page.go +48 −0
  • internal/sheets/page_test.go +61 −0
diff --git a/internal/sheets/app.go b/internal/sheets/app.go
index 115d8bc..4520f71 100644
--- a/internal/sheets/app.go
+++ b/internal/sheets/app.go
@@ -30,5 +30,9 @@ func (a *App) Router() http.Handler {
r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
+ // Exact "/" only, not a catch-all: chi answers 404 for every
+ // other path, which is what an operator hitting a wrong URL
+ // should see (TestIndexIsNotACatchAll pins this).
+ r.Get("/", a.handleIndex)
return r
}
diff --git a/internal/sheets/page.go b/internal/sheets/page.go
new file mode 100644
index 0000000..bdc0aae
--- /dev/null
+++ b/internal/sheets/page.go
@@ -0,0 +1,48 @@
+package sheets
+
+import "net/http"
+
+// indexPage is the entire root page: bare fact, not marketing. It has
+// no dynamic content and no user input to escape, so a plain constant
+// string stands in for html/template here rather than pretending to
+// need one.
+//
+// No stylesheet is linked. oficina/estilo exists and is pinned to
+// rastrillo's day theme, but this app does not consume it and nobody
+// has decided that it should — the grid slice is where that decision
+// belongs. Semantic, unstyled HTML is the honest choice until then.
+//
+// The two routes named below are registered by rastrillo.Serve's outer
+// mux, not by this package (see cmd/sheets/main.go). They are named
+// here because someone who lands on / is usually an operator checking
+// whether the instance is alive, and both were verified answering on
+// the deployed instance.
+const indexPage = `<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Oficina Sheets</title>
+</head>
+<body>
+<main>
+<h1>Oficina Sheets</h1>
+<p>This is the Oficina Sheets engine: sealed storage, formula
+evaluation, and XLSX import and export.</p>
+<p>It has no user interface yet. That is deliberate: the grid is the
+next slice of work, not built here.</p>
+<p>In production this instance answers <code>GET /healthz</code> and
+<code>GET /api/version</code>. There is nothing else to reach at this
+address.</p>
+<p>The design is at
+<code>docs/superpowers/specs/2026-08-30-sheets-design.md</code> in this
+repository.</p>
+</main>
+</body>
+</html>
+`
+
+// handleIndex serves the one page this app has.
+func (a *App) handleIndex(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Write([]byte(indexPage))
+}
diff --git a/internal/sheets/page_test.go b/internal/sheets/page_test.go
new file mode 100644
index 0000000..e1c9ea6
--- /dev/null
+++ b/internal/sheets/page_test.go
@@ -0,0 +1,61 @@
+package sheets
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+func get(t *testing.T, path string) *httptest.ResponseRecorder {
+ t.Helper()
+ rec := httptest.NewRecorder()
+ New(nil).Router().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
+ return rec
+}
+
+func TestIndexServesHTML(t *testing.T) {
+ rec := get(t, "/")
+ if rec.Code != http.StatusOK {
+ t.Fatalf("GET / = %d, want 200", rec.Code)
+ }
+ if ct := rec.Header().Get("Content-Type"); ct != "text/html; charset=utf-8" {
+ t.Errorf("Content-Type = %q", ct)
+ }
+ // The page's job is to say what this address is and what it is not.
+ for _, want := range []string{
+ "<h1>Oficina Sheets</h1>",
+ "no user interface yet",
+ "/healthz",
+ "/api/version",
+ "2026-08-30-sheets-design.md",
+ } {
+ if !strings.Contains(rec.Body.String(), want) {
+ t.Errorf("body missing %q", want)
+ }
+ }
+}
+
+// TestIndexIsNotACatchAll is the point of the "/" route being exact.
+// A catch-all would serve the page at every URL, so a mistyped path
+// would look like a working page instead of a 404 — and nothing else
+// in the suite would notice.
+func TestIndexIsNotACatchAll(t *testing.T) {
+ for _, path := range []string{"/nope", "/api/sheets", "/healthz/x"} {
+ if code := get(t, path).Code; code != http.StatusNotFound {
+ t.Errorf("GET %s = %d, want 404", path, code)
+ }
+ }
+}
+
+// TestIndexLeaksNothing: the one rule reaches even a static page. No
+// cell content, sheet title or tab name exists here, and no build,
+// path or host detail should either.
+func TestIndexLeaksNothing(t *testing.T) {
+ body := get(t, "/").Body.String()
+ for _, forbidden := range []string{"/home/", "oncarlos.com", "sqlite", ".db"} {
+ if strings.Contains(strings.ToLower(body), strings.ToLower(forbidden)) {
+ t.Errorf("page contains %q", forbidden)
+ }
+ }
+}