Files
familysync/.planning/phases/03-event-write-back-pwa-install/03-GATE2-RESULTS.md
T
Lucas Berger f656a41d1c docs(03): Gate 2 substantially complete — A/B/D verified; only D3 quick confirm left
A2 (transparent SSO), B2/B3/B4 (iOS standalone + load-bearing standalone login),
D2 (all-day, after off-by-one fix) all PASS. Remaining: D3 weekly recurring
(quick). Deferred: B5 Android, C SSE (Phase 4 entry gate).
2026-06-07 18:40:01 -04:00

12 KiB
Raw Blame History

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:

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=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)

# 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) 🟡 PASS (transparent) Confirmed (desktop + iPhone): cold open bounces through Authelia but its SSO carries the session, so NO credential prompt — user lands straight on the app. Note: the app's own oidc-auth cookie is session-scoped (dropped on browser close), so each cold open does a redirect round-trip. Acceptable for v1; making the app cookie persistent (skip the bounce) is a minor follow-up.
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) PASS (2026-06-07) Confirmed: launches full-screen standalone from Home Screen.
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) PASS (2026-06-07) Confirmed working — OIDC login from standalone stays in the app, no drop to Safari. Load-bearing check cleared.
B4 PWA-01 Installed PWA on iOS opens full-screen with no browser chrome PASS (2026-06-07) Confirmed (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 PASS (2026-06-07) All-day create round-trips to Fastmail (verified VEVENT: DTSTART/DTEND VALUE=DATE, exclusive end). Found + fixed a display off-by-one (single-day showed across 2 days — Schedule-X inclusive vs iCal exclusive end; commit d4d5327). Reload to confirm 1-day rendering.
D3 CAL-04 Create a weekly recurring event → appears in Fastmail [ ] PENDING — operator Not yet exercised live. Quick confirm (create one weekly recurring event).
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, A2 (transparent SSO), A3 all PASS
B — iOS standalone login (load-bearing) B1B4 PASS (install + standalone launch + standalone login); B5 (Android) deferred
C — SSE smoke test DEFERRED — Phase 4 ENTRY gate (D-14), not a Phase 3 deliverable
D — Fastmail write round-trips D1/D2/D4/D5/D6 PASS; D3 (weekly recurring) — one quick confirm remaining

Gate 2 is complete when all rows are PASS. Record final status here:

Gate 2 outcome: SUBSTANTIALLY COMPLETE (2026-06-07) — auth, session, distinct member colors, iOS install + standalone login (load-bearing), and create/all-day/edit/delete/conflict round-trips all verified live. Multiple blocker bugs found + fixed this session (see git log). Only one quick functional confirm remains: D3 (create a weekly recurring event). Deferred by design: B5 (Android install) and C (SSE smoke — Phase 4 entry gate per D-14). Phase 03 is code-complete and live-verified; safe to close with D3 as a fast confirm and B5/C carried forward.