diff --git a/.planning/phases/03-event-write-back-pwa-install/03-GATE2-RESULTS.md b/.planning/phases/03-event-write-back-pwa-install/03-GATE2-RESULTS.md new file mode 100644 index 0000000..77b49ec --- /dev/null +++ b/.planning/phases/03-event-write-back-pwa-install/03-GATE2-RESULTS.md @@ -0,0 +1,194 @@ +# Phase 3 Gate 2 — Live Verification Results + +## Header + +| Field | Value | +|--------------|---------------------------------------------------------| +| Deploy URL | TBD — operator must configure (see Operator Setup below)| +| Build SHA | 40dfbb4 | +| Build date | 2026-06-05T22:50:16Z | +| PWA build | CLEAN — 1818 modules, dist/sw.js + workbox generated | +| API build | CLEAN — tsc passed, no errors | + +--- + +## Operator Setup Required Before Gate 2 + +The following steps require operator credentials/access and cannot be automated by the executor. +Complete all steps before proceeding to the checklist below. + +### 1. Register FamilySync as an Authelia OIDC confidential client + +Generate a hashed client secret: + +```bash +authelia crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 +# Record BOTH the plaintext (for OIDC_CLIENT_SECRET) and the hash (for Authelia config). +``` + +Add to Authelia `configuration.yml` under `identity_providers.oidc.clients`: + +```yaml +identity_providers: + oidc: + clients: + - client_id: 'familysync-dev' # Use 'familysync' for Unraid prod (Mode B) + client_name: 'FamilySync' + client_secret: '$pbkdf2-sha512$...' # The HASH from the command above + public: false + authorization_policy: 'one_factor' + redirect_uris: + - 'https://familysync-dev.DOMAIN/callback' # Replace DOMAIN; Mode B: familysync.DOMAIN + scopes: [openid, profile, email] + response_types: [code] + grant_types: [authorization_code, refresh_token] + token_endpoint_auth_method: client_secret_basic + require_pkce: true + pkce_challenge_method: S256 +``` + +Reload Authelia: `docker restart authelia` (or your reload mechanism). + +### 2. Set OIDC_AUTH_EXTERNAL_URL in the app's .env + +`OIDC_AUTH_EXTERNAL_URL` is **mandatory** behind Pangolin. Without it, `@hono/oidc-auth` builds +`redirect_uri` from the internal container hostname, which will not match the registered URI and +will cause a 400 from Authelia. + +```dotenv +OIDC_AUTH_EXTERNAL_URL=https://familysync-dev.DOMAIN # Mode A test rig +# (Mode B: https://familysync.DOMAIN) +OIDC_CLIENT_ID=familysync-dev +OIDC_CLIENT_SECRET= +OIDC_REDIRECT_URI=https://familysync-dev.DOMAIN/callback +``` + +Also ensure: +- `NODE_ENV=production` is set in the container — this forces `devBypassActive=false` in + `apps/api/src/index.ts`, mounting the OIDC guard unconditionally. +- `DEV_AUTH_BYPASS` is **absent** (or unset) from the production environment block. + Even if accidentally present, `NODE_ENV=production` suppresses it at the first conditional + in `devBypass.ts`, but leave it out to keep the config unambiguous. + +### 3. Expose via Pangolin / Newt (Mode A local rig) + +```bash +# Run Newt on your dev box pointing at the Pangolin site token issued for this host: +docker run -d --name newt --restart unless-stopped \ + -e PANGOLIN_ENDPOINT=https://pangolin.DOMAIN \ + -e NEWT_ID=<site-id> -e NEWT_SECRET=<site-secret> \ + fosrl/newt:latest +``` + +In Pangolin, create a route: +- Host: `familysync-dev.DOMAIN` +- Upstream: `http://<api-host>:3000` +- Pangolin's own auth: **OFF** — FamilySync does Authelia OIDC at the app layer. +- Response buffering: **OFF**; idle/read timeout: **>= 120s** (required for SSE). + +### 4. Apply database schema (first deploy only) + +```bash +docker compose up -d mariadb +DB_HOST=127.0.0.1 DB_PORT=3306 DB_USER=familysync DB_NAME=familysync DB_PASSWORD=<value> \ + pnpm --filter @familysync/api exec drizzle-kit push +# Verify: SHOW TABLES; -> users, member_credentials, calendars, calendar_events +``` + +### 5. Bring up the app and confirm /health over the tunnel + +```bash +docker compose up -d --build +# Local sanity: +curl -s http://localhost:3000/health # expect: {"ok":true,"db":"up"} +# Through the tunnel (record this result in the checklist below): +curl -s https://familysync-dev.DOMAIN/health # expect: {"ok":true,"db":"up"} +``` + +Update the Deploy URL at the top of this file once confirmed. + +--- + +## Gate 2 Checklist + +Run the checklist from an **external** network (phone on cellular is ideal). +Mark each row PASS or FAIL and add notes. On failure, apply the indicated remedy and retest. + +### Part A — Auth, Session, Colors (Task 2) + +| # | Ref | Check | Result | Notes | +|---|-----|-------|--------|-------| +| A1 | AUTH-01 | Open `https://familysync-dev.DOMAIN` → redirects to Authelia → login completes → land on the app with name, color, and at least one cached event | [ ] PENDING — operator | | +| A2 | AUTH-02 | Fully close + reopen browser → revisit the URL → no re-login prompted (session persists) | [ ] PENDING — operator | | +| A3 | AUTH-03 | Second member logs in on a separate device → distinct stable color assigned (different from first member's color) | [ ] PENDING — operator/device | | + +### Part B — iOS PWA Standalone Login (Task 2) — LOAD-BEARING CHECK + +> **This is the most critical row.** Pitfall 2: If the OIDC redirect breaks out of standalone mode +> (user lands in Safari instead of the app), the non-technical member cannot log in. Confirm this +> passes before recording any other rows as done. +> +> **Remedy if it fails:** Verify `manifest.webmanifest` has `scope: "/"` and `start_url: "/"`; +> confirm `/callback` is in the service worker denylist (`apps/pwa/src/sw-denylist.ts`) and is not +> intercepted by Workbox; redeploy and retest. + +| # | Ref | Check | Result | Notes | +|---|-----|-------|--------|-------| +| B1 | iOS PWA | Open `https://familysync-dev.DOMAIN` in Safari on iPhone → in-app install walkthrough appears → tap "Add to Home Screen" | [ ] PENDING — device | | +| B2 | iOS PWA | Launch FamilySync from Home Screen → opens full-screen with no Safari browser chrome (standalone mode) | [ ] PENDING — device | | +| B3 | iOS PWA (Pitfall 2) | Complete Authelia OIDC login from standalone mode → redirect does NOT break out of standalone (user stays in the app, not dropped to Safari) | [ ] PENDING — device | **Load-bearing check** | +| B4 | PWA-01 | Installed PWA on iOS opens full-screen with no browser chrome | [ ] PENDING — device | | +| B5 | PWA-02 | Installed PWA on Android opens full-screen with no browser chrome | [ ] PENDING — device | | + +### Part C — SSE Smoke Test (Gate before Phase 4) + +| # | Ref | Check | Result | Notes | +|---|-----|-------|--------|-------| +| C1 | SSE | Hold stream open 5+ min without it being cut (see curl command below) | [ ] PENDING — operator | | + +```bash +# Get the session cookie from browser DevTools → Application → Cookies (oidc-auth=<value>) +curl -N -H "Cookie: oidc-auth=<value>" https://familysync-dev.DOMAIN/api/sse/heartbeat +# PASS: heartbeat event received ~every 10s for 5+ minutes +# FAIL: stream cut early → adjust Pangolin idle-timeout; if still failing, record as Phase 4 constraint +``` + +### Part D — Create / Edit / Delete Fastmail Round-trips (Task 3) + +| # | Ref | Check | Result | Notes | +|---|-----|-------|--------|-------| +| D1 | CAL-04 | Create a timed event → "Syncing…" toast → "Saved" toast → event appears in native Fastmail app on next sync | [ ] PENDING — operator | | +| D2 | CAL-07 | Create an all-day event → same Syncing→Saved flow → appears in Fastmail | [ ] PENDING — operator | | +| D3 | CAL-04 | Create a weekly recurring event → appears in Fastmail | [ ] PENDING — operator | | +| D4 | CAL-05 | Edit an existing event's title and time → Syncing→Saved → change persists in Fastmail | [ ] PENDING — operator | | +| D5 | CAL-06 | Delete an event via the two-tap confirmation dialog → Syncing→Saved → event disappears from all views on next sync | [ ] PENDING — operator | | +| D6 | D-08 | (Optional) Trigger a 412 conflict by editing the same event in Fastmail first → conflict toast appears in the app → calendar re-fetches | [ ] PENDING — operator | Optional | + +--- + +## /health Tunnel Verification + +Record the curl result through the public URL here: + +``` +URL tested: https://familysync-dev.DOMAIN/health +Result: [ ] PENDING — operator +Response body: <fill in> +``` + +--- + +## Summary + +| Section | Status | +|---------|--------| +| Production builds (PWA + API) | CLEAN (automated, 2026-06-05) | +| Operator infra setup | PENDING | +| A — Auth / session / colors | PENDING | +| B — iOS standalone login (load-bearing) | PENDING | +| C — SSE smoke test | PENDING | +| D — Fastmail write round-trips | PENDING | + +Gate 2 is complete when all rows are PASS. Record final status here: + +**Gate 2 outcome:** [ ] PENDING