# Roadmap: Bigscreen Website — Page Setup & Publishing Flow v1.0

## Overview

This milestone introduces a dynamic page-publishing pipeline so a non-engineer marketer (Max) can publish and update standalone webpages to live `bigscreenvr.com/<path>` URLs via a CLI + Claude Code skill — without touching Jenkins or Builder.io. The journey runs bottom-up across four coarse phases: (1) close six external-system verification gaps and stand up the backend registry + OAuth contract; (2) build the `bsweb` CLI in parallel with CloudFront/S3 infrastructure so the publish pipeline reaches S3 end-to-end; (3) wire the SPA's new dynamic dispatcher above the existing regex filter and adapt `bigscreen10` content into `/10years` for a first end-to-end dev deploy; (4) promote everything to production, smoke-test rollback, retire the `bigscreen10` VPS, and ship the Claude Code skill last so it documents real CLI behavior.

**Granularity:** coarse (4 phases)
**Mode:** mvp (vertical slices favored over horizontal layers)
**Parallelization:** enabled (Phase 2 splits CLI + CDN tracks)

## Phases

**Phase Numbering:**

- Integer phases (1, 2, 3): Planned milestone work
- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED)

Decimal phases appear between their surrounding integers in numeric order.

- [ ] **Phase 1: Foundations, Registry & OAuth** - Close six verification gaps, build `site_pages` data model + apps/api endpoints, wire `website:read`/`website:write`/`website:promote` OAuth scopes
- [ ] **Phase 2: CLI & CDN Pipeline** - Ship `bsweb` CLI (login/publish/ls/rollback) in parallel with CloudFront behaviors + S3 bucket + public snapshot endpoint
- [ ] **Phase 2.1: Staging Flow via dev-website hostname discriminator** (INSERTED 2026-05-21) - SPA hostname-discriminator on `dev-website.bigscreencloud.com`; `staging_sha` column on `site_pages`; `bsweb publish` targets staging by default; `bsweb promote` flips staging→prod (gated by `website:promote` scope)
- [ ] **Phase 3: SPA Dispatcher & /10years Dev Deploy** - Add dynamic-route branch above `builderIoFilter`, adapt `bigscreen10` content, achieve first end-to-end publish of `/10years` in dev (staging-first via 2.1, then prod)
- [ ] **Phase 4: Production Cutover & Claude Skill** - Promote to prod via final Jenkins SPA deploy, smoke-test rollback, retire `bigscreen10` VPS, ship Claude Code skill documenting real CLI behavior

## Phase Details

### Phase 1: Foundations, Registry & OAuth

**Goal**: Six external-system assumptions are verified, the `site_pages` data model + write API exist on `apps/admin_api`, the public read snapshot exists on `apps/api`, and OAuth scopes gate every write — all running locally against dev Postgres + LocalStack before any CLI or CDN work begins.
**Mode:** mvp
**Depends on**: Nothing (first phase)
**Requirements**: FND-01, FND-02, FND-03, FND-04, FND-05, FND-06, REG-01, REG-02, REG-03, REG-04, REG-05, REG-06, REG-07, REG-08, AUTH-01, AUTH-02, AUTH-03, AUTH-04
**Success Criteria** (what must be TRUE):

  1. A locked design doc records: CloudFront distribution ID + behavior-ordering plan, OAuth flow choice (PKCE-loopback vs device-flow), `apps/admin_api` reachability path for public-IP traffic, render-layer choice (iframe vs scoped-inject) backed by a 1-day prototype against `bigscreen10`, reserved-paths allowlist, and registry SPOF mitigation
  2. A developer can run the cloud monorepo locally, hit `POST /admin/site/pages/upload-url` and `POST /admin/site/pages/publish` with a `website:write` bearer token, and see a row appear in the `site_pages` table with a matching `site_pages_audit` entry that distinguishes `actor=human` from `actor=agent`
  3. `GET /site/pages.json` on `apps/api` returns the public read snapshot through the bigscreenvr.com CORS umbrella with no admin token required
  4. Every `/admin/site/*` endpoint rejects requests without `requireScopeAndPolicy({ scopes: ["website:write"|"website:read"], policies: [Admin] })`, and a reserved-path write (e.g. `path: "/"`) is rejected server-side with a structured error
  5. Per-actor write rate-limit blocks runaway bulk mutations

**Plans**: 5 plans
Plans:
**Wave 1**

- [x] 01-01-PLAN.md — Walking Skeleton: VERIFICATION-MEMO.md (FND-01..06 closure) + Shadow DOM prototype + DB migration + OAuth scopes

**Wave 2** *(blocked on Wave 1 completion)*

- [x] 01-02-PLAN.md — Foundation modules: SitePagesErrors + Schemas + reservedPaths + Audit + Database + DB unit tests

**Wave 3** *(blocked on Wave 2 completion)*

- [ ] 01-03-PLAN.md — Publish vertical slice: SitePagesPresign + Api (upload-url/publish) + admin_api router + bsweb-cli OAuth client + integration test

**Wave 4** *(blocked on Wave 3 completion)*

- [ ] 01-04-PLAN.md — Admin reads + rate-limit middleware: SitePagesRateLimit + admin list/history/rollback/unpublish handlers + admin_api router (6 routes mounted) + admin-reads integration test

**Wave 5** *(blocked on Wave 4 completion)*

- [ ] 01-05-PLAN.md — Public snapshot + final integration tests: SitePagesPublic + /site/pages.json on apps/api + public snapshot integration test + rate-limit integration test

**Cross-cutting constraints:**

- After the rate-limit block, no audit rows are appended for the rejected requests

### Phase 2: CLI & CDN Pipeline

**Goal**: `bsweb` CLI can authenticate against Arda, content-hash a page directory, upload to a new S3 bucket via presigned URLs, and call the publish endpoint — and the CloudFront distribution serves `/static-pages/*` from S3 (with sha-rewrite) and `/api/site/pages.json` from `apps/api` with correct precedence over the SPA catch-all.
**Mode:** mvp
**Depends on**: Phase 1
**Requirements**: CLI-01, CLI-02, CLI-03, CLI-04, CLI-05, CLI-06, CLI-07, CLI-08, CLI-09, CLI-10, CLI-11, CLI-12, CLI-13, CLI-14, CLI-15, CLI-16, CDN-01, CDN-02, CDN-03, CDN-04, CDN-05, CDN-06, CDN-07, CDN-08, CDN-09
**Success Criteria** (what must be TRUE):

  1. Max (or a developer) can run `bsweb login`, complete the Arda OAuth flow (PKCE-loopback or device-flow per Phase 1 outcome), and have tokens stored in the OS keychain via `@napi-rs/keyring`
  2. `bsweb publish ./<dir>` is idempotent, prints a file/URL/byte-delta diff before applying, is dry-run by default when invoked by an agent, requires `--apply` for live publish, and respects default-deny upload (`.env*`, `*.key`, `node_modules/` etc. excluded)
  3. Destructive ops (`bsweb unpublish <slug>`, `bsweb rollback`) require typed-confirm tokens and produce structured `{code, message, suggestion, docs_url}` errors; every command supports `--json` schema-versioned output and `bsweb schema --json` self-describes the manifest
  4. `curl https://<dev-host>/static-pages/<slug>/<sha>/index.html` returns the uploaded file with correct Content-Type per extension, served from the new `bigscreen-static-pages-prod` OAC-only bucket with CloudFront Function rewriting `<slug>` → `<currentSha>`
  5. The `/static-pages/*` and `/api/site/pages.json` behaviors take precedence over the SPA catch-all; the existing `403/404 → /index.html` error rewrite is scoped to `Accept: text/html` or excluded from `/static-pages/*`; publishing invalidates only `/api/site/pages.json*`

**Plans**: TBD

### Phase 2.1: Staging Flow via dev-website hostname discriminator (INSERTED 2026-05-21)

**Goal**: Enable staff/marketer preview of new and updated pages on `dev-website.bigscreencloud.com/<path>` (existing IP-gated host running the same SPA infra) BEFORE promoting to live `www.bigscreenvr.com/<path>`. Mechanism is web-code + schema only — no new CloudFront, DNS, or S3 bucket. SPA detects hostname, fetches `?env=staging` registry. CLI gains `publish` (staging-targeted by default) + `promote` (atomic staging→prod sha flip, gated by `website:promote` scope).
**Mode:** mvp
**Depends on**: Phase 2
**Requirements**: TBD (to be derived during /gsd-discuss-phase 2.1 — promoted from backlog item 999.1)
**Success Criteria** (what must be TRUE):

  1. Visiting `dev-website.bigscreencloud.com/<path>` on the dev SPA fetches `/api/site/pages.json?env=staging` (instead of prod) via hostname detection, and renders the `staging_sha` content for the matched slug
  2. `site_pages` schema gains `staging_sha TEXT NULL` alongside `current_sha`; audit table `op` CHECK constraint extends to include `'promote'`
  3. `bsweb publish <slug> [path]` writes to `staging_sha` by default; `bsweb publish --prod` is a destructive opt-in that calls promote in one step (gated by `website:promote`)
  4. `bsweb promote <slug>` atomically copies `staging_sha` → `current_sha`, writes audit row `op='promote'` with `actor_claim`, requires bearer token with `website:promote` scope
  5. `apps/api` GET `/site/pages.json?env=staging|prod` returns the appropriate sha pointer per slug; default `env` when omitted is `prod`
  6. S3 bucket layout unchanged — content keyed by sha lives in the same prefix regardless of staging/prod (content is immutable per sha)
  7. End-to-end smoke: publish a test page to staging → verify rendering on dev-website → promote → verify rendering on www, all without Jenkins or Builder.io

**Plans**: TBD
**UI hint**: no
**Promoted from**: Backlog item 999.1 (captured 2026-05-21, promoted 2026-05-21)

### Phase 3: SPA Dispatcher & /10years Dev Deploy

**Goal**: The React SPA fetches the live registry, dispatches matched paths to a new `<DynamicPage>` branch above the existing `builderIoFilter`, and renders `/10years` (adapted from `bigscreen10`) plus one namespace sub-route wrapped in the site nav + footer — proving the end-to-end pipeline works in dev without breaking any existing route.
**Mode:** mvp
**Depends on**: Phase 2
**Requirements**: SPA-01, SPA-02, SPA-03, SPA-04, SPA-05, SPA-06, SPA-07, SPA-08, SPA-09, SPA-10, SPA-11, PAGE-01, PAGE-02, PAGE-03, PAGE-04, PAGE-05, PAGE-06, PAGE-07, PAGE-08
**Success Criteria** (what must be TRUE):

  1. Visiting `/10years` on the dev SPA renders the adapted `bigscreen10` content inside the existing `<Page>` wrapper (Header + Footer), with `react-router-dom ≥6.28` using `createBrowserRouter` + `patchRoutesOnNavigation` and the new dispatcher branch added *above* `builderIoFilter` in `src/App.js`
  2. An E2E smoke test confirms every existing route (`/`, `/about`, `/software`, `/privacypolicy`, `/enrollprivacy`, `/hardwareterms`, `/termsofservice`, `/scans/*`, `/account/*`, `/token2/*`, `/auth/*`, `/browser/*`) still works after the dispatcher change, with documented resolution order: exact-match registry → prefix-match registry → React-owned routes → `builderIoFilter` → 404
  3. The SPA bundles a fallback registry at build time; the live registry *augments* it; a circuit-breaker falls through to built-in routes after two consecutive `/api/site/pages.json` fetch failures, and SWR caching with revalidate-on-focus is active
  4. At least one namespace sub-route (e.g. `/10years/intro`) is reachable, exercising subtree routing; CSS bleed is prevented per the Phase 1 render-layer decision (iframe structural isolation OR publish-time scoped-CSS); asset paths resolve correctly under nested URLs via `<base href>` injection or CLI path rewrite
  5. A test mutation to `/10years` content deploys end-to-end via the new pipeline in dev (no Jenkins, no Builder.io); the `static-pages` repo exists with at least one page and a GitHub Actions workflow that runs optional `build` + invokes `bsweb publish` on merge

**Plans**: TBD
**UI hint**: yes

### Phase 4: Production Cutover & Claude Skill

**Goal**: The OAuth client + scopes, admin_api routes, CloudFront behaviors, and SPA changes are promoted to production (via the final Jenkins SPA deploy needed for this milestone); `/10years` is live on `www.bigscreenvr.com` end-to-end with rollback smoke-tested; the `bigscreen10` VPS is retired; and the Claude Code skill ships last so it documents the CLI's real, shipped behavior.
**Mode:** mvp
**Depends on**: Phase 3
**Requirements**: CUT-01, CUT-02, CUT-03, CUT-04, CUT-05, CUT-06, CUT-07, SKILL-01, SKILL-02, SKILL-03, SKILL-04, SKILL-05
**Success Criteria** (what must be TRUE):

  1. `https://www.bigscreenvr.com/10years` is live end-to-end via the new pipeline (OAuth scopes + client in prod, admin_api + api routes deployed, CloudFront behaviors on the prod distribution, SPA shipped via the final Jenkins deploy) and renders wrapped in the site nav + footer
  2. A test mutation to `/10years` content deploys to production via the new pipeline without invoking Jenkins or Builder.io, satisfying the atomic milestone success criterion
  3. `bsweb rollback <slug> --to <sha>` is smoke-tested in production: the prior revision is restored within seconds via a single `currentSha` flip and `/api/site/pages.json*` invalidation
  4. The `bigscreen10` VPS + nginx + direct GitHub Actions deploy is decommissioned; `www.bigscreenvr.com/10years` is the only live address
  5. `.claude/skills/publish-page/SKILL.md` ships in the `static-pages` repo (Anthropic Agent Skills format) enumerating forbidden operations (never publish to `/`, never `--force`, never edit `reserved-paths.json`), mandating `bsweb validate` + `bsweb publish --dry-run` before live publish, alongside `CLAUDE.md` day-to-day workflow guidance and bundled reference docs (`manifest-schema.md`, `troubleshooting.md`)

**Plans**: TBD

## Progress

**Execution Order:**
Phases execute in numeric order: 1 → 2 → 2.1 → 3 → 4

| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. Foundations, Registry & OAuth | 2/5 | In Progress|  |
| 2. CLI & CDN Pipeline | 0/TBD | Not started | - |
| 2.1. Staging Flow via dev-website hostname discriminator | 0/TBD | Not started (INSERTED 2026-05-21) | - |
| 3. SPA Dispatcher & /10years Dev Deploy | 0/TBD | Not started | - |
| 4. Production Cutover & Claude Skill | 0/TBD | Not started | - |

## Backlog

*(empty — 999.1 promoted to Phase 2.1 on 2026-05-21)*
