Commit Graph
215 Commits
Author SHA1 Message Date
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
Lucas Berger 46bea03757 feat(15-01): fix 13 markdown violations + wire md:lint to fast-checks CI step
- Fix MD040 (11 bare fences): add language tags (text/bash) across 7 files
- Fix MD031 (2 violations): add blank lines around fence in GETTING-STARTED.md
- Wire 'Markdown lint' step to fast-checks job (after Format check, before Typecheck)
- Reformat .markdownlint-cli2.jsonc per Prettier (trailing commas in JSONC)
- pnpm md:lint exits 0; pnpm format:check exits 0; gate can fail on bare fence (verified)
2026-06-12 10:46:01 -04:00
Lucas Berger f75bc4239f style(14-01): apply prettier formatting to layout.spec.ts for CI format:check gate
CI / fast-checks (pull_request) Successful in 1m22s
CI / api (pull_request) Successful in 59s
CI / harness (pull_request) Successful in 3m58s
2026-06-12 08:31:33 -04:00
Lucas Berger bfc49d199d chore(14-01): update spec headers + README for desktop profile, cosmetic CI step rename
- Updated calendar.spec.ts header to list all three profiles (iphone/pixel/desktop)
- Updated lists.spec.ts header to list all three profiles (iphone/pixel/desktop)
- Updated e2e/README.md preamble to add 'Desktop Chrome (1280x720)'
- Added --project=desktop example to README run-commands block
- Updated README full-suite command comment to name all three profiles
- Cosmetic: ci.yml step-name and comment updated to mention desktop (no plumbing change)
- Full suite verified: 85 passed, 5 skipped (3 desktop geometry + 2 parity guards), 0 failed
2026-06-12 08:23:10 -04:00
Lucas Berger 29035999ea feat(14-01): desktop-skip three mobile-only layout assertions, add D-04 parity
- Added test.skip(testInfo.project.name === 'desktop') to the two safe-area-inset
  BottomTabBar in-viewport tests (BottomTabBar returns null at >=768px on desktop)
- Added test.skip(testInfo.project.name === 'desktop') to the 56x56 FAB geometry test
  (on desktop 'New Event' resolves to the toolbar button, not the 56px FAB)
- Added desktop-only D-04 parity test asserting 'New Event' toolbar button height >=44px
  guarded by test.skip(testInfo.project.name !== 'desktop')
- Updated header jsdoc to list all three profiles including desktop
- All mobile assertions preserved (toBeGreaterThanOrEqual(56) and (44) still present)
2026-06-12 08:20:06 -04:00
Lucas Berger d3bc69657b feat(14-01): add desktop Playwright project (Desktop Chrome, 1280x720, no hasTouch)
- Appended 'desktop' project entry after 'pixel' in playwright.config.ts projects array
- Uses devices['Desktop Chrome'] with serviceWorkers: 'block' (D-02/Pitfall 15)
- No baseURL override — inherited from top-level use block (D-08)
- Updated file header jsdoc: three-profile matrix, added --project=desktop example
2026-06-12 08:19:04 -04:00
Lucas Berger af78ccc8b9 fix(quick-260611-tfc): make notificationclick openWindow fallback reachable
- Add .then(navigated) check: opens new window when client.navigate() resolves null
- Add .catch(): opens new window when client.focus() or client.navigate() rejects
- Both branches guarded by self.clients.openWindow per spec
- Returned chain (not floating) satisfies no-floating-promises gate
- All other behaviour preserved: close(), url extraction, post-loop fallback
2026-06-11 21:14:14 -04:00
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
2026-06-11 20:35:18 -04:00
Lucas Berger 3f2e3ea659 fix(13-02): resolve residual no-unnecessary-type-assertion in SyncStateToast.test
- Replace inline 'as string | null' assertion with an explicit typed const
  declaration inside the vi.hoisted() callback body
- 'value: string | null' typed const satisfies both ESLint (no assertion) and
  tsc (null assignment on line 82 is type-safe)
2026-06-11 20:30:51 -04:00
Lucas Berger 03e953158a fix(13-02): eliminate all ESLint violations — pnpm lint exits 0
- eslint.config.js: disable React Compiler rules (v7 flat.recommended enables
  them; codebase does not use the Compiler); add e2e/ to disableTypeChecked
  block; promote exhaustive-deps to error
- API broker: remove redundant as-casts (outboxWorker, poller, reminderScheduler,
  expand, sync, vevent, spike); add targeted ical.js no-unsafe-assignment/argument
  disables with justifying comments inside try blocks
- API routes/sse.ts: fix no-misused-promises on async writeSSE callback with
  void+IIFE+catch pattern
- API routes/lists.ts: let → const for updateValues
- API tests: remove unused imports (beforeEach, eq, vi); rename unused vars
  with _ prefix; remove unused lastActiveId assignment
- PWA components: void navigate() and void queryClient.invalidateQueries() on
  all fire-and-forget call sites; fix CalendarShell explicit-type-casts;
  Couldn't → HTML entity
- PWA test files: as unknown as Response for partial mock objects; string | null
  type annotation on mockLastSyncedUid; remove async from test callbacks without
  await; act(() => {}) not await act(async () => {}) for sync ops
- sw.ts: restructure Notification.data?.url access as let+if so disable
  comments land on the exact violation lines; void self.skipWaiting()
2026-06-11 20:23:38 -04:00
Lucas Berger df62d333d0 chore(13-01): install ESLint+Prettier, author flat config, wire lint/format scripts
- Install eslint@9.39.4 @eslint/js@9.39.4 typescript-eslint@8.61.0
  eslint-plugin-react@7.37.5 eslint-plugin-react-hooks@7.1.1
  eslint-config-prettier@10.1.8 prettier@3.8.4 as root devDependencies
- Author eslint.config.js: ignores → base recommendedTypeChecked (projectService:true)
  → pwa-react block (apps/pwa/** only) → disableTypeChecked override (config files +
  apps/api/tests) → prettier-last (eslint-config-prettier/flat)
- Add .prettierrc (singleQuote:true, printWidth:100) and .prettierignore
- Add "type":"module" to root package.json; add format/format:check scripts
- Add lint script (--max-warnings 0) to apps/api and apps/pwa
2026-06-11 19:44:57 -04:00
Lucas Berger 9c38dd33ff fix(07): WR-02 explicit readiness flag + WR-01 /api/me dev-bypass gate in global-setup 2026-06-11 07:51:51 -04:00
Lucas Berger 5322cfc2b0 fix(07): WR-06 correct misleading 'remove by reload' comments to describe evaluate(remove) 2026-06-11 07:51:45 -04:00
Lucas Berger 2b745adb29 fix(07): WR-05 drop redundant unroute calls (per-test context isolation handles cleanup) 2026-06-11 07:51:41 -04:00
Lucas Berger c564fc67a1 fix(07): WR-07 prove SW block via getRegistration, skip when SW unavailable 2026-06-11 07:51:34 -04:00
Lucas BergerandClaude Opus 4.8 53c3ca56b8 fix(07-04): make calendar populated-state test non-vacuous (BL-01) + deterministic seed window (BL-02)
Deep review found the calendar 'populated state' assertions were vacuous:
- getByText('Nothing here').toHaveCount(0) targeted CalendarShell's EmptyState,
  which CalendarShell NEVER renders (success branch always mounts ScheduleXCalendar;
  EmptyState.tsx is dead code, imported by nothing). The check was permanently green
  regardless of the seed — a regression dropping all events would have shipped green.
- .sx-react-calendar-wrapper renders on any successful auth, with or without events,
  so it never proved the seed reached the UI.

Replaced the dead-EmptyState check with a real DB→UI proof: assert the seeded event
title 'Seeded Test Event' is rendered in the grid. Verified non-vacuous — passes with
the seed on both profiles; with /api/events mocked to [] the title is absent (would fail).

BL-02: the seed anchored the event at now+24h. Both phone profiles render the
month-agenda view of the CURRENT month, so on a month's last day 'tomorrow' falls into
the next month and vanishes from the grid, making the new visibility assertion date-fragile.
Re-anchored to noon-today (UTC) — always today's local date, always in the current-month view.

Verified: full 58-test suite passes both profiles; typecheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 07:38:41 -04:00
Lucas BergerandClaude Opus 4.8 fcc680e553 fix(07-02): fail-closed guard on global-setup seed (CR-01, data-loss prevention)
global-setup.ts TRUNCATEs four tables against whatever DB_* points at, with no
production guard — an operator with prod DB_* still exported could wipe lists/
list_items/list_shares/calendar_events. The README promised a DEV_AUTH_BYPASS
guardrail the code never enforced. Adds a fail-closed guard mirroring
apps/api/src/auth/devBypass.ts: hard NODE_ENV==='production' check first, then
require DEV_AUTH_BYPASS==='true' before opening any DB connection. README updated
with the test-process env requirement (run command + CI runner env).

Verified: guard throws without DEV_AUTH_BYPASS; full 58-test suite passes with it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 02:23:38 -04:00