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>
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). |