You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

84 lines
4.7 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# pages — Planning
## Phases
| Phase | Content | Status |
|---|---|---|
| 0 | Security analysis + design (`plans/2026-07-12-pages-design.md`) | done |
| 1 | v0 implementation on branch `pages-v0` (beans below) | done |
| 2 | Deploy + manual QA against real Cloudflare account | deployed; manual QA (login/upload/delete) pending |
| 3 | v1 candidates: per-page subdomains, page overwrite, Yew UI, OpenAPI | not started |
## v0 tickets (beans, serial)
1. scaffold — crate skeleton, wrangler.toml, hello-world worker builds
2. core — slug/mime/upload/header policy modules + unit tests
3. db — D1 schema + query layer
4. auth — GitHub OAuth, sessions, auth middleware
5. api — pages + users JSON API
6. serve — page content serving from R2
7. frontend — embedded management UI
8. infra — OpenTofu + deployment docs
## Work log
- **2026-07-12** — Project initialized. Security analysis and v0 design
written; beans created; implementation pipeline started (Sonnet
implementation agents, Opus review agents, one commit per ticket).
- **2026-07-13** — infra ticket (pages-ihlp) done: `infra/*.tf` (R2 bucket,
D1 database, DNS placeholder record, rate limiting ruleset) mirroring
`../quotesdb/infra` style, `infra/README.md` deployment guide, `tofu fmt`
+ `tofu validate` passing.
- **2026-07-13** — v0 implementation complete: all 8 beans done on
`pages-v0`, one reviewed commit per ticket (Sonnet implementation, Opus
review). 142 unit tests + 17 doctests; both-target clippy clean. Next:
phase 2 (real deploy + manual QA per `infra/README.md`).
- **2026-07-13** — Deployed. tofu applied R2 + D1 + DNS (rate-limit ruleset
deliberately NOT applied — quotesdb's live http_ratelimit ruleset owns
the zone's single slot; merge required if wanted). Remote schema applied;
GITHUB_CLIENT_SECRET secret set; route pages.elijah.run/* live. Smoke
tests pass (/, /api/me, /auth/login 302, reserved + unknown slugs 404
with sandbox CSP). Pending manual QA: GitHub login as admin, upload,
view, delete.
- **2026-07-13** — pages-shqc: per-page/per-user "trusted" escape hatch from
the CSP sandbox (`pages.trusted OR owner's users.trusted`, computed at
serve time). `migrations/0002-trust.sql` added for the live DB (not yet
applied remotely — manual step); schema.sql, db.rs, api.rs, serve.rs,
core/headers.rs, and the admin UI all updated; 148 unit + 18 doctests,
both-target clippy clean.
- **2026-07-14** — pages-ytau: in-place page updates, retiring the design's
"no page overwrite: delete + re-upload" scope cut. New `PUT
/api/pages/:slug` (design §4.6b) replaces a page's content — owner-or-admin
*and* `can_upload`, new objects written before stale keys are pruned, D1
stats refreshed while `owner_id`/`trusted`/`created_at` are preserved. No
schema change. The UI's always-visible upload card became a modal driven by
a "+ New Page" button and a per-row "Update" button (slug pre-filled,
read-only). 152 unit + 19 doctests, both-target clippy clean. Verified end
to end against `wrangler dev` with local D1/R2 (create → serve → replace →
stale assets 404 → stats/trust/ownership preserved; 401/403/404/400/413
paths) and the UI driven in jsdom (37 assertions).
Local-dev gotcha found while verifying: `wrangler dev` rewrites the request
`Origin` to the `[[routes]]` host (`pages.elijah.run`), so a local
`BASE_URL=http://localhost:8787` makes every mutating route 403 on the
Origin check. Set `BASE_URL=http://pages.elijah.run` in `.dev.vars` when
exercising the API locally.
- **2026-07-21** — pages-9lsk: page **aliases** (design §4.1). A page keeps
one canonical `slug` and gains up to 10 removable, long-lived alternate
names in the same URL namespace. New `aliases` table
(`migrations/0003-aliases.sql`, idempotent `CREATE TABLE IF NOT EXISTS`) +
`idx_aliases_slug`; namespace uniqueness across `pages.slug`+`aliases.alias`
is enforced in the handlers (SQLite can't express a cross-table rule).
Serving is **serve-direct**: `routes::serve` falls back to
`Db::get_page_by_alias` on a canonical miss and serves the canonical page's
R2 content under its `<slug>/` prefix — no redirect, no second copy, and a
real page always wins over an alias of the same name. New `POST
/api/pages/:slug/aliases` (can_upload + owner-or-admin) and `DELETE
/api/pages/:slug/aliases/:alias` (owner-or-admin); `create_page` now rejects
a name held by an alias; `delete_page` removes a page's aliases;
`GET /api/pages` returns each page's `aliases[]`. Management UI shows alias
chips (with × to remove) and an "Add alias" button. No R2/behavioral change
to existing pages. Replaced the earlier rename+conditional-redirect idea
(dropped — 301-cache fragility). 196 unit + 20 doctests, both-target clippy
clean.