carlos / platform Public

spec: console redesign — carloku-informed reskin + IA split

Direction approved 2026-08-06: horizontal tabs for app sub-pages, domains
under app Settings, one branch for reskin + restructure. POST paths and
gate ordering frozen; GET pages split per the route table.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 8d7089cf659a1de4c312acea36df8479094a9485 parent dd87bf6
1 file changed, +184 −0
  • docs/superpowers/specs/2026-08-06-console-redesign-design.md +184 −0
diff --git a/docs/superpowers/specs/2026-08-06-console-redesign-design.md b/docs/superpowers/specs/2026-08-06-console-redesign-design.md
new file mode 100644
index 000000000..a96b85c9a
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-06-console-redesign-design.md
@@ -0,0 +1,184 @@
+# Console redesign: carloku-informed reskin + information architecture
+
+**Date:** 2026-08-06
+**Status:** approved direction (tabs / domains-under-settings / one branch — Paul, 2026-08-06)
+
+## Why
+
+The console reads like a modified blog theme: serif body text (Charter/Georgia),
+one "Apps" nav link, and two overloaded pages — `apps.html` is account home +
+team + create-app + account settings in one scroll, and `app.html` is a 203-line
+everything-page. carloku.com established a design language (purple accent,
+system sans + ui-monospace, hairline rules, light/dark) that the console should
+inherit, reshaped for a Heroku/Render/Fly-class console.
+
+## Non-goals
+
+- No JavaScript beyond the existing switcher script; no webfonts, no favicon,
+ no off-origin subresources (`icons_test.go` invariants stand unchanged).
+- No new features: no env/config UI, no app rename/delete, no fleet nav link
+ (`/fleet` stays deliberately unlinked and unprobeable).
+- No POST path moves. Step-up stash/resume re-enters the same path and the
+ gate-order tests pin behavior; forms move to new pages, their actions do not.
+- No API changes (`/api/cli/*`, `/auth/cli/*` JSON endpoints untouched).
+
+## Visual direction
+
+Adopt the carloku tokens wholesale in `layout.html`:
+
+```css
+:root {
+ --bg: #ffffff; --ink: #201d29; --muted: #6f6480;
+ --accent: #5b3e96; --accent-strong: #4c3380;
+ --accent-soft: rgba(91, 62, 150, 0.09);
+ --rule: #eae7f0;
+ --bad: #b3403a; /* keep, re-tuned against the new palette */
+ --warn: #a6790f; /* DEFINE it — today referenced but undefined */
+ --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ --sans: ui-sans-serif, system-ui, -apple-system, sans-serif;
+}
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg: #16141c; --ink: #f0eef6; --muted: #a99ebd;
+ --accent: #b394f2; --accent-strong: #c9b2f7;
+ --accent-soft: rgba(179, 148, 242, 0.12);
+ --rule: #2c2836; --bad: #e07b76; --warn: #d9a94a;
+ }
+}
+```
+
+Dark `--bad`/`--warn` values may be tuned during build for contrast (AA on
+`--bg`); everything else is carloku-verbatim.
+
+**Type.** Body becomes `var(--sans)` (~1rem/1.6). `var(--mono)` is the working
+console typeface: page eyebrows, tab labels, chips, identifiers (release IDs,
+channels, hosts, sqids, addresses), table cells that carry machine truth.
+Headings sans semibold. The serif stack is deleted.
+
+**Page anatomy.** Every page opens with a header block:
+
+- mono eyebrow naming the context (`ACCOUNT · AMADAN`, `APP · KEYMAIL-RELAY`,
+ `YOUR SETTINGS`),
+- an `h1`,
+- a one-line `.lede` in `--muted` saying what the page is for, in plain human
+ words ("People who can see and ship this account's apps.").
+
+Content max-width widens 56rem → 64rem (consoles need table room); prose
+elements keep a ~40rem reading measure. Buttons take the carloku primary style
+(accent bg, `--bg` text, 0.4rem radius, 600 weight; hover `--accent-strong`).
+Destructive buttons stay `--bad`-outlined. Guidance rows (e.g. the domains DNS
+hint) may use carloku's numbered-circle affordance.
+
+**Navigation.**
+
+- Top bar (account context): switcher · **Apps · Team · Settings** links,
+ right side: address + personal menu (`Your settings`, sign out) + the
+ existing signout form. Active section gets an accent underline. Signed-out
+ header keeps the `CARLOS console` wordmark (test-pinned).
+- App pages add a horizontal tab bar under the app header: **Overview ·
+ Releases · Instances · Activity · Settings**. Active tab = accent
+ underline (2px). Pure CSS, `flex-wrap` for narrow screens; no media queries
+ required.
+
+**Home (signed-out)** gets the carloku hero treatment: mono eyebrow, big h1
+with accent-colored full stop, lede, primary "Sign in with Keymail" button.
+
+## Information architecture
+
+GET pages split; POST endpoints keep their exact paths and their exact gate
+order (session → account/owner byte-identical 404 → CSRF → `requireFreshAuth`
+LAST).
+
+| Route (GET) | Page | Content |
+|---|---|---|
+| `/a/{sqid}/` | Apps | apps table only + "Create app" button (link) |
+| `/a/{sqid}/apps/new` | Create app | name form; POST already lives at this path |
+| `/a/{sqid}/team` | Team | members table, transfer/remove, invite form, pending invites |
+| `/a/{sqid}/settings` | Account settings | account identity (name, sqid) + danger zone: delete (zero-apps rule and type-the-name intact) |
+| `/a/{sqid}/apps/{app}` | App · Overview | release ladder + live summary (domains OK/instances running chips linking to their tabs) |
+| `/a/{sqid}/apps/{app}/releases` | App · Releases | releases table + promote forms, channels + rollback, canaries + create/retire |
+| `/a/{sqid}/apps/{app}/instances` | App · Instances | instances table + per-host resource sparkline tiles |
+| `/a/{sqid}/apps/{app}/activity` | App · Activity | audit table, cap 10 → 50 |
+| `/a/{sqid}/apps/{app}/settings` | App · Settings | app identity + **Domains & TLS** (Heroku precedent; the tab has real content today) |
+| `/settings` | Your settings | command-line access (cli_tokens content moves here) |
+| `/auth/cli/tokens` | 302 → `/settings` | Keymail notification bodies and cli_approve links keep working |
+
+Existing legacy redirects (`/apps`, `/apps/{app}`) stay. No new legacy
+redirects needed — the old GET URLs (`/a/{sqid}/`, `/a/{sqid}/apps/{app}`)
+remain real pages, just slimmer.
+
+**Overview summary chips** are derived from data already loaded for the app
+page today (domains, instances); no new store queries beyond what each split
+page needs. Each split page loads only its own data — the app page today
+loads everything for every visit; after the split, each handler fetches its
+tab's slice.
+
+## Code structure
+
+**Templates.** `apps.html` splits into `apps.html`, `app_new.html`,
+`team.html`, `account_settings.html`. `app.html` splits into
+`app_overview.html`, `app_releases.html`, `app_instances.html`,
+`app_activity.html`, `app_settings.html`. New shared partials in
+`layout.html` or their own file: `pagehead` (eyebrow/h1/lede), `appnav`
+(tab bar). `ladder.html` unchanged (its markup is heavily test-pinned;
+it gets restyled by CSS only). `cli_tokens.html` becomes `settings.html`
+(personal). All CSS stays in the single `layout.html` `<style>` block.
+
+**Handlers** (`internal/console/`):
+
+- `handleAccountHome` slims to apps-list only.
+- New: `handleNewAppForm` (GET `/a/{sqid}/apps/new`), `handleTeam`,
+ `handleAccountSettings`, `handleSettings` (personal), plus app tab handlers
+ `handleAppReleases`, `handleAppInstances`, `handleAppActivity`,
+ `handleAppSettings`. `handleApp` becomes the Overview.
+- All account-scoped GETs call the existing `account()` / `owner()` /
+ `accountApp()` gates — non-members get byte-identical 404s on every new
+ route. Personal `/settings` is session-gated only (like `/auth/cli/tokens`
+ today): signed-out gets 302 `/`.
+- `accountPage()` gains an `ActiveNav` field; a small `appPage()` helper
+ wraps `accountApp()` + common app-header data + `ActiveTab`.
+- `page` struct gains `ActiveNav`, `ActiveTab`, `Eyebrow`, `Lede` (or
+ equivalent — final shape at implementation).
+- Route registrations grouped and commented in `NewServer`.
+
+**Icons.** New Lucide icons vendored into `icons.go` as needed (e.g.
+`settings`, `users`, `activity`, `globe`) following the existing const+map
+pattern; `TestTemplateIconSlugsAllExist` keeps them honest.
+
+## Error handling
+
+Unchanged in kind: `renderMessage` pages, cookie flashes, redirect-then-render
+for every POST. The `message` template gets the same page-anatomy restyle.
+404-parity: every new account-scoped GET must answer signed-out with 302 `/`
+and non-member with byte-identical `http.NotFound` — asserted per-route in
+tests.
+
+## Testing
+
+- Update the ~110 markup assertions across `server_test.go`,
+ `server_account_test.go`, `ladder_test.go` (CSS-only changes should leave
+ most ladder assertions alone), `authcli_test.go`, `stepup_test.go`
+ (paths unchanged — expect near-zero churn), `icons_test.go` (invariants
+ unchanged; new icons registered).
+- New tests: each new GET route renders for members / 404s byte-identically
+ for non-members / redirects signed-out; tab bar marks the active tab;
+ `/auth/cli/tokens` redirects to `/settings`; create-app GET page renders
+ and its POST still round-trips; team actions still work from `/team`;
+ delete-account still gated behind zero apps + type-the-name + step-up from
+ `/settings`.
+- `go build ./... && go vet ./... && go test ./... -count=1` clean before
+ merge; `gofmt -l` clean on touched files.
+
+## Risks
+
+- **PR #63 (customer fleets)** touches `internal/console/server.go` — expect a
+ route-table rebase whichever lands second. No template overlap.
+- The markup-pinned test suite is the bulk of the work; budget accordingly.
+- Deploy: console is a long-running unit — promote → adopt → **restart
+ console** on both boxes, per standing rule.
+
+## Rollout
+
+One branch, one PR. Screenshots (light + dark) in the PR body. After merge:
+ship → promote → adopt → restart consoles (flagship + tito). Website batch
+untouched (console is not the marketing site).