docs: add landing-page + doc-reconciliation build spec
Workstream A: public /welcome landing page (bubbletea aesthetic, one Dex login flow, logged-in shortcuts) — with the oidc.go facts verified against main, incl. the two corrections that only surface from reading the code (logout must redirect to /welcome not /auth/login; bare-/ vs deep-link redirect split). Workstream B: reconcile the guardrail docs against deployed reality (v0.4.0) — auth.go comments, data-model isolation status + migrations 002-006 schema, architecture web surface, use-case scenarios for the Stage-1 features, ADR ordering, and a requirements-vs-shipped deviation check. Structured as two parallel workstreams so the doc audit isn't done cursorily alongside the build.
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
# Spec — Landing page + documentation reconciliation
|
||||
|
||||
**Date:** 2026-06-03
|
||||
**Status:** Ready to build
|
||||
**Scope:** Two parallel workstreams — (A) a public landing page; (B) reconciling the
|
||||
requirements / use-case / architecture / data-model docs against the deployed reality
|
||||
(v0.4.0). These are separate concerns; do not let one worker do both, or the audit gets
|
||||
done cursorily.
|
||||
|
||||
All work: read `CLAUDE.md` + `DECISIONS.md` first. TBD — commit directly to `main`, one
|
||||
logical change per commit, conventional commits, `task check` green before every commit.
|
||||
After editing any `.templ`, run `templ generate` (the repo commits both `views.templ` and the
|
||||
generated `views_templ.go`).
|
||||
|
||||
---
|
||||
|
||||
## Workstream A — Public landing page
|
||||
|
||||
### Goal
|
||||
A public landing page at `/welcome`, in the established bubbletea aesthetic, that lets a
|
||||
visitor sign in (one Dex flow) and, if already logged in, jump to their Tapir page or log out.
|
||||
New public transport surface only — no engine/core change (ADR-003).
|
||||
|
||||
### Verified facts (read from `internal/web/oidc/oidc.go` @ main — do not re-guess)
|
||||
- Auth endpoints are exactly `/auth/login`, `/auth/callback`, `/auth/logout`.
|
||||
- `isPublicPath(p)` = `p == "/healthz" || strings.HasPrefix(p, "/auth/")` — the single
|
||||
public-route chokepoint inside `DexAuth.Middleware`.
|
||||
- `DexAuth.CurrentUser(r) (web.User, bool)` reads the session cookie and does NOT redirect —
|
||||
this is the "peek" the landing page uses to branch logged-in vs logged-out.
|
||||
- `handleCallback` redirects to `/` on success (correct — leave as-is).
|
||||
- `handleLogout` currently redirects to `loginPath` (`/auth/login`) — this is wrong for this
|
||||
feature (see A3).
|
||||
- There is NO separate "sign up" against Dex/OIDC: one authorization flow. Registration is
|
||||
Tapir's own `/register` step (ADR-012), reached after first login for an unknown subject.
|
||||
|
||||
### Tasks
|
||||
**A1 — make `/welcome` public.** In `oidc.go`, extend `isPublicPath`:
|
||||
```go
|
||||
func isPublicPath(p string) bool {
|
||||
return p == "/healthz" || p == "/welcome" || strings.HasPrefix(p, "/auth/")
|
||||
}
|
||||
```
|
||||
|
||||
**A2 — unauthenticated bare-`/` → `/welcome`; deep links unchanged.** In `DexAuth.Middleware`,
|
||||
the unauthenticated branch currently always calls `redirectToLogin`. Change it so that when
|
||||
`r.URL.Path == "/"` an unauthenticated visitor is redirected to `/welcome`; for any other
|
||||
guarded path keep `redirectToLogin` (so a shared `/v/{id}` deep link still bounces through Dex
|
||||
and returns to the destination). Keep the `isPublicPath` check first (redirect-loop guard).
|
||||
|
||||
**A3 — logout lands on `/welcome`, not login.** In `handleLogout`, change the final redirect
|
||||
from `loginPath` to `/welcome`. As written it sends the user to `/auth/login`, which
|
||||
immediately starts a fresh Dex login — visibly failing to log out. This intentionally breaks
|
||||
the existing logout test (oidc_test.go) which asserts redirect to `/auth/login`; update that
|
||||
test to expect `/welcome`. That break is expected, not a regression.
|
||||
|
||||
**A4 — mount the landing handler** in `internal/web/handlers.go` `Router()`, on `root`,
|
||||
OUTSIDE `Auth.Middleware`, alongside `/healthz`:
|
||||
```go
|
||||
root.HandleFunc("GET /welcome", a.handleWelcome)
|
||||
```
|
||||
`handleWelcome` peeks `a.Auth.CurrentUser(r)` and renders `WelcomePage(user, ok)`. Not behind
|
||||
`Auth.Middleware` or `registrationGate`.
|
||||
|
||||
**A5 — `WelcomePage` templ component** in `views.templ`. Reuse the existing shared
|
||||
layout/header partial and the established aesthetic (#7653FC purple rounded ╭─╮╰─╯ box, pink
|
||||
tapir mascot, #0EF9B6 mint accents) — match the existing pages, do not reinvent styling.
|
||||
- Logged out (`ok == false`): tapir mascot + tagline; one primary CTA **"Get Started"** →
|
||||
`/auth/login`; honest sub-text: "New here? You'll set up your account right after signing in
|
||||
— returning users go straight through." One button only (see verified facts: no separate
|
||||
Dex sign-up; two buttons to the same URL would mislead).
|
||||
- Logged in (`ok == true`): "Go to my Tapir" → `/`; "Log Out" → `/auth/logout`. May greet via
|
||||
`user.Email`.
|
||||
|
||||
**A6 — tests** (extend `handlers_test.go` patterns). Note `StubAuth.CurrentUser` always returns
|
||||
true; for the logged-out case use a fake Auth returning `(web.User{}, false)`.
|
||||
- `GET /welcome`, no session → "Get Started" → `/auth/login`.
|
||||
- `GET /welcome`, with session → "Go to my Tapir" + "Log Out".
|
||||
- Unauthenticated `GET /` → 302 `/welcome`.
|
||||
- Unauthenticated `GET /v/{id}` → still 302 `/auth/login` (deep link preserved).
|
||||
- Authenticated `GET /` → still serves the list, unchanged.
|
||||
- oidc: `handleLogout` → 302 `/welcome` (update the existing test).
|
||||
|
||||
**A out of scope:** no Dex config change, no new auth/session logic, no sign-up backend.
|
||||
|
||||
---
|
||||
|
||||
## Workstream B — Documentation reconciliation
|
||||
|
||||
### Why
|
||||
The guardrail docs were written before Stage 1 and the web surface. Several now describe the
|
||||
opposite of the deployed reality (v0.4.0). Stale guardrail docs are worse than none — a future
|
||||
cold session (human or agent) trusts them. This workstream brings requirements, use cases,
|
||||
architecture, and data-model back in sync with `main`. Each fix is one commit; cite the ADR or
|
||||
migration that is the source of truth.
|
||||
|
||||
### Known drift to fix (verified this session — not exhaustive; the worker confirms against code)
|
||||
**B1 — `internal/web/auth.go` comments.** The `User.Subject` doc and package doc still say
|
||||
"single-user allowlist (ADR-011)" / "Stage-0". Code is multi-user (ADR-012). Update the
|
||||
comments to describe the current multi-user reality; reference ADR-012.
|
||||
|
||||
**B2 — `docs/data-model.md` isolation status.** It says isolation enforcement is "dormant at
|
||||
Stage 0". It is now LIVE: Postgres RLS, `FORCE`d on all user-owned tables, with a passing
|
||||
two-user isolation test (ADR-012, migration 003). Rewrite that section to describe enforced
|
||||
RLS as the current state; keep the history honest (was dormant at Stage 0, enforced from
|
||||
Stage 1).
|
||||
|
||||
**B3 — `docs/data-model.md` schema completeness.** The doc predates migrations 002–006. Add
|
||||
the entities/columns that now exist: `summary_actions` (002), RLS (003), `user_identities`
|
||||
(004, dex_subject→user_id), `video_connections` (005), `users.auto_summarize` +
|
||||
`videos.summarize_requested` (006). The ER section should match the live schema. Cross-check
|
||||
against `internal/adapters/store/migrations/*.up.sql` — those are ground truth.
|
||||
|
||||
**B4 — `docs/architecture/architecture.md`.** Predates the entire web surface. Update the C4
|
||||
container diagram and text to include: `tapir serve` (HTMX+Templ web reader/writer), the Dex
|
||||
OIDC session layer (`internal/web/oidc`), registration gate, web-initiated YouTube connect,
|
||||
account management, and the immediate-processing path (web "Summarize" button → background
|
||||
goroutine → status poll). The engine/ports/sinks core is unchanged (ADR-003) — show the web
|
||||
surface as a new transport over the same core, not a core change.
|
||||
|
||||
**B5 — `docs/use-cases/*.feature`.** Add scenarios for the behaviours now live and unspecced:
|
||||
register (new subject → registration → user row; returning user straight through), connect
|
||||
YouTube (web OAuth), disconnect, delete-account (cascade + secret purge, Dex untouched —
|
||||
ADR-013), manual-vs-auto summarize mode + the Summarize button, and the landing page
|
||||
(logged-out CTA; logged-in shortcuts). Keep them as executable-style Gherkin consistent with
|
||||
the existing files.
|
||||
|
||||
**B6 — `DECISIONS.md` ADR ordering (cosmetic).** ADR-010 sits before ADR-009/011 (append
|
||||
order). Reorder to numeric while you're in the file. Pure tidy, no content change.
|
||||
|
||||
**B7 — requirements check.** If a requirements doc exists (e.g. `docs/ui-spec.md`, referenced
|
||||
by ADR-011), reconcile it with what shipped: note where the build deviated (e.g. the spinner /
|
||||
immediate processing / summarize mode were beyond the original spec) so the spec reflects
|
||||
reality or explicitly records the deviation. Do not silently rewrite history — record
|
||||
deviations as deviations.
|
||||
|
||||
### B working method
|
||||
- Source of truth order: migrations + code > ADRs > prose docs. When a prose doc disagrees
|
||||
with code, the code wins and the doc is corrected (unless the code is the bug — then flag it,
|
||||
don't quietly doc around it).
|
||||
- One logical doc per commit. Cite the ADR/migration that justifies each change in the commit
|
||||
body.
|
||||
- This is an audit, not a rewrite: preserve the docs' structure and the "rejected alternatives
|
||||
/ history" honesty. The goal is *current and trustworthy*, not *pretty*.
|
||||
|
||||
---
|
||||
|
||||
## Coordination
|
||||
A and B touch mostly different files (A: oidc.go, handlers.go, views.templ, tests; B: docs/* +
|
||||
auth.go comments). The one overlap is `auth.go` (B1 edits comments) vs A (reads it) — no
|
||||
conflict. Run A and B in parallel; commit independently to `main`.
|
||||
|
||||
If anything in B reveals that code, not docs, is wrong (e.g. an isolation gap, a migration that
|
||||
doesn't match the data-model intent), STOP and surface it — that's a finding, not a doc edit.
|
||||
Reference in New Issue
Block a user