Commit Graph
393 Commits
Author SHA1 Message Date
Lucas Berger bf091102f2 fix(08-fix): IN-01 document MariaDB-only long-unique HASH index dependency 2026-06-11 19:27:11 -04:00
Lucas Berger c0f892cae5 fix(db): squash migrations to single baseline (cold-migrate was broken)
0000_easy_slipstream already created lists/list_shares/list_items and the
calendars unique constraint, but 0001_lists_schema re-created those tables and
0001_calendars_user_url_unique was an orphan (not in _journal) — so a cold
`drizzle-kit migrate` against an empty DB failed with ERROR 1050 'Table lists
already exists'. Dev only survived because its DB was built incrementally; CI
is the first cold migrate and exposed it.

Regenerated a single 0000_baseline.sql from schema.ts. Verified on a fresh
mariadb:11: migrate succeeds, schema is structurally identical to the running
dev DB, `drizzle-kit generate` reports no drift, and all 238 API tests pass.
Local dev DBs must be rebuilt (drop + db:migrate); no prod exists.
2026-06-11 14:28:08 -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
Lucas Berger b074b4abb2 feat(07-04): add lists.spec.ts — populated and empty-state tests (TEST-01)
- Populated state: asserts 'Open list: E2E Grocery List' button visible + listitem count ≥1 + 'No lists yet' absent + no overflow
- Empty state: routes /api/lists to 200 [] before goto, asserts 'No lists yet' + 'Tap + to create' visible + no overflow; unroutes after
- Seeded DB not mutated — empty state is network-simulated (T-07-11 / D-06)
- No absolute URLs; both states pass Rule 2 overflow check
2026-06-11 02:11:44 -04:00
Lucas Berger 17b625b6fe feat(07-04): add calendar.spec.ts — populated, error, auth-bypass + SW precondition tests
- Populated state: asserts .sx-react-calendar-wrapper visible + 'Nothing here' absent
- Error state: mocks /api/events* to 500, asserts 'Couldn't load events' heading + Retry ≥44px + no overflow; unroutes after
- TEST-02 preconditions: asserts nav landmark visible (authed reach) + URL on localhost (no Authelia redirect) + navigator.serviceWorker.controller null (SW block)
- No absolute URLs; route mocks paired with unroute (T-07-11)
2026-06-11 02:11:36 -04:00
Lucas BergerandClaude Opus 4.8 4cd7931c3c fix(07-03): make e2e typecheck pass — add DOM lib to tsconfig.e2e, cast styleHandle el to Element
Playwright transpiles specs without typechecking (esbuild), so layout.spec.ts ran
green while `tsc -p tsconfig.e2e.json` failed: page.evaluate(() => document...)
callbacks need the DOM lib, and styleHandle.evaluate((el) => el.remove()) typed el as
Node (no .remove()). Phase 8 CI runs the typecheck gate, so this would have broken CI.
Adds DOM/DOM.Iterable to the e2e tsconfig (also covers 07-04 specs) and casts el to Element.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 02:06:15 -04:00
Lucas Berger 52e14a88db feat(07-03): layout.spec.ts — UI-SPEC Rules 1-4 + harness self-validation
- Rule 1: boundingBox assertions for BottomTabBar Calendar/Lists tabs (≥44px),
  PhoneNav settings button (≥44px), New Event FAB (≥56px) on both profiles
- Rule 2: scrollWidth ≤ clientWidth on /calendar and /lists
- Rule 3: BottomTabBar in-viewport (bottom edge ≤ viewport height), PhoneNav visible
- Rule 4: navigation landmark locatable by role+name (getByRole with accessible name)
- Self-validation: addStyleTag injection proves Rule 1 tracks geometry (20px height <44),
  proves Rule 2 detects overflow (2000px body width); both recover after removal
- Runs on iphone (WebKit/390px) and pixel (Chromium/412px) profiles; 30 tests pass
2026-06-11 02:00:46 -04:00
Lucas Berger d3c6726301 fix(07-02): MariaDB TIMESTAMP format in global-setup seed
- Replace ISO 8601 'T' separator with space in dtstart_utc value
- MariaDB TIMESTAMP requires 'YYYY-MM-DD HH:MM:SS', not 'YYYY-MM-DDTHH:MM:SSZ'
- Was causing 'Incorrect datetime value' error blocking all e2e harness runs
2026-06-11 02:00:38 -04:00
Lucas Berger 535ba11cda docs(07-02): add e2e/README.md — run instructions and security guardrails
- Documents pnpm test:e2e run commands and single-profile / headed variants
- Documents DEV_AUTH_BYPASS=true must be set before API starts (Pitfall 5)
- States production compose MUST NOT set DEV_AUTH_BYPASS (Elevation of Privilege)
- Lists PLAYWRIGHT_BASE_URL and DB_* env vars (all credentials env-only, never hardcoded)
- States no storageState file is used (D-01 — no expiring session cookie)
- Describes globalSetup readiness gate + seed anchors (Milk/Eggs/Seeded Test Event)
- Notes Phase 8 CI scope and --with-deps WebKit requirement
2026-06-11 01:50:58 -04:00
Lucas Berger 53498e3408 feat(07-02): implement globalSetup — /health readiness gate + deterministic DB seed
- Poll baseURL/health with 60s timeout; throw on expiry (D-08 readiness gate)
- TRUNCATE list_items, list_shares, lists, calendar_events (FK checks toggled)
- INSERT IGNORE calendars guard for id=10 (Pitfall 4 CI resilience)
- Seed one timed calendar_event on calendar_id=10 (uid: e2e-seed-event-001)
- Seed E2E Grocery List (owner_id=1, is_shared=true) + list_shares + Milk/Eggs items
- Add mysql2@3.22.4 devDependency to apps/pwa (needed for global-setup types)
- No @playwright/test imports — plain Node.js only (Pitfall 2)
2026-06-11 01:50:10 -04:00
Lucas Berger 4536987915 chore(07-01): isolate vitest glob from e2e, add tsconfig.e2e.json typecheck gate
- Add exclude: ['e2e/**', 'node_modules/**'] to vitest.config.ts test block
  to prevent Playwright specs from being picked up by Vitest jsdom runner
- Add tsconfig.e2e.json extending main tsconfig with node types for
  playwright.config.ts and e2e/**/* typecheck coverage
- Add @types/node to pwa devDependencies (required by playwright.config.ts)
- Update typecheck script to run both src and e2e tsc passes
- 191 unit tests still pass; no e2e import errors in vitest run
2026-06-11 01:43:26 -04:00
Lucas Berger 44fea2c5e2 feat(07-01): add playwright.config.ts with two-profile device matrix
- Two projects: iphone/WebKit (iPhone 14) + pixel/Chromium (Pixel 7)
- serviceWorkers: 'block' on both profiles per D-02/Pitfall 15
- env-driven baseURL via PLAYWRIGHT_BASE_URL (D-08/Rule 8)
- globalSetup ref to e2e/global-setup.ts (Plan 02 implements)
- webServer manages vite only with reuseExistingServer (D-10)
- no storageState, no toHaveScreenshot per D-01/UI-SPEC Rule 6
- add e2e/global-setup.ts placeholder (stub) so config path resolves
2026-06-11 01:41:44 -04:00
Lucas Berger 0c24f77bdb chore(07-01): install @playwright/test@1.60.0, add test:e2e scripts
- Add @playwright/test@1.60.0 to apps/pwa devDependencies (pinned, not floated)
- Install WebKit and Chromium browser engines via playwright install
- Add test:e2e, test:e2e:ui, test:e2e:headed scripts to apps/pwa/package.json
- Add root workspace test:e2e delegate script to package.json
2026-06-11 01:40:30 -04:00
Lucas Berger a9c3304c4e docs: generate project documentation 2026-06-10 18:17:51 -04:00
Lucas Berger 7ac4c29ea9 fix(06): IN-06 fold long DESCRIPTION line per RFC 5545 in weekly-count3 fixture 2026-06-10 16:56:06 -04:00
Lucas Berger 1ab9710066 fix(06): IN-05 extract duplicated dialog focus-trap into shared useFocusTrap hook 2026-06-10 16:55:30 -04:00
Lucas Berger a570135a8d fix(06): IN-02 document resolveDefaultView is an SSR guard, not breakpoint logic 2026-06-10 16:54:12 -04:00
Lucas Berger 8b79d499f6 fix(06): IN-01 correct stale CalendarOccurrence.id comment to ev-<uid>-<epochMs> form 2026-06-10 16:53:52 -04:00
Lucas Berger d4a0ed7bf3 fix(06): WR-08 require well-formed timed shape before new Date() in parseDateTime 2026-06-10 16:53:38 -04:00
Lucas Berger 0511a23886 fix(06): WR-06 bound post-write targeted resync with 10s timeout so a hang cannot wedge drain 2026-06-10 16:53:13 -04:00
Lucas Berger 9f88068d77 fix(06): WR-05 also match SessionExpiredError by name across module-graph boundaries 2026-06-10 16:52:47 -04:00
Lucas Berger 746c3c70d7 fix(06): WR-04 use local-date helper in calendarStore instead of UTC toISOString slice 2026-06-10 16:52:33 -04:00
Lucas Berger ac0f8d282b fix(06): WR-03 sanitize recurrenceCount input and require finite integer >= 1 2026-06-10 16:51:59 -04:00
Lucas Berger 5724fe85d2 fix(06): WR-02/WR-07 require end date for bound=until and guard non-empty startDate compare 2026-06-10 16:51:41 -04:00
Lucas Berger eb00ec7dfb fix(06): WR-01 gate all-day SQL branch on hasRrule=0 to stop duplicate occurrences 2026-06-10 16:51:19 -04:00
Lucas Berger d101aa899d fix(06): CR-01 validate recurrenceUntil as YYYY-MM-DD to close RRULE injection 2026-06-10 16:51:06 -04:00
Lucas Berger 562026149f fix(06-06): remap primary-family container var so fallback all-day events stay solid
The per-family remap (shared, member-1..4) only fills all-day pills whose
Schedule-X colorName is registered. Member calendars absent from the current
/api/me members list fall back to Schedule-X's built-in primary family, which
was not remapped — so those all-day events degraded to the light tint. Remap
--sx-color-primary-container as well so all-day pills stay solid in the
fallback case too (production member-N calendars already covered).
2026-06-10 16:19:45 -04:00
Lucas Berger 089b53d767 fix(06): hide BottomTabBar on desktop so it no longer overlaps Settings
- Add isPhone() helper using window.matchMedia('(max-width: 767px)') consistent with AppNav
- Return null when isPhone() is false (desktop ≥768px) — BottomTabBar is phone-only
- Prevents the position:fixed bottom bar from overlaying AppNav sidebar avatar/Settings on desktop
- RED test committed in prior commit (740e342)
2026-06-10 16:03:32 -04:00
Lucas Berger 740e34210b test(06): add failing BottomTabBar hidden-on-desktop test (FIX 4) 2026-06-10 16:02:43 -04:00
Lucas Berger 051874ba12 fix(06): make AppNav persistent across routes so Lists keeps the nav
- Lift AppNav from CalendarShell to App.tsx as a sibling of <Routes>
- App.tsx fetches /api/me (same query key as CalendarShell — deduplicated by TanStack Query)
- App.tsx provides the outer layout (phone: column, desktop: row) with AppNav always rendered
- CalendarShell simplified: no longer manages AppNav, outer flex layout stays in App.tsx
- AuthSplash gains overlay prop (position:fixed inset:0 z-index:999) so it covers AppNav when needed
- CalendarShell uses AuthSplash with overlay=true so auth splashes cover full viewport
- Remove onOpenSettings prop from CalendarShell (wired directly in App.tsx to SettingsSheet)
- Desktop sidebar nav (FamilySync brand, Calendar/Lists links) now persists on /lists route
2026-06-10 16:02:02 -04:00
Lucas Berger 6070437812 test(06): add AppNav persistence test for app-shell layout (FIX 3 contract) 2026-06-10 15:57:01 -04:00
Lucas Berger e392c69196 fix(06-05): make AuthSplash dead-end state reachable + persist redirect guard
- CalendarShell now captures maybeRedirectToLogin() return value in meQuery.isError effect
- When the one-shot guard is exhausted (returns false), arm loginRedirectExhausted state
- Render AuthSplash state=dead-end (tap-to-retry) when guard is exhausted, not indefinite redirecting spinner
- Reset loginRedirectExhausted on successful auth (meQuery.isSuccess) for session recovery
- Add sessionStorage.clear() to beforeEach so CalendarShell tests are isolated
- RED test committed in prior commit (36ef7a0)
2026-06-10 15:55:06 -04:00
Lucas Berger 36ef7a00b7 test(06-05): add failing dead-end AuthSplash test for exhausted redirect guard (D-11) 2026-06-10 15:53:55 -04:00
Lucas Berger 6dbb1664ff fix(06-06): target real Schedule-X all-day class for filled pills (999.6)
- Replace non-existent .sx__all-day-event selector with confirmed class names
- .sx__date-grid-event: exclusively all-day in week/day view (timed go to .sx__time-grid-event)
- .sx__month-grid-event:not(:has(.sx__month-grid-event-time)): all-day in month view
- Use CSS custom property inheritance to remap -container vars to solid main colors in date-grid and month-grid-day__events scopes
- Cover shared + member-1..4 calendar colorNames (N-member design)
- Source-confirmed from @schedule-x/calendar@4.6.0 dist/core.js
2026-06-10 15:53:14 -04:00
Lucas Berger c864fc4eea fix(260610-ka9): restore audible Android push notifications
- Add icon, badge, renotify:true, vibrate to showNotification options so
  reused-tag updates produce heads-up + sound/vibration on Android Chromium
- Narrow cast (as NotificationOptions) to handle renotify/vibrate absent
  from this lib.dom version without suppressing other errors
- Generalize ANDROID_STEPS first step to browser-agnostic (Chrome or Edge)
2026-06-10 14:39:46 -04:00
Lucas Berger 8343faddce feat(260610-k1z-01): wire persistSessionCookie into index.ts + add unit tests
- Mount persistSessionCookie() immediately after oidcAuthMiddleware() inside !devBypassActive block
- Test A: truthy oidcAuthJwt produces Set-Cookie with Max-Age, SameSite=Lax, HttpOnly, Secure
- Test B: falsy/absent oidcAuthJwt emits no oidc-auth cookie (no-resurrection guard)
2026-06-10 14:32:42 -04:00
Lucas Berger aabcb5d043 feat(260610-k1z-01): add persistSessionCookie() middleware (AUTH-02)
- Re-issues oidc-auth cookie with maxAge so PWA sessions survive close/reopen
- Guards on c.get('oidcAuthJwt'): only runs when @hono/oidc-auth set a valid session
- Falsy oidcAuthJwt falls straight through — no resurrection of deleted/absent cookies
- Cookie attrs mirror the library: httpOnly, secure, sameSite=Lax, conditional domain
- maxAge reads OIDC_AUTH_EXPIRES (default 86400s)
2026-06-10 14:30:59 -04:00
Lucas Berger f82837ca03 test(quick-260610-jlp): drop unused React import in InstructionSheet test 2026-06-10 14:15:02 -04:00
Lucas Berger 874c030291 fix(quick-260610-jlp-01): wire SettingsSheet 'How to enable' to open InstructionSheet
- Add instructionsOpen state to SettingsSheet
- Change broken onClick={onClose} to onClick={() => setInstructionsOpen(true)}
- Render InstructionSheet conditionally when instructionsOpen=true
- Add InstructionSheet.test.tsx: asserts dialog opens + onClose not called (UAT-05-T4)
2026-06-10 14:12:38 -04:00
Lucas Berger 74b5d44712 refactor(quick-260610-jlp-01): extract InstructionSheet into shared component
- Move isIOS, IOS_STEPS, ANDROID_STEPS, InstructionSheetProps, InstructionSheet from PermissionDeniedBanner verbatim
- Export InstructionSheet as named export from InstructionSheet.tsx
- PermissionDeniedBanner now imports from ./InstructionSheet.js (behaviour identical)
2026-06-10 14:10:58 -04:00
Lucas Berger d9efbc1060 fix(quick-260610-i4x-01): replace node-cron schedule() with setInterval in three broker workers
- poller.ts: setInterval(cb, 5 * 60 * 1000) replaces schedule('*/5 * * * *', cb)
- outboxWorker.ts: setInterval(cb, 15 * 1000) replaces schedule('*/15 * * * * *', cb)
- reminderScheduler.ts: setInterval(cb, 60 * 1000) replaces schedule('* * * * *', cb)
- Remove 'import { schedule } from node-cron' from all three files
- Update doc comments to reflect setInterval and document why (node-cron 4.2.1 silent skip)
- Callback bodies and .catch wrappers unchanged; typecheck clean; 91/91 broker tests pass
2026-06-10 13:06:56 -04:00
Lucas Berger 19d92c671b test(260610-hbu): drop unused 'now' param in rowForNow helper 2026-06-10 12:38:37 -04:00
Lucas Berger 93bb2c1c68 test(260610-hbu-02): update reminderScheduler tests for catch-up + per-uid dedup
- Add SINGLE-FIRE: 3 consecutive ticks, exactly 1 dispatch total
- Add MISSED-TICK-RECOVERY: fires at 8-min lead when ideal 15-min tick skipped
- Add ALREADY-STARTED: dtstart<=now returns no rows, 0 dispatches
- Add CR-01 pruning: started-event entry pruned after dtstart passes
- Add D-16: empty subscriptions, zero sends, no crash
- Add T-05-19: per-sub error isolation, both subs attempted when first throws
- Add fan-out: 2 subs -> 2 dispatches for one event
- Rewrite WR-01 test to per-uid dedup language; remove minuteBucket tests
- Update file docblock for catch-up (now, now+16min] window and per-uid dedup
2026-06-10 12:36:23 -04:00
Lucas Berger 3fdb242f7e fix(260610-hbu-01): catch-up window + per-uid dedup in reminderScheduler
- Replace [now+14min, now+16min] window with (now, now+16min] catch-up
- Replace minuteBucket-keyed Set with uid-keyed Map for exactly-once dedup
- Lead-accurate body: 'Starts in N min' (Math.max(1, round(lead/60000)))
- CR-01 pruning: drop entries whose dtstart <= now (event started)
- WR-01 preserved: mark uid sent after all dispatches complete
- Drop gte import; add gt import from drizzle-orm
2026-06-10 12:32:43 -04:00
Lucas Berger 69e5ae8726 fix(06): set hasRrule on EventDetailPopover test fixtures
Plan 06-05 added hasRrule as a required field on the PWA CalendarOccurrence
type (mirroring 06-03's server-side field), but pre-existing fixtures in
EventDetailPopover.test.tsx did not set it — breaking tsc --noEmit / vite build
(vitest passed because esbuild strips types). Both fixtures are non-recurring
single events, so hasRrule: false is correct. Post-merge integration fix.
2026-06-10 11:44:37 -04:00