diff --git a/docs/specs/landing-page-and-doc-reconciliation.md b/docs/specs/landing-page-and-doc-reconciliation.md new file mode 100644 index 0000000..cb0df82 --- /dev/null +++ b/docs/specs/landing-page-and-doc-reconciliation.md @@ -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.