Files
tapir/docs/specs/landing-page-and-doc-reconciliation.md
mathias 6b817f11b9
CI / Lint / Test / Vet (push) Successful in 11s
CI / Build & Import (push) Successful in 10s
CI / Mirror to GitHub (push) Failing after 3s
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.
2026-06-03 19:18:40 +00:00

154 lines
8.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 002006. 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.