A3 PASS (distinct colors after collision fix), B1 PASS (wife installed + logged in on iPhone). Added backlog 999.5: first-login provider-setup prompt to attach each member's own Fastmail app password (per-member personal calendar, D-09). Remaining Gate 2: A2, B2/B3/B4, D2, D3 (quick operator confirms); B5/C deferred.
208 lines
12 KiB
Markdown
208 lines
12 KiB
Markdown
# Phase 3 Gate 2 — Live Verification Results
|
|
|
|
## Header
|
|
|
|
| Field | Value |
|
|
|--------------|---------------------------------------------------------|
|
|
| Deploy URL | LIVE via Pangolin/Newt (operator domain) — confirmed reachable; real Authelia OIDC login working 2026-06-07 |
|
|
| Build SHA | 86069b8 (2026-06-07 live bring-up + write-path fixes) |
|
|
| Build date | 2026-06-07 |
|
|
| PWA build | CLEAN — dist/sw.js + workbox generated; 140/140 tests |
|
|
| API build | CLEAN — tsc passed; 102/102 tests |
|
|
|
|
> **2026-06-07 live verification note.** Gate 2 was executed live against the running
|
|
> Docker stack through Pangolin/Newt (Mode A). Several blocker bugs were found and fixed
|
|
> during this session (see commits): newt MTU blackhole, OIDC state-cookie churn, event
|
|
> write-path timezone + calendar identity, missing calendars join (edit/delete 503),
|
|
> delete cache-reconciliation, post-write refetch race, and a calendar remount flash.
|
|
> Rows verified below were confirmed via operator browser testing + backend evidence
|
|
> (calendar_outbox rows reaching `done` against caldav.fastmail.com). playwright-cli is
|
|
> unavailable in this WSL2 env, so desktop rows were operator-driven, not automated.
|
|
|
|
---
|
|
|
|
## 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=<plaintext from the crypto hash step>
|
|
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 | ✅ PASS (2026-06-07) | Real Authelia OIDC login lands on the calendar; name (email claim), assigned color, and cached events render. Name self-heals to full name once Authelia emits name/preferred_username (see backlog/memory). |
|
|
| A2 | AUTH-02 | Fully close + reopen browser → revisit the URL → no re-login prompted (session persists) | [ ] PENDING — operator | Refresh-token session wired (offline_access). Operator must confirm across a full browser restart within the session lifespan. |
|
|
| A3 | AUTH-03 | Second member logs in on a separate device → distinct stable color assigned (different from first member's color) | ✅ PASS (2026-06-07) | Second member (amelia, id=3) logged in on her iPhone. Found + fixed a collision bug (both members were #E8734A — COUNT%palette reused a slot after a deletion); now luc=#E8734A, amelia=#4A90D9 (distinct, stable). Fix: first-unused-palette-color (commit f700182). |
|
|
|
|
### 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" | ✅ PASS (2026-06-07) | Wife added FamilySync to her iPhone Home Screen and logged in (user id=3 created). |
|
|
| B2 | iOS PWA | Launch FamilySync from Home Screen → opens full-screen with no Safari browser chrome (standalone mode) | 🟡 LIKELY — confirm | Installed to Home Screen; operator to confirm it launches full-screen with no Safari chrome. |
|
|
| 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) | 🟡 CONFIRM | She logged in successfully; confirm the login was completed *launched from the Home Screen icon* (standalone) and did not drop to Safari. **Load-bearing check.** |
|
|
| B4 | PWA-01 | Installed PWA on iOS opens full-screen with no browser chrome | 🟡 LIKELY — confirm | Same as B2. |
|
|
| B5 | PWA-02 | Installed PWA on Android opens full-screen with no browser chrome | [ ] PENDING — device | Android install not yet exercised. |
|
|
|
|
### 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 | ✅ PASS (2026-06-07) | Timed create round-trips to caldav.fastmail.com (outbox rows reach `done`); appears in the app. Timezone fix applied (was 4h off). |
|
|
| D2 | CAL-07 | Create an all-day event → same Syncing→Saved flow → appears in Fastmail | [ ] PENDING — operator | Timed create path verified; all-day not yet exercised live. Quick confirm. |
|
|
| D3 | CAL-04 | Create a weekly recurring event → appears in Fastmail | [ ] PENDING — operator | Not yet exercised live. Quick confirm. |
|
|
| D4 | CAL-05 | Edit an existing event's title and time → Syncing→Saved → change persists in Fastmail | ✅ PASS (2026-06-07) | Edit/move confirmed working; update outbox rows reach `done`; post-write refetch race fixed so the change shows without manual refresh. |
|
|
| D5 | CAL-06 | Delete an event via the two-tap confirmation dialog → Syncing→Saved → event disappears from all views on next sync | ✅ PASS (2026-06-07) | Delete confirmed working; delete cache-reconciliation fix means the event leaves the cache/UI (was lingering as a ghost). |
|
|
| 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 | ✅ PASS (2026-06-07) | Observed live: a stale-etag update produced `412 conflict` (outbox id=7) and the "This event changed elsewhere" conflict toast; calendar re-syncs. |
|
|
|
|
---
|
|
|
|
## /health Tunnel Verification
|
|
|
|
Record the curl result through the public URL here:
|
|
|
|
```
|
|
URL tested: https://<operator-domain>/health (via Pangolin/Newt) + http://localhost:3000/health
|
|
Result: ✅ PASS (2026-06-07) — app reachable through the tunnel; real OIDC login completed
|
|
Response body: {"ok":true,"db":"up"}
|
|
```
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
| Section | Status |
|
|
|---------|--------|
|
|
| Production builds (PWA + API) | ✅ CLEAN (2026-06-07; 102 API + 140 PWA tests) |
|
|
| Operator infra setup | ✅ DONE (Authelia client + Pangolin/Newt live; OIDC login working) |
|
|
| A — Auth / session / colors | 🟡 A1 + A3 PASS; A2 (session persist) PENDING quick confirm |
|
|
| B — iOS standalone login (load-bearing) | 🟡 B1 PASS (installed + logged in on iPhone); B2/B3/B4 confirm; B5 (Android) pending |
|
|
| C — SSE smoke test | ⏳ PENDING — operator; this is the Phase 4 ENTRY gate (D-14), not a Phase 3 deliverable |
|
|
| D — Fastmail write round-trips | 🟡 D1/D4/D5/D6 PASS; D2 (all-day) + D3 (recurring) PENDING quick confirm |
|
|
|
|
Gate 2 is complete when all rows are PASS. Record final status here:
|
|
|
|
**Gate 2 outcome:** 🟡 NEARLY COMPLETE — core auth, distinct member colors, iOS install+login, and
|
|
create/edit/delete/conflict round-trips all verified live (2026-06-07). Remaining quick operator
|
|
confirms: A2 (session persists across restart), B2/B3/B4 (launch-from-Home-Screen standalone +
|
|
standalone login), D2 (all-day), D3 (recurring). Optional/deferred: B5 (Android install),
|
|
C (SSE — Phase 4 entry gate, carried per D-14). Phase 03 code is complete and live-verified.
|