Add deployable skeleton: platform contract + one placeholder page
The home had zero Go files. This is the smallest honest deploy target: go.mod, cmd/home/main.go on rastrillo v0.21.0's Resolve->Serve contract (no database opened — this skeleton has nothing to persist yet, and rastrillo.Handler skips OpenDB whenever DBPath is unset), and internal/home serving one unstyled page at "/" stating what the home is, that it isn't built yet, and where the design spec lives. Makefile and .amadan/ci.d mirror sheets-core's, minus the migrations and race targets neither apply to yet. No sign-in, directory, assertions or provisioning — those are docs/superpowers/specs/2026-08-30-home-design.md's substantial project, deliberately out of scope here so this doesn't become a half-built identity service. 🤖 Generated with Claude Opus 5, at the direction of Paul Campbell.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APtNStXDZ5H2fAc1ZuGqtS
12 files changed,
+339
−0
.amadan/ci.d/10-vet+8 −0.amadan/ci.d/20-fmt+8 −0.amadan/ci.d/30-build+8 −0.amadan/ci.d/40-go-test+8 −0.amadan/timeout+1 −0Makefile+39 −0cmd/home/main.go+60 −0go.mod+21 −0go.sum+55 −0internal/home/app.go+51 −0internal/home/app_test.go+38 −0internal/home/page.go+42 −0
CI passed — run details
Intent
diff --git a/.amadan/ci.d/10-vet b/.amadan/ci.d/10-vet| new file mode 100755 |
| index 0000000..698073f |
| --- /dev/null |
| +++ b/.amadan/ci.d/10-vet |
| @@ -0,0 +1,8 @@ |
| +#!/bin/sh |
| +# One step of the home's CI. It runs a Makefile target rather than its |
| +# own copy of the commands, so CI and the local gate stay ONE |
| +# definition: `make test` runs these same targets in this same order, |
| +# and `make test-vet` is exactly what CI runs here. Debugging CI is |
| +# running it. |
| +set -eu |
| +exec make test-vet |
diff --git a/.amadan/ci.d/20-fmt b/.amadan/ci.d/20-fmt| new file mode 100755 |
| index 0000000..b802353 |
| --- /dev/null |
| +++ b/.amadan/ci.d/20-fmt |
| @@ -0,0 +1,8 @@ |
| +#!/bin/sh |
| +# One step of the home's CI. It runs a Makefile target rather than its |
| +# own copy of the commands, so CI and the local gate stay ONE |
| +# definition: `make test` runs these same targets in this same order, |
| +# and `make test-fmt` is exactly what CI runs here. Debugging CI is |
| +# running it. |
| +set -eu |
| +exec make test-fmt |
diff --git a/.amadan/ci.d/30-build b/.amadan/ci.d/30-build| new file mode 100755 |
| index 0000000..43a17da |
| --- /dev/null |
| +++ b/.amadan/ci.d/30-build |
| @@ -0,0 +1,8 @@ |
| +#!/bin/sh |
| +# One step of the home's CI. It runs a Makefile target rather than its |
| +# own copy of the commands, so CI and the local gate stay ONE |
| +# definition: `make test` runs these same targets in this same order, |
| +# and `make test-build` is exactly what CI runs here. Debugging CI is |
| +# running it. |
| +set -eu |
| +exec make test-build |
diff --git a/.amadan/ci.d/40-go-test b/.amadan/ci.d/40-go-test| new file mode 100755 |
| index 0000000..94160ec |
| --- /dev/null |
| +++ b/.amadan/ci.d/40-go-test |
| @@ -0,0 +1,8 @@ |
| +#!/bin/sh |
| +# One step of the home's CI. It runs a Makefile target rather than its |
| +# own copy of the commands, so CI and the local gate stay ONE |
| +# definition: `make test` runs these same targets in this same order, |
| +# and `make test-go` is exactly what CI runs here. Debugging CI is |
| +# running it. |
| +set -eu |
| +exec make test-go |
diff --git a/.amadan/timeout b/.amadan/timeout| new file mode 100644 |
| index 0000000..960e1d6 |
| --- /dev/null |
| +++ b/.amadan/timeout |
| @@ -0,0 +1 @@ |
| +15m |
diff --git a/Makefile b/Makefile| new file mode 100644 |
| index 0000000..0c08b0a |
| --- /dev/null |
| +++ b/Makefile |
| @@ -0,0 +1,39 @@ |
| +.PHONY: build test test-vet test-fmt test-build test-go |
| + |
| +# One binary, CGO_ENABLED=0, cgo-free SQLite (AGENTS.md / Deploys). This |
| +# skeleton opens no database yet — see cmd/home/main.go — but the |
| +# constraint holds regardless: nothing here may need cgo to build. |
| +build: |
| + CGO_ENABLED=0 go build ./... |
| + |
| +# The gate, split into named targets so CI can report them individually |
| +# (.amadan/ci.d calls these one at a time, each its own step). `make |
| +# test` runs exactly the same things in the same order, so the local |
| +# gate and CI stay ONE definition rather than two lists that drift |
| +# apart. Debugging CI is running `make test`. |
| +# |
| +# Order is cheapest-and-fastest first: vet and gofmt are near-instant, |
| +# build catches compile errors before time is spent on tests, plain |
| +# `go test` runs last. No test-migrations (no database yet) and no |
| +# test-race (no concurrency-bearing package yet) — see Sheets' |
| +# Makefile for what those look like once this app needs them. |
| +test: test-vet test-fmt test-build test-go |
| + |
| +test-vet: |
| + CGO_ENABLED=0 go vet ./... |
| + |
| +# Fails on unformatted files rather than rewriting them. CI must never |
| +# silently "fix" a diff out from under a branch — a red test-fmt tells |
| +# the author to run `gofmt -w` themselves. |
| +test-fmt: |
| + @if [ -n "$$(gofmt -l .)" ]; then \ |
| + echo "gofmt needs to be run on:"; \ |
| + gofmt -l .; \ |
| + exit 1; \ |
| + fi |
| + |
| +test-build: |
| + CGO_ENABLED=0 go build ./... |
| + |
| +test-go: |
| + CGO_ENABLED=0 go test -count=1 ./... |
diff --git a/cmd/home/main.go b/cmd/home/main.go| new file mode 100644 |
| index 0000000..93252d3 |
| --- /dev/null |
| +++ b/cmd/home/main.go |
| @@ -0,0 +1,60 @@ |
| +// Command home is the Oficina suite home binary. |
| +// |
| +// This is a deployable skeleton: it satisfies the platform activation |
| +// contract and serves internal/home's one placeholder page. None of |
| +// the home's real functionality exists yet — see |
| +// docs/superpowers/specs/2026-08-30-home-design.md and |
| +// internal/home/page.go. |
| +// |
| +// The platform activation contract has no $PORT: the platform (or a |
| +// developer) invokes this binary as |
| +// |
| +// home --socket <path> |
| +// |
| +// and rastrillo.Resolve/Serve own that contract end to end — activation |
| +// argv, LISTEN_FDS, $STATE_DIRECTORY, GET /healthz, GET /api/version, |
| +// SIGTERM drain, baseline security headers. |
| +// |
| +// Unlike Sheets (internal/sheets, one SQLite file per instance), this |
| +// skeleton opens no database: it has no models and nothing to persist |
| +// yet. rastrillo.Options.DBPath stays unset, so |
| +// rastrillo.Handler/Serve never call OpenDB at all (see serve.go: |
| +// "var db *sql.DB; ... if opts.DBPath != \"\" { db, err = OpenDB(...) }") |
| +// — there is no db.Open call to make and no handle to blank before |
| +// Serve. When the home gains real storage, this file grows the same |
| +// Resolve -> db.Open -> build mux -> blank DBPath -> Serve shape Sheets |
| +// uses. |
| +package main |
| + |
| +import ( |
| + "log/slog" |
| + "net/http" |
| + "os" |
| + |
| + "github.com/carlosframework/rastrillo" |
| + |
| + "amadan.net/oficina/home/internal/home" |
| +) |
| + |
| +func main() { |
| + logger := slog.Default() |
| + |
| + opts, err := rastrillo.Resolve(rastrillo.Options{Logger: logger}) |
| + if err != nil { |
| + logger.Error("resolve", "err", err) |
| + os.Exit(1) |
| + } |
| + |
| + app := home.New() |
| + |
| + // rastrillo.Options.Mux wants a *http.ServeMux; the app's own |
| + // router is a chi.Router (an http.Handler), so it mounts under one. |
| + mux := http.NewServeMux() |
| + mux.Handle("/", app.Router()) |
| + opts.Mux = mux |
| + |
| + if err := rastrillo.Serve(opts); err != nil { |
| + logger.Error("serve", "err", err) |
| + os.Exit(1) |
| + } |
| +} |
diff --git a/go.mod b/go.mod| new file mode 100644 |
| index 0000000..ede8ee7 |
| --- /dev/null |
| +++ b/go.mod |
| @@ -0,0 +1,21 @@ |
| +module amadan.net/oficina/home |
| + |
| +go 1.25.0 |
| + |
| +require ( |
| + github.com/carlosframework/rastrillo v0.21.0 |
| + github.com/go-chi/chi/v5 v5.3.2 |
| +) |
| + |
| +require ( |
| + github.com/dustin/go-humanize v1.0.1 // indirect |
| + github.com/google/uuid v1.6.0 // indirect |
| + github.com/mattn/go-isatty v0.0.20 // indirect |
| + github.com/ncruces/go-strftime v1.0.0 // indirect |
| + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect |
| + golang.org/x/sys v0.46.0 // indirect |
| + modernc.org/libc v1.74.1 // indirect |
| + modernc.org/mathutil v1.7.1 // indirect |
| + modernc.org/memory v1.11.0 // indirect |
| + modernc.org/sqlite v1.55.0 // indirect |
| +) |
diff --git a/go.sum b/go.sum| new file mode 100644 |
| index 0000000..9d009d5 |
| --- /dev/null |
| +++ b/go.sum |
| @@ -0,0 +1,55 @@ |
| +github.com/carlosframework/rastrillo v0.21.0 h1:uqw/ZK+y/VwS+jDnIv2llAIug3AuFWesSp1xTEPS45w= |
| +github.com/carlosframework/rastrillo v0.21.0/go.mod h1:pZlrE5F5OhspvZFzef0DrfjHAjA8FXycb9J5/8vc8Tw= |
| +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= |
| +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= |
| +github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY= |
| +github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= |
| +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= |
| +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= |
| +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= |
| +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= |
| +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= |
| +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= |
| +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= |
| +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= |
| +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= |
| +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= |
| +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= |
| +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= |
| +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= |
| +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= |
| +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= |
| +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= |
| +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
| +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= |
| +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= |
| +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= |
| +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= |
| +modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc= |
| +modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= |
| +modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= |
| +modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk= |
| +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= |
| +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= |
| +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= |
| +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= |
| +modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI= |
| +modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= |
| +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= |
| +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= |
| +modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ= |
| +modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os= |
| +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= |
| +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= |
| +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= |
| +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= |
| +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= |
| +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= |
| +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= |
| +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= |
| +modernc.org/sqlite v1.55.0 h1:hIFh0MCH0rGinQ/4KYb5/UbCkRkb+UP+OkLCVWa5MTM= |
| +modernc.org/sqlite v1.55.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw= |
| +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= |
| +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= |
| +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= |
| +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= |
diff --git a/internal/home/app.go b/internal/home/app.go| new file mode 100644 |
| index 0000000..d62bd2a |
| --- /dev/null |
| +++ b/internal/home/app.go |
| @@ -0,0 +1,51 @@ |
| +// Package home is the Oficina suite home app: identity, directory, |
| +// provisioning glue shared by every instance in the suite. See |
| +// AGENTS.md (binding) and |
| +// docs/superpowers/specs/2026-08-30-home-design.md (the design) for |
| +// what this service is and, just as importantly, is not. |
| +// |
| +// This package is a deployable skeleton, not the home: it wires the |
| +// platform contract and serves one placeholder page. None of the |
| +// home's actual functionality — sign-in, directory, assertions, |
| +// provisioning — is implemented yet. See page.go. |
| +package home |
| + |
| +import ( |
| + "net/http" |
| + |
| + "github.com/go-chi/chi/v5" |
| +) |
| + |
| +// App holds the wiring. One per process. |
| +// |
| +// It holds no database handle: the skeleton has no state to keep, and |
| +// AGENTS.md's "no address at rest" tests have nothing to guard until |
| +// real storage exists. Kept as a struct (rather than a bare package |
| +// func) so App/New/Router matches the Sheets app's shape at |
| +// internal/sheets/app.go — see New's comment. |
| +type App struct{} |
| + |
| +// New builds an App. It takes no arguments today because there is |
| +// nothing yet to wire in — no database, no session store, no identity |
| +// plugin. Kept as a constructor rather than a free function so this |
| +// package reads like Sheets' internal/sheets.New(db *gorm.DB) *App: |
| +// adding real wiring later is a one-line signature change, not a |
| +// reshape. |
| +func New() *App { return &App{} } |
| + |
| +// Router returns the mounted handler. |
| +func (a *App) Router() http.Handler { |
| + r := chi.NewRouter() |
| + r.Get("/", a.handleIndex) |
| + // This /healthz is NOT what answers in a deployed instance: |
| + // rastrillo.Serve registers its own GET /healthz on the outer mux |
| + // (serve.go's buildHandler), and Go 1.22+ http.ServeMux prefers that |
| + // exact pattern over this router's "/" mount, so the outer one wins |
| + // every time in production. It exists here so App's own router is |
| + // testable in isolation (see TestHealthz) and as the seam later |
| + // routes mount alongside — mirrors Sheets' internal/sheets/app.go. |
| + r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) { |
| + w.WriteHeader(http.StatusOK) |
| + }) |
| + return r |
| +} |
diff --git a/internal/home/app_test.go b/internal/home/app_test.go| new file mode 100644 |
| index 0000000..2c639aa |
| --- /dev/null |
| +++ b/internal/home/app_test.go |
| @@ -0,0 +1,38 @@ |
| +package home |
| + |
| +import ( |
| + "net/http" |
| + "net/http/httptest" |
| + "testing" |
| +) |
| + |
| +// TestIndex asserts the one page this skeleton serves answers 200 with |
| +// HTML — the placeholder content itself is not asserted here so this |
| +// test does not need updating every time the wording changes. |
| +func TestIndex(t *testing.T) { |
| + app := New() |
| + req := httptest.NewRequest(http.MethodGet, "/", nil) |
| + rec := httptest.NewRecorder() |
| + app.Router().ServeHTTP(rec, req) |
| + if rec.Code != http.StatusOK { |
| + t.Fatalf("GET / = %d, want 200", rec.Code) |
| + } |
| + if ct := rec.Header().Get("Content-Type"); ct == "" { |
| + t.Fatalf("GET / Content-Type is empty") |
| + } |
| +} |
| + |
| +// TestHealthz exercises App.Router()'s own /healthz in isolation via |
| +// httptest — it does NOT prove what a deployed instance answers. In |
| +// production rastrillo.Serve's outer mux registers its own GET /healthz |
| +// ahead of this one and wins (see the comment on Router in app.go); this |
| +// test only proves the app's router itself is wired and reachable. |
| +func TestHealthz(t *testing.T) { |
| + app := New() |
| + req := httptest.NewRequest(http.MethodGet, "/healthz", nil) |
| + rec := httptest.NewRecorder() |
| + app.Router().ServeHTTP(rec, req) |
| + if rec.Code != http.StatusOK { |
| + t.Fatalf("healthz = %d, want 200", rec.Code) |
| + } |
| +} |
diff --git a/internal/home/page.go b/internal/home/page.go| new file mode 100644 |
| index 0000000..43d8a7c |
| --- /dev/null |
| +++ b/internal/home/page.go |
| @@ -0,0 +1,42 @@ |
| +package home |
| + |
| +import "net/http" |
| + |
| +// indexPage is the entire placeholder 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. rastrillo's ui/tokens.css needs a theme |
| +// (day, plain or signal — see ui/themes/*.css) to paint anything; |
| +// picking one is a design decision, and design-system questions are |
| +// paused on this project (AGENTS.md's Frontend section assumes a |
| +// themed app, which this skeleton deliberately is not yet). Semantic, |
| +// unstyled HTML is the honest choice until that pause lifts. |
| +const indexPage = `<!doctype html> |
| +<html lang="en"> |
| +<head> |
| +<meta charset="utf-8"> |
| +<title>Oficina home</title> |
| +</head> |
| +<body> |
| +<main> |
| +<h1>Oficina home</h1> |
| +<p>This is the Oficina suite home: identity, a directory of your teams, |
| +and provisioning, shared by every app in the suite.</p> |
| +<p>It is not built yet. This page is a placeholder that proves the |
| +deploy path works. There is no sign-in, no directory, and no |
| +provisioning here.</p> |
| +<p>The design is at |
| +<code>docs/superpowers/specs/2026-08-30-home-design.md</code> in this |
| +repository.</p> |
| +</main> |
| +</body> |
| +</html> |
| +` |
| + |
| +// handleIndex serves the one page this skeleton has. |
| +func (a *App) handleIndex(w http.ResponseWriter, _ *http.Request) { |
| + w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| + w.Write([]byte(indexPage)) |
| +} |