Commit Graph
231 Commits
Author SHA1 Message Date
Lucas Berger 93c47b38aa fix(19): CR-02 send initialPassword + map 409 conflict in fetchCreateMember 2026-06-17 20:15:58 -04:00
Lucas Berger 1688f229e0 fix(19): CR-01 align OIDC-link client contract with server (authorizationUrl) 2026-06-17 20:15:23 -04:00
Lucas BergerandClaude Opus 4.8 53da4be62b fix(19): correct admin reset-password client URL (AUTH-LOCAL-08 blocker)
VERIFICATION.md found a cross-layer URL mismatch: fetchAdminResetPassword
POSTed to /api/admin/members/:id/reset-password but the API registers the
route as /api/admin/members/:id/password (admin.ts), so the Admin reset sheet
404'd on every submit. Confirmed live: old path -> 404, correct path -> 400
(route reached). Unit tests missed it because API tests hit the real path
directly and PWA tests mock the fetcher — no test crossed both layers.

Fix the client URL and add a URL-contract regression test that pins the exact
path (asserts fetch is called with /api/admin/members/:id/password).

PWA 266/266 (+1), typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:58:17 -04:00
Lucas BergerandClaude Opus 4.8 17a531550a fix(19): resolve post-merge wave-4 harness integration gaps
Two issues surfaced only when plans 19-04 (login UI) and 19-05 (Option C
bypass + login.spec) were merged together and run against the real stack —
neither executor could catch them in isolation:

1. LOCAL_SESSION_SECRET was added to the CI harness (ci.yml) but not to the
   local dev stack (docker-compose.dev.yml). Without it the real-login success
   path (POST /api/auth/local/login) 503s when signing the session cookie, so
   the e2e round-trip failed. Add the same fixed dev-only value to the dev
   compose override (dev-only target; never a production secret).

2. login.spec test 1 assumed clearing the local-session cookie yields a
   logged-out state, but under the always-on DEV_AUTH_BYPASS devAuthBypass()
   injects DEV_USER into /api/me regardless of any cookie — a logged-out state
   is architecturally unreachable in this bypass-only harness. Reframe the test
   to drive /login directly (validating the real-browser render of all brand +
   form surfaces) and move the unauthenticated root->/login redirect-gate
   coverage to a unit test in App.test.tsx where meQuery.isError is controllable.

Result: API 446/446, PWA 265/265 (+2 gate tests), e2e desktop 42 passed / 3
skipped (all login specs green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:43:11 -04:00
Lucas Berger 11977fddf4 chore: merge executor worktree (worktree-agent-ad5628ea37541e256) 2026-06-17 17:28:49 -04:00
Lucas Berger 19c45eb069 feat(19-04): AdminPage LOCAL ACCOUNTS + SettingsSheet change-password / link-OIDC
- Add hasLocalCredential to AdminMember type (mirrors API extension from plan 19-02)
- Add createMember mutation + Surface 11A inline add-member form in AdminPage
- Add Surface 11B Reset-password button in MemberRow (hasLocalCredential gate)
- Add ResetPasswordSheet component (bottom-sheet, role=dialog, focus-managed, Escape closes)
- Add Surface 12 Change-password row in SettingsSheet (hasLocalCredential gate)
- Add Surface 13 Link-OIDC identity row in SettingsSheet (hasLocalCredential + oidcEnabled gate)
- Add ChangePasswordSheet component (current/new/confirm fields, change-password mutation)
- Add LinkOidcSheet component (confirmation dialog; uses generic OIDC copy per D-06, no provider branding)
- Fix: update InstructionSheet.test.tsx to wrap with QueryClientProvider (Rule 1 - now uses useQuery)
- Fix: remove stale eslint-disable in App.test.tsx (lint --max-warnings 0 would fail)
- All 263 tests pass; typecheck clean; lint clean
2026-06-17 17:21:20 -04:00
Lucas Berger 1f94dc5eb7 feat(19-05): global-setup local_credentials seed + login.spec.ts + CI harness env
- global-setup.ts: TRUNCATE local_credentials + seed devuser/devpass (PHC scrypt inline)
- Create login.spec.ts: real-login-form e2e (gate redirect, wrong-password error, correct login)
- ci.yml: add LOCAL_SESSION_SECRET dev value + local_credentials seed step in harness job
- Fix all test mocks: add devSessionCookieMiddleware no-op to vi.mock(devBypass.js) blocks
  in admin/setup/push/lists/localAuth/authMode/requireAdmin tests (Rule 1 - Bug: missing export)
- Full API suite: 446/446 tests pass; pnpm typecheck: exit 0
2026-06-17 17:21:05 -04:00
Lucas Berger 32d0408774 feat(19-04): LoginPage (Surfaces 1-10) + App.tsx authModeQuery gate + /login route
- Create LoginPage with BrandSlot, username/password form, show/hide toggle
- Four error states: invalid credentials, rate-limit, locked, server (all per UI-SPEC)
- OIDC method divider + 'Login with OIDC' button rendered only when oidcEnabled
- Accessibility: role=main, h1 in BrandSlot, h2 Sign in, aria-live error banner, 44px targets
- Focus management: username autofocus, Enter navigates username→password→submit
- App.tsx: add authModeQuery (queryKey ['authMode'], staleTime 60s)
- App.tsx: add /login standalone route (sibling of /setup, no AppNav/BottomTabBar)
- App.tsx: login gate after setup gate — meQuery error + localEnabled → Navigate /login
- App.tsx: OidcRedirect helper for OIDC-only mode (meQuery error + !localEnabled + oidcEnabled)
- Fix App.test.tsx to include fetchAuthMode mock and hasLocalCredential in user fixture
2026-06-17 17:14:18 -04:00
Lucas Berger 869cdc26c8 feat(19-04): add LoginError, fetchAuthMode/login/logout + auth fetchers, BrandSlot, brand-seam tokens
- Add LoginError class (4 codes: invalid/rate-limit/locked/server) mirroring SessionExpiredError shape
- Add hasLocalCredential to MeUser interface
- Add fetchAuthMode, fetchLocalLogin, fetchLocalLogout (pre-auth endpoints)
- Add fetchChangePassword, fetchCreateMember, fetchAdminResetPassword, fetchLinkOidc
- Create BrandSlot component with Phase-17-ready placeholder (48px circle, FS initials, h1, tagline)
- Add --brand-logo-* CSS custom properties to tokens.css (Phase 17 seam)
2026-06-17 17:10:30 -04:00
Lucas BergerandClaude Opus 4.8 7354f3ec4f fix(12): unblock CI security + harness jobs
CI / changes (pull_request) Successful in 3s
CI / fast-checks (pull_request) Successful in 1m56s
CI / api (pull_request) Successful in 1m44s
CI / harness (pull_request) Successful in 6m28s
CI / security (pull_request) Successful in 1m11s
CI / gate (pull_request) Successful in 0s
security/gitleaks: allowlist apps/api/tests/routes/setup.test.ts — synthetic
  VAPID test pair (verified absent from .env), same class as existing fixture
  allowlist entries.
security/audit: waive GHSA-88fw-hqm2-52qc (hono CORS) — not exploitable, the
  app uses no hono cors() middleware; newly-published vs pinned hono 4.12.23.
harness/e2e: seed app_config.setup_complete='true' + a dev-admin credential in
  global-setup so the Phase-12 setup gate no longer redirects every spec to
  /setup (was causing all 95 e2e failures) and no onboarding banner renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:24:10 -04:00
Lucas BergerandClaude Opus 4.8 a193bc8236 fix(12): satisfy CI fast-checks — lint unused vars, typed contract-test body, prettier
CI / changes (pull_request) Successful in 3s
CI / fast-checks (pull_request) Successful in 2m16s
CI / api (pull_request) Failing after 1m37s
CI / harness (pull_request) Failing after 1h3m45s
CI / security (pull_request) Failing after 11s
CI / gate (pull_request) Failing after 1s
- Remove unused 'res'/'container' assignments (no-unused-vars)
- setupClient.contract.test.ts: typed parseSentBody helper + non-async json mock
  (no-unsafe-*/require-await)
- Prettier format 7 setup files

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:53:14 -04:00
Lucas Berger a13fc11556 feat(12-05): preserve Instance fields across Back navigation (gap 4)
- Lift appUrl/oidcIssuer/oidcClientId/vapidPublicKey into SetupPage so Step2 unmount preserves them
- Step2Config now reads/writes these via fields/setFields props
- Fastmail app password stays in Step3 local state, never lifted/persisted, cleared on unmount (T-12-15)
- Tests: Back from Calendar restores all four Instance values; password not persisted across nav
2026-06-15 21:32:06 -04:00
Lucas Berger 35db5c57e6 feat(12-05): drop DB-vs-env aside, add read-only DB-name field (gaps 1, 3)
- Remove the 'written to the database — not your environment file' aside from the Instance step intro
- Render a read-only, disabled DB-name field under App URL, populated from GET /api/setup/status dbName
- Helper text explains DB is configured via Docker env; only dbName is surfaced (T-12-3DB)
- Tests: assert aside absent, DB field readOnly/disabled with mocked dbName, existing DB validation row intact
2026-06-15 21:31:26 -04:00
Lucas Berger 2b3569ff20 fix(12-07): make ['me'] fresh on shell entry so post-wizard banner clears (gap 6)
- Root cause confirmed = mechanism (ii): ['me'] staleness, NOT a backend linking gap
  (upsertUser claim preserves users.id → credential stays linked → DB needsProviderSetup=false)
- SetupBanner ['me'] query staleTime 5min → 0 so a pre-claim stale cache entry is
  refetched on mount; banner hides once needsProviderSetup resolves false
- App.tsx boot ['me'] staleTime also set to 0 (committed with Task 1) for the same reason
- Add SetupBanner.test.tsx regression: absent when false, present (no dismiss) when true,
  stale-cache refetch hides banner; success-only dismissal contract preserved (no X button)
- Log pre-existing PWA lint errors (SetupPage.test.tsx, setupClient.contract.test.ts) to deferred-items.md
2026-06-15 21:23:46 -04:00
Lucas Berger fdcb4dc442 feat(12-07): gate /setup route on setupComplete (gap 5)
- Reverse-gate the /setup route: setupComplete===true → SetupPage alreadyLocked
  (Surface 8 'Setup already complete'); loading → no-flash placeholder; else wizard
- Add App.test.tsx reverse-gate tests (already-complete surface + active wizard on /setup)
- SetupPage mock now respects the alreadyLocked prop
2026-06-15 21:21:29 -04:00
Lucas Berger fbd3b77bde feat(12-06): expose non-secret DB name via GET /api/setup/status (gap 3)
- status returns { setupComplete, dbName } from process.env.DB_NAME (null fallback)
- only the DB name; never DB_HOST/DB_USER/DB_PASSWORD
- SetupStatusResponse carries dbName?: string | null for the PWA read-only field
2026-06-15 21:13:47 -04:00
Lucas BergerandClaude Sonnet 4.6 c86cff5dad fix(12): IN-04 trim email before sending to /api/setup/credential
email.trim() was already used in the saveDisabled guard but not applied to
the mutate call payload. A non-empty value with leading/trailing spaces would
pass the guard and reach the server untrimmed, causing Zod's z.string().email()
to reject it with a generic 400 and no diagnostic path for the user.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:22:47 -04:00
Lucas BergerandClaude Sonnet 4.6 066b69f2be fix(12): CR-01 remove extraneous providerType from postSetupCredential wire body
The server's credentialSchema does not declare providerType; it was being
silently stripped by Zod. Remove it from the request body and add a
contract test suite asserting the exact wire keys sent, mirroring the
existing BUG-1 tests for postSetupConfig.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:14:18 -04:00
Lucas BergerandClaude Sonnet 4.6 0d53249b02 fix(12-04): wire validateSetupVapid into setup wizard (close CR-01 / SETUP-02 gap)
- Import validateSetupVapid from api/client.ts in SetupPage.tsx
- Add vapid: ValidationRowState to validationRows state (alongside db/oidc)
- Extend configMutation.onSuccess chain: DB → OIDC → VAPID (sequential)
- Add ValidationRow for VAPID with pending/success/failure text
- Gate setBothPassed(true) on all three rows passing (db AND oidc AND vapid)
- Update anyPending and handleSaveAndValidate reset to include vapid state
- All 249 PWA tests pass; TypeScript clean

Closes CR-01; satisfies SETUP-02 "VAPID private key decodes to 32 bytes"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 15:13:40 -04:00
Lucas BergerandClaude Sonnet 4.6 7d0205df05 test(12-04): RED — wizard must validate VAPID before proceeding (SETUP-02 gap)
- Add 4 VAPID validation tests to SetupPage.test.tsx (CR-01 gap closure)
- Tests assert: validateSetupVapid is called, VAPID row renders, Continue
  is blocked when VAPID fails, Continue appears only after all 3 pass
- 3 tests currently FAIL (RED) — current code lacks validateSetupVapid import
  and has no vapid ValidationRow or vapid gate on bothPassed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 15:12:03 -04:00
Lucas Berger 120ce85a59 fix(12-04): align setup config client to camelCase API contract + readable errors
BUG 1: Rename SetupConfigPayload fields from snake_case to camelCase to match
the API configSchema (appExternalUrl, oidcIssuer, oidcClientId, vapidPublicKey).
Update SetupPage.tsx handleSaveAndValidate to send the correct camelCase keys.

BUG 2: Extract human-readable message from ZodError object in postSetupConfig
error handler. When body.error is an object with issues[], use issues[0].message
instead of stringifying the object (which produces "[object Object]").

All 245 PWA tests pass; TypeScript clean.
2026-06-15 14:48:07 -04:00
Lucas Berger 9f20c8b7cc test(12-04): RED regression for setup /config payload contract + error rendering
- Add setupClient.contract.test.ts with 9 tests targeting two bugs:
  BUG 1: SetupConfigPayload interface must use camelCase keys matching
    the API configSchema (appExternalUrl, oidcIssuer, oidcClientId,
    vapidPublicKey) — TypeScript compile error confirms mismatch
  BUG 2: postSetupConfig must throw readable string when API returns
    ZodError object in error field (not [object Object])
- Tests 7-8 fail (BUG 2 confirmed); TypeScript errors confirm BUG 1
2026-06-15 14:47:13 -04:00
Lucas Berger 1587bca9a0 feat(12-04): App.tsx setup-status gate + /setup route + redirect
- Add setupQuery (queryKey: setupStatus, staleTime: 0) alongside meQuery;
  queries GET /api/setup/status via fetchSetupStatus on every app load
- Add <Route path="/setup" element={<SetupPage />}> as standalone pre-auth route
- Add redirect gate: while loading → aria-hidden div (no flash); setupComplete===false
  → <Navigate to="/setup"> (no AppNav/BottomTabBar rendered); true → normal shell
- Add App.test.tsx covering both branches (setupComplete false/true) + loading state;
  236 tests pass, typecheck clean
2026-06-15 14:31:03 -04:00
Lucas Berger 62d80f6c46 feat(12-04): setup API client functions + SetupPage wizard component
- Add 7 setup functions to client.ts: fetchSetupStatus, postSetupConfig,
  validateSetupDb, validateSetupOidc, validateSetupVapid, postSetupCredential,
  postSetupComplete; plus SetupAlreadyLockedError for 423 handling
- Add SetupPage.tsx: standalone 4-step wizard (Welcome → Instance Configuration
  → Calendar Credential → Terminal/Locked) with Surface 2 step indicator,
  Surface 5 validation rows, Surface 6 action row, Surface 7 terminal screen,
  Surface 8 already-locked screen; role=main, aria-live, no nav shell
- No dangerouslySetInnerHTML; no AppNav/BottomTabBar imports
- All 230 pwa tests pass; typecheck clean; build green
2026-06-15 14:27:25 -04:00
Lucas Berger eb84e6e8e2 test(12-04): add failing tests for SetupPage wizard and setup API client (RED)
- Tests for all 7 setup client function exports (fetchSetupStatus, postSetupConfig,
  validateSetupDb, validateSetupOidc, validateSetupVapid, postSetupCredential, postSetupComplete)
- Tests for SetupPage rendering: page title, Welcome step heading, 4-step indicator,
  Continue button, role=main, aria-live region
- Tests for no AppNav/BottomTabBar in wizard output
- Tests for Already Locked screen (alreadyLocked prop) and Sign-in link
2026-06-15 14:22:19 -04:00
Lucas BergerandClaude Opus 4.8 1b4ff3cf93 test(18): scope timezone e2e to desktop profile (fix harness cross-profile leak)
CI / changes (pull_request) Successful in 2s
CI / fast-checks (pull_request) Successful in 1m28s
CI / api (pull_request) Successful in 1m6s
CI / harness (pull_request) Successful in 4m22s
CI / security (pull_request) Successful in 41s
CI / gate (pull_request) Successful in 1s
The timezone spec mutates the single household_timezone row, but e2e global-setup
resets it only once per run. Running on all three device profiles (iphone/pixel/
desktop) let one profile's "Save persists" write leak into another profile's
first-run assertions, failing the harness job in CI (workers=1, serial). The admin
timezone UI is desktop-focused, so skip the spec on non-desktop profiles — matching
the layout.spec.ts desktop-only pattern. Full harness: 107 passed, 19 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 09:30:55 -04:00
Lucas Berger c5b892c3a5 Merge remote-tracking branch 'origin/main' into gsd/phase-18-auto-timezone-detection-and-ability-to-change-timezone
CI / changes (pull_request) Successful in 3s
CI / fast-checks (pull_request) Successful in 1m27s
CI / api (pull_request) Successful in 1m5s
CI / harness (pull_request) Failing after 6m44s
CI / security (pull_request) Successful in 39s
CI / gate (pull_request) Failing after 1s
# Conflicts:
#	apps/pwa/vite.config.ts
2026-06-15 09:20:54 -04:00
Lucas BergerandClaude Opus 4.8 d6f6a5ae6f fix(18): searchable timezone combobox with type-to-search
Replace the picker with an accessible combobox (role=combobox + role=listbox):
focusing shows the full zone list (no typing/erasing needed), typing filters it
case-insensitively (underscores ignored, so "york" matches America/New_York),
with arrow-key navigation, Enter/click to select, and Escape to close. Fixes the
datalist limitation where a pre-filled value collapsed the dropdown to one match.
e2e updated to type+click options and a type-to-search case added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 08:36:49 -04:00
Lucas BergerandClaude Opus 4.8 46d7fcc2d2 chore(pwa): allow internal split-DNS host on the vite dev server
Add .bergerhouse.net (apex + subdomains) to server.allowedHosts so the dev
PWA is reachable through the reverse proxy / tunnel (e.g.
familysync-dev.bergerhouse.net). Dev-server only; production builds ignore it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 08:19:42 -04:00
Lucas BergerandClaude Opus 4.8 a8d6142566 fix(18): timezone picker shows full list on tap (native select)
The IANA picker was an <input list=datalist>, which filters the dropdown by
whatever text is already in the field — so with the stored zone pre-filled a
user only saw a single option and had to erase the value (undiscoverable) to
browse. datalist is also unreliable in iOS Safari.

Replace it with a native <select> grouped by region (<optgroup>): tapping
shows the whole list with no typing/erasing, and it renders as the native
wheel picker on iOS. The "Use detected" one-tap shortcut still covers the
common case. Option labels are shortened (region stripped, underscores → spaces)
while values remain full IANA ids. e2e updated from fill() to selectOption().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 08:19:42 -04:00
Lucas BergerandClaude Opus 4.8 ddeb87cdba test(18): make timezone e2e deterministic and align with WR-01 fix
The timezone-verify spec assumed a first-run (unset) starting state, but
e2e global-setup truncated only the list/event tables — never app_config —
so a prior run's saved household_timezone leaked across runs. Clear that key
in global-setup so the spec always starts from isExplicitlySet:false.

Also repurpose the stale "Save disabled when unchanged" assertion: after the
WR-01 fix, first-run Save is correctly ENABLED when the input matches the
displayed default (saving confirms the detected zone). The disabled-when-
unchanged-and-explicit case remains covered by the persist-across-reload test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 23:07:36 -04:00
Lucas Berger 173e06ea77 fix(18): enable first-run timezone save when not explicitly set (WR-01)
- Derive isExplicit from timezoneQuery.data?.isExplicitlySet
- Apply the input===stored no-op guard only when isExplicit is true
- Keep pending and empty-input guards unconditional
- Add unit tests (AdminPage.timezone.test.ts) verifying first-run Save is
  enabled when isExplicitlySet:false and input matches stored fallback value
2026-06-14 22:53:44 -04:00
Lucas Berger 3013b53b19 test(18-04): playwright-cli timezone round-trip e2e spec
- 6 desktop tests covering the full 18-04 acceptance criteria:
  timezone section visible, combobox pre-filled, save disabled when
  unchanged, save enables on change, persists across reload, use-detected
  affordance sets browser zone
- All 6 pass against the real 18-02 API endpoints
2026-06-14 22:44:44 -04:00
Lucas Berger 43d6689167 feat(18-04): add Timezone section (searchable IANA picker + save) to AdminPage
- Import fetchAdminTimezone + setAdminTimezone from api/client.js
- timezoneQuery: useQuery(['admin','timezone'], fetchAdminTimezone, retry:false, staleTime:60s)
- timezoneMutation: useMutation(setAdminTimezone) with invalidateQueries on success
- Timezone <section aria-label="Timezone"> after Shared Calendar (with marginBottom on preceding section)
- Searchable <input type=text list=iana-zones> + <datalist> from Intl.supportedValuesOf (guarded)
- 'Use detected: <zone>' affordance for D-02 one-tap seed
- 'Using system default' note when isExplicitlySet === false (D-06)
- Save button disabled while pending or when input equals stored value
- No touch to eventDateTime.ts / hydrateEvents.ts / other sections (D-07)
2026-06-14 22:37:21 -04:00
Lucas Berger 57424e6770 feat(18-04): add fetchAdminTimezone + setAdminTimezone to PWA API client
- Export AdminTimezoneResponse interface (timezone: string, isExplicitlySet: boolean)
- fetchAdminTimezone(): GET /api/admin/config/timezone with credentials/redirect pattern
- setAdminTimezone(timezone): PUT /api/admin/config/timezone with JSON body
- Both wrappers call handleAuthResponse (same auth handling as sibling admin calls)
2026-06-14 22:35:19 -04:00
Lucas BergerandClaude Opus 4.8 cc875de0eb chore(dev): allow tunnel host + all interfaces in Vite dev server
CI / changes (pull_request) Successful in 1s
CI / fast-checks (pull_request) Successful in 1m26s
CI / api (pull_request) Successful in 1m5s
CI / harness (pull_request) Successful in 4m16s
CI / security (pull_request) Successful in 39s
CI / gate (pull_request) Successful in 1s
Reaching the dev PWA through the Pangolin/newt tunnel failed: Vite's default
host check 403s any non-localhost Host header ('Blocked request'), which the
tunnel health checks on / and /health read as unhealthy. Add allowedHosts:true
and host:true so the dev server accepts the tunnel hostname and listens on all
interfaces. Dev-only config; the production image serves the built PWA itself.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 19:03:38 -04:00
Lucas BergerandClaude Opus 4.8 eff9b13c66 fix(11): make CI green — pin TZ in all-day scheduler tests, drop redundant casts
CI / changes (pull_request) Successful in 2s
CI / fast-checks (pull_request) Successful in 1m25s
CI / api (pull_request) Successful in 1m3s
CI / harness (pull_request) Successful in 4m14s
CI / security (pull_request) Successful in 41s
CI / gate (pull_request) Successful in 1s
fast-checks failed on 3 no-unnecessary-type-assertion ESLint errors (reminderIsCustom is now a real CalendarOccurrence field). api failed on 4 all-day 9 AM-local tests that assumed a UTC-4 host; CI runs UTC. Pin process.env.TZ=America/New_York in the all-day describe (production code reads TZ at call time, D-04).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 11:22:04 -04:00
Lucas Berger a3aec2d21d style(11-05): prettier format EventForm.test.tsx WR-03 additions 2026-06-14 08:26:00 -04:00
Lucas Berger a04c76b823 fix(11-05): WR-03 gate helper text on active preset set only
- helper text condition now uses (allDay ? ALLDAY_REMINDER_PRESETS : TIMED_REMINDER_PRESETS)
- previously checked !TIMED && !ALLDAY: a timed event with 10080 (in ALLDAY set) was
  incorrectly treated as 'in presets' and suppressed the helper text
- synthetic option gating for each allDay/timed branch was already correct
2026-06-14 08:24:24 -04:00
Lucas Berger 401591374a test(11-05): RED — WR-03 helper text suppressed for timed off-list 10080
- timed event with reminderLeadMinutes=10080 must show 'Custom reminder kept' helper
- currently suppressed: helper text checks !TIMED && !ALLDAY, but 10080 is in ALLDAY
- fix: gate helper text on active preset set only (allDay ? ALLDAY : TIMED)
2026-06-14 08:24:01 -04:00
Lucas Berger f6b47ebf1e fix(11-05): CR-01 surface reminderIsCustom to preserve custom VALARMs on edit
- expand.ts: add reminderIsCustom:boolean to CalendarOccurrence interface;
  derived from classifyValarms kind==='custom'; propagated to both
  non-recurring and recurring occurrence branches
- client.ts: mirror reminderIsCustom on CalendarOccurrence (atomic mirror)
- EventForm.tsx: extend deriveReminderValue to accept isCustom flag;
  returns '__custom__' when true, making the existing D-08 preserve branch
  live — editing a custom-alarm event now omits reminderLeadMinutes from
  the payload so outboxWorker extractValarms keeps the original VALARM
- Fix existing test fixtures (EventForm.test.tsx, EventDetailPopover.test.tsx)
  to include reminderIsCustom:false on all CalendarOccurrence literals

Fixes CAL-14 Pitfall 1: Apple Calendar absolute DATE-TIME / multi-VALARM
alarms no longer silently stripped on any edit round-trip from the PWA.
2026-06-14 08:11:46 -04:00
Lucas Berger 5d6cb47191 test(11-05): RED — CR-01 custom alarm round-trip
- expand.test.ts: 3 new tests asserting reminderIsCustom:true for
  absolute DATE-TIME trigger and multi-VALARM, false for relative preset
- EventForm.test.tsx: 3 new tests asserting __custom__ picker init,
  'Custom (kept)' option visibility, and payload omits reminderLeadMinutes
- Fixtures: absolute-alarm.ics (DATE-TIME VALARM), multi-alarm.ics (2 VALARMs)
- All 6 new tests FAIL (RED): reminderIsCustom field not yet on interface
2026-06-14 08:08:24 -04:00
Lucas Berger b9b3191b5b style(11-04): apply prettier to phase-11 modified files
- apps/pwa/src/components/EventForm.tsx (Task 2)
- apps/api/src/broker/{expand,reminderScheduler,sync,vevent}.ts (Plans 11-01/11-03)
- apps/api/tests/broker/{reminderScheduler,sync}.test.ts (Plans 11-01/11-03)
2026-06-14 06:54:11 -04:00
Lucas Berger fe549ef2b0 feat(11-04): add reminder picker to EventForm (allDay swap, edit pre-population, payload mapping)
- TIMED_REMINDER_PRESETS + ALLDAY_REMINDER_PRESETS sets for preset classification
- humanizeReminderLead() + deriveReminderValue() helpers for off-list synthetic options
- reminderValue state (__none__ / numeric string / __custom__ sentinels)
- allDay toggle resets reminderValue to __none__ (D-03 — no carry-over)
- Reset effect derives reminderValue from occurrence.reminderLeadMinutes on mount/cache update
- Reminder <select id=event-reminder> after Recurrence picker: allDay-conditional option swap (D-02), timed presets when !allDay, day-granularity presets when allDay; synthetic off-list option; Custom (kept) read-only disabled option
- Helper text under select in edit mode for off-list/custom states (D-07)
- executeSubmit payload: __none__ → null, numeric → integer, __custom__ → field omitted (D-08)
- 10 new Phase 11 reminder picker tests (create/allDay-swap/edit-pre-pop/payload-mapping); fix pre-existing None-ambiguity test
- pwa vitest 201/201 pass; tsc --noEmit clean
2026-06-13 22:36:31 -04:00
Lucas Berger 2c30afe8ff feat(11-04): add reminderLeadMinutes to CreateEventPayload + CalendarOccurrence
- CalendarOccurrence: required reminderLeadMinutes: number | null (atomic mirror of expand.ts, Plan 11-03)
- CreateEventPayload: optional reminderLeadMinutes?: number | null with absent/null/0/positive contract (D-08)
- Update CalendarOccurrence fixtures in EventForm.test.tsx + EventDetailPopover.test.tsx to include the new required field (reminderLeadMinutes: null)
- pwa tsc --noEmit exits 0
2026-06-13 22:33:06 -04:00
Lucas Berger 79fe3e0e04 fix(10-04): prettier format + remove unnecessary type assertions
- Run prettier on all new/modified PWA files (CredentialSheet, SetupBanner, AdminPage, admin.spec.ts)
- Remove unnecessary 'as React.RefObject<HTMLElement | null>' casts flagged by @typescript-eslint/no-unnecessary-type-assertion
- Format pre-existing API files from Plans 02/03 (me.ts, user.test.ts, requireAdmin.test.ts, me.test.ts)
- All 270 API tests + 191 PWA vitest tests pass; lint/typecheck/build clean
2026-06-13 15:22:50 -04:00
Lucas Berger 7808426a2f feat(10-04): AdminPage + /admin route + conditional nav entries + e2e spec
- AdminPage: Admin Settings heading, MEMBERS section (avatar+status+action), SHARED CALENDAR radio group + two-tap Save + empty state
- App.tsx: /admin route gated by meQuery.data.user.isAdmin (loading gate prevents flash), SetupBanner mounted above content, BottomTabBar + AppNav receive isAdmin
- AppNav.tsx: ShieldCheck Admin nav entry rendered only when isAdmin=true (D-03 UX gating)
- BottomTabBar.tsx: ShieldCheck Admin tab rendered only when isAdmin=true (D-03 UX gating)
- e2e/admin.spec.ts: 5 assertions across 3 profiles (15 total tests) — admin sees nav+page+members, non-admin: no nav entry + /admin redirects to /calendar
- All 15 e2e tests pass (iphone/pixel/desktop); production build clean
2026-06-13 15:16:11 -04:00
Lucas Berger 2c2c71e7cc feat(10-04): add CredentialSheet and SetupBanner components
- CredentialSheet: admin-rotate/admin-add/self-service modes, role=dialog, aria-modal
- Password field type=password autoComplete=new-password, never pre-filled (T-10-16)
- Fastmail link target=_blank rel=noopener noreferrer (UI-SPEC Surface 3)
- Loader2 spinner + CalDAV failure copy on mutation error
- Success invalidates ['admin','members'] + ['me'] → SetupBanner unmounts
- Escape closes, focus returns to trigger (a11y)
- SetupBanner: renders on needsProviderSetup=true only, role=status aria-live=polite
- KeyRound icon, 'Set up your calendar' heading, 'Set up now' CTA (no X/dismiss)
- Success-only dismissal: ['me'] invalidation is the ONLY code path to hide the banner
- All styling via var(--token); 44px touch targets throughout
2026-06-13 15:03:57 -04:00
Lucas Berger bfe1eff5a3 feat(10-04): extend client.ts — MeUser.isAdmin+needsProviderSetup + admin/self-service fetchers
- Add isAdmin and needsProviderSetup to MeUser interface (D-03 UX gating)
- Add AdminMember, SaveCredentialPayload, AdminCalendar, SaveMyCredentialPayload types
- Add fetchAdminMembers, saveCredential, fetchAdminCalendars, setSharedCalendar fetchers
- Add saveMyCredential (self-service, no userId field — T-10-12/Pitfall 6)
- All fetchers use credentials:'include', redirect:'manual', handleAuthResponse
- Password never logged or stored beyond in-flight request body (T-10-15)
2026-06-13 15:02:06 -04:00
Lucas Berger bb00c71730 feat(10-01): seed dev-bypass user id=1 as is_admin=true in e2e global-setup
- INSERT INTO users (id=1, is_admin=true) ON DUPLICATE KEY UPDATE is_admin=true (idempotent)
- Supplies placeholder non-null oidc_iss='dev-bypass', oidc_sub='dev-user-1', color='#4A90D9'
- requireAdmin (Plan 02) does a DB lookup for the bypass user; without this seed it would 403
- Existing calendar/event/list seeds unchanged (INSERT IGNORE INTO calendars, Seeded Test Event)
2026-06-13 14:23:09 -04:00