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