diff --git a/README.md b/README.md index 1d24904..96feb82 100644 --- a/README.md +++ b/README.md @@ -27,5 +27,7 @@ complete entry-routing rule (landing / create-storefront / admin), and the INV-1 whole-flow bootstrap test — all skinned to the Claude Design export (`wiggleverse-ecomm-content/ui/designs/ecomm-login-and-create-storefront-designs/`), which also re-skinned the SLICE-2 Landing + Sign-in screens. See -[`docs/BOOTSTRAP.md`](./docs/BOOTSTRAP.md) to run it locally. Deployed environments -(SLICE-4) are next. +[`docs/BOOTSTRAP.md`](./docs/BOOTSTRAP.md) to run it locally. SLICE-4's deploy +contract is in place (versioned `/healthz`, backend-served SPA, `SmtpMailer` with +honest 502 on delivery failure); PPE provisioning + the bootstrap rehearsal are the +remaining SLICE-4 steps (BOOTSTRAP.md's PPE section is the runbook). diff --git a/docs/BOOTSTRAP.md b/docs/BOOTSTRAP.md index 0d0c503..cd0d7c7 100644 --- a/docs/BOOTSTRAP.md +++ b/docs/BOOTSTRAP.md @@ -4,8 +4,9 @@ Bringing an environment from **empty persistence** to "first merchant, first storefront" through the product flows alone (SD-0001 BUC-5). Empty is a working state (INV-1): the app applies its own schema migrations at startup; there is no seed step. -This document grows per environment. SLICE-1 ships the **localhost** section; the -pre-production and production sections land with SLICE-4. +This document grows per environment. SLICE-1 shipped the **localhost** section; +SLICE-4 adds **pre-production (PPE)**. The production section lands with the prod +stand-up. ## Localhost @@ -61,4 +62,67 @@ environment starts from (BUC-5a). `scripts/dev.sh` sets `ECOMM_DATABASE_URL` to the local compose DSN (`postgresql://ecomm:ecomm@localhost:5432/ecomm`). No deployment shape is baked into the app (INV-8); deployed environments supply this and other config from Secret -Manager (SLICE-4). +Manager (see PPE below). + +## Pre-production (PPE) + +PPE is stood up and deployed through the **launch-app / flotilla-core** suite only +(handbook §8.5 — provisioning is an operator-run gesture). The app's deployment +record is `deployment.toml` at this repo's root; flotilla-core consumes it. + +### One-time provisioning (operator-run, in order) + +1. **Cloud foundation** — `scaffold-gcp-project`: the GCP project, its dedicated + `--no-activate` gcloud config, billing, core APIs, ADC quota pin. +2. **Managed database** — `provision-datastore` (launch-app §6.6a, built for ecomm's + D-7/D-8): Cloud SQL for PostgreSQL 16, private-IP-only, automated backups + + point-in-time recovery; writes the full DSN to Secret Manager as + `/ecomm-ppe-database-url`. Bound on the deployment record as + `ECOMM_DATABASE_URL`. +3. **Secrets** (references only — bytes go in via stdin, never through a session): + `ECOMM_SESSION_SECRET` (fresh random), `ECOMM_SMTP_PASSWORD` (the shared + Wiggleverse relay credential), and flotilla's own Gitea read token for this + private repo. +4. **VM + edge** — `provision-vm`: the e2-micro, IAP-only SSH, nginx + Cloudflare + origin TLS, the systemd unit, DNS for `ecomm-ppe.wiggleverse.org`. +5. **Deployment record** — `flotilla-core deployment scaffold ecomm … -o + deployment.toml`, validate, commit it here, `deployment import --reconcile`. + +### Deploy + +Each release is a git tag `v` matching the repo-root `VERSION` file (the +pin). The one gesture, from the flotilla-core repo's venv: + +```bash +CLOUDSDK_ACTIVE_CONFIG_NAME=ecomm .venv/bin/flotilla-core deploy ecomm +``` + +Nine phases, fail-stop; it ends by polling `https://ecomm-ppe.wiggleverse.org/healthz` +and requiring `status == "ok"` and `version == `. Watch it yourself the same +way: + +```bash +curl https://ecomm-ppe.wiggleverse.org/healthz +``` + +### Configuration surface (PPE values) + +| Env var | Kind | Value | +| --- | --- | --- | +| `ECOMM_DATABASE_URL` | secret ref | `/ecomm-ppe-database-url` (from provision-datastore) | +| `ECOMM_SESSION_SECRET` | secret ref | `/ecomm-ppe-session-secret` | +| `ECOMM_SMTP_PASSWORD` | secret ref | the shared Wiggleverse relay credential | +| `ECOMM_MAILER` | overlay | `smtp` | +| `ECOMM_COOKIE_SECURE` | overlay | `1` | +| `ECOMM_SMTP_HOST` / `_PORT` / `_USER` / `_FROM` | overlay | the relay coordinates (non-secret) | + +### The rehearsal (PUC-11) + +From empty persistence: the deploy migrates the schema at startup (INV-7); then a +real sign-up → one-time code arriving by **real email** → create storefront → admin, +through the public flows alone. Record each rehearsal here when it happens. + +## Production + +Lands with the prod stand-up — the **identical gesture** on a prod deployment record +(BUC-5a): same provisioning skills, same deploy, same rehearsal. diff --git a/docs/superpowers/plans/2026-06-10-slice-4-deploy-contract.md b/docs/superpowers/plans/2026-06-10-slice-4-deploy-contract.md new file mode 100644 index 0000000..1f376ec --- /dev/null +++ b/docs/superpowers/plans/2026-06-10-slice-4-deploy-contract.md @@ -0,0 +1,55 @@ +# SLICE-4 (part 1) — Deploy-Contract Code Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make ecomm deployable by flotilla-core's 9-phase gesture and rehearsable on PPE: versioned health, SPA served by the backend, real `SmtpMailer` with honest delivery failure (closes ecomm#7), per SD-0001 §7.2 (SLICE-4) — the code half. Provisioning (Cloud SQL via the new launch-app `provision-datastore`, VM, deployment.toml) and the PPE rehearsal follow as operator gestures in the same session. + +**Anchor:** SD-0001 §7.2 SLICE-4 (R2a, checked this session). flotilla contract: flotilla-core SPEC §8.1 (checkout `v` tag → pip → `npm ci && npm run build` → write `backend/.env` → restart → verify `body.version == target && body.status == "ok"`); provision-vm: nginx proxies ALL routes to uvicorn `app.main:app` (WorkingDirectory `backend/`), so the backend must serve `frontend/dist`. + +**Architecture:** `VERSION` at repo root is the single version source (healthz body + FastAPI version + the deploy pin). `create_app()` mounts `frontend/dist` (when present) after all API routes — SPA fallback via `StaticFiles(html=True)`. `SmtpMailer` is the second adapter of the existing mailer port (STARTTLS smtplib, config from `ECOMM_SMTP_*`, INV-8); delivery failure raises `MailerError` → `accounts.request_code` sends **before** commit (rollback on failure — no orphan code, no tripped cooldown; ecomm#7) → BFF surfaces `502 delivery_failed` (INV-9). + +**Tech Stack:** stdlib `smtplib`/`email.message`; FastAPI `StaticFiles`; no new dependencies. + +--- + +### Task 1: VERSION + versioned /healthz + +**Files:** Create `VERSION` (root). Modify `backend/app/main.py`, `backend/tests/test_healthz.py`. + +- [ ] Write `VERSION` containing `0.4.0`. +- [ ] Test first: `test_healthz_ok_on_migrated_empty_db` asserts `{"status": "ok", "version": "0.4.0"}` read from the VERSION file (compare against `(repo_root/"VERSION").read_text().strip()`, not a literal). Run → FAIL. +- [ ] `main.py`: add `_APP_VERSION = (Path(__file__).resolve().parents[2] / "VERSION").read_text().strip()` (fallback `"0.0.0"` when missing); healthz returns `{"status": "ok", "version": _APP_VERSION}`; `FastAPI(version=_APP_VERSION)`. Run → PASS. Commit. + +### Task 2: backend serves the SPA (deploy phase-8 contract) + +**Files:** Modify `backend/app/main.py`. Test `backend/tests/test_static_spa.py`. + +- [ ] Test first: `create_app(database_url=..., static_dir=tmp_path)` with a `tmp_path/index.html`; GET `/` → 200 + the html; GET `/healthz` and `/api/auth/me` still answer JSON (API wins over the mount). Default `static_dir=None` → resolves `repo_root/frontend/dist`, skipped silently when absent (dev: Vite serves). Run → FAIL. +- [ ] `create_app(database_url=None, static_dir: str | Path | None = None)`; after the last route: resolve dir, `if dir/index.html exists: app.mount("/", StaticFiles(directory=dir, html=True), name="spa")`. Run → PASS (whole suite). Commit. + +### Task 3: SmtpMailer + config surface (INV-8) + +**Files:** Modify `backend/app/platform/{mailer,config}.py`. Test `backend/tests/test_mailer.py` (extend). + +- [ ] Config additions: `smtp_host()` (`ECOMM_SMTP_HOST`), `smtp_port()` (`ECOMM_SMTP_PORT`, 587), `smtp_user()`, `smtp_password()`, `smtp_from()` (default = user), `smtp_starttls()` (default on). +- [ ] Tests first: `MailerError` exists; `build_mailer("smtp")` returns `SmtpMailer` wired from env (monkeypatched); `SmtpMailer.send` drives a monkeypatched `smtplib.SMTP` (starttls → login → send_message with To/Subject/From + body) and never logs the body; SMTP exception → `MailerError`. Run → FAIL. +- [ ] Implement: `MailerError(Exception)`; `SmtpMailer` (EmailMessage; `smtplib.SMTP(host, port, timeout=10)`, STARTTLS per config, login when user set, `send_message`; `except Exception → raise MailerError`; logs only `smtp sent to=` — §6.6 log hygiene); `build_mailer("smtp")` builds it from config. Run → PASS. Commit. + +### Task 4: honest delivery failure — send-before-commit + 502 (closes ecomm#7) + +**Files:** Modify `backend/app/domains/accounts/{errors,service,__init__}.py`, `backend/app/main.py`. Test `backend/tests/test_accounts_request_code.py` + `test_auth_endpoints.py` (extend). + +- [ ] Tests first: a mailer whose `send` raises `MailerError` → service raises `accounts.DeliveryFailed`, **no `auth_code` row remains**, and an immediate retry is **not** cooldown-blocked; endpoint test: 502 `{"error": {"code": "delivery_failed"}}`. Run → FAIL. +- [ ] Implement: `DeliveryFailed(AccountsError)`; `request_code` moves `conn.commit()` **after** `mailer.send(...)`, wrapping send in `try/except MailerError → conn.rollback(); raise DeliveryFailed`; BFF maps it to `_error(502, "delivery_failed", "We couldn't send the code — try again.")`. Run → PASS (whole backend suite). Commit. + +### Task 5: BOOTSTRAP.md PPE section + housekeeping + gate + +**Files:** Modify `docs/BOOTSTRAP.md`, `README.md`, `frontend/package.json` (0.4.0). + +- [ ] BOOTSTRAP.md: replace the "land with SLICE-4" sentence; add **Pre-production (PPE)** section — prerequisites (suite-run provisioning: scaffold-gcp-project → provision-datastore (Cloud SQL, engineering#46) → provision-vm → define-deployment/import; secrets as references), the one deploy gesture (`CLOUDSDK_ACTIVE_CONFIG_NAME= flotilla-core deploy `), how to watch `/healthz`, the rehearsal walk (PUC-11), reset-to-empty note; Prod section: placeholder "lands with the prod stand-up" honestly. +- [ ] README status: deploy contract in place; frontend package 0.4.0. +- [ ] `./scripts/check.sh` → all green. Commit. Push, PR citing SD-0001 §7.2 SLICE-4 + ecomm#7, merge, **tag `v0.4.0` on the merge commit and push the tag** (the deploy pin). + +## Self-review + +Spec coverage: SmtpMailer + config wiring INV-8 ✓ (T3), §6.6 hardening — Secure cookies already config-driven, log hygiene ✓ (T3 no-body logging; LogMailer is dev-only by config), `deployment.toml` + provisioning deliberately deferred to the Phase-C suite gestures (needs the GCP project id that scaffold-gcp-project mints), BOOTSTRAP.md PPE ✓ (T5), versioned health for the §8.1 verify ✓ (T1), SPA serving for the §2 topology ✓ (T2), ecomm#7 ✓ (T4). E2E browser tests still deferred per §6.8. Type consistency: `MailerError` lives in platform/mailer; `DeliveryFailed` in accounts errors; both exported via package surfaces. diff --git a/frontend/package.json b/frontend/package.json index 11eda0d..4103db0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "wiggleverse-ecomm-frontend", "private": true, - "version": "0.3.0", + "version": "0.4.0", "type": "module", "scripts": { "dev": "vite",