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.
12 KiB
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
doneagainst 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:
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:
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.
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=productionis set in the container — this forcesdevBypassActive=falseinapps/api/src/index.ts, mounting the OIDC guard unconditionally.DEV_AUTH_BYPASSis absent (or unset) from the production environment block. Even if accidentally present,NODE_ENV=productionsuppresses it at the first conditional indevBypass.ts, but leave it out to keep the config unambiguous.
3. Expose via Pangolin / Newt (Mode A local rig)
# 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)
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
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.webmanifesthasscope: "/"andstart_url: "/"; confirm/callbackis 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 |
# 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.