Commit Graph
443 Commits
Author SHA1 Message Date
Lucas Berger fb30800e9a fix(17): WR-01 trap focus in modal dialogs via useFocusTrap 2026-06-18 13:41:33 -04:00
Lucas Berger a4a7438641 fix(17): WR-05 IN-03 IN-01 resize-aware useIsPhone hook; OidcRedirect side-effect in effect 2026-06-18 13:40:00 -04:00
Lucas BergerandClaude Opus 4.8 b5fcd1d172 fix(17): resolve post-merge wave 2 integration issues
- SettingsSheet: void the navigate('/login') promise (react-router v7 returns Promise) — eslint no-floating-promises
- InstructionSheet.test: wrap SettingsSheet render in MemoryRouter — 17-05 added useNavigate() which needs Router context

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:01:39 -04:00
Lucas Berger 4887ba1b5f chore: merge executor worktree (worktree-agent-a12887546a61a8053) 2026-06-18 12:58:59 -04:00
Lucas Berger aaf754d938 chore: merge executor worktree (worktree-agent-a65077bbd9885bf7e) 2026-06-18 12:58:58 -04:00
Lucas Berger 9e39a5c492 chore: merge executor worktree (worktree-agent-ad448992e936c77aa) 2026-06-18 12:58:57 -04:00
Lucas Berger 944045cd7e test(17-06): add admin two-tab ARIA and keyboard assertions to admin.spec.ts
- Add 'Admin two-tab ARIA strip (D-10)' describe block: tablist visible,
  both named tabs visible, ArrowRight/ArrowLeft keyboard switching,
  panel aria-labelledby, phone overflow check
- Add 'Admin success toast structure (D-08)' describe: role=status absent on load
- Satisfies Wave 0 admin-ARIA CI requirement — 12/12 tests pass on pixel profile
2026-06-18 12:56:34 -04:00
Lucas Berger 620d64138a feat(17-06): add success toasts to admin create-member and reset-password
- Add toast state + 3000ms auto-dismiss useEffect to AdminPage
- Set toast 'Member added.' in createMemberMutation.onSuccess
- Propagate 'Password reset.' up from ResetPasswordSheet via onSuccess callback
- Render role=status/aria-live=polite toast with CheckCircle icon
- Phone toast offset uses calc(var(--bottom-chrome-h) + var(--space-4)) to clear BottomTabBar
2026-06-18 12:53:12 -04:00
Lucas Berger b71238634f feat(17-05): add phone/desktop style branch to CredentialSheet (D-09)
- Add window.matchMedia('(max-width: 767px)') phone check
- Desktop branch: position fixed, top/left 50%, translate(-50%, -50%), maxWidth 480px, borderRadius 12px
- Phone branch: unchanged bottom-sheet (bottom 0, left 0, right 0, borderRadius 12px 12px 0 0)
- role=dialog, aria-modal, aria-label unchanged — only position CSS branched
- SettingsSheet, ChangePasswordSheet, LinkOidcSheet centering in prior commit (Task 1)
2026-06-18 12:52:59 -04:00
Lucas Berger 132a5e4eae feat(17-05): add Sign out control with handleSignOut to SettingsSheet (D-07)
- Add LogOut icon (lucide-react) and fetchLocalLogout (api/client) imports
- Add useNavigate (react-router) for post-logout redirect
- Implement handleSignOut: fire-and-best-effort try/catch, navigate to /login in both branches
- Add Sign out button row at bottom of sheet (44px tap target, var(--color-destructive), divider separator)
- Always navigates to /login even on API failure (server cookie cleared or expired)
2026-06-18 12:52:48 -04:00
Lucas Berger 85a803fba6 test(17-03): add FAB↔BottomTabBar overlap regression guard to layout.spec.ts
- New test: "New Event FAB does not overlap BottomTabBar (A — phone only)"
- Skips on desktop profile; runs iphone + pixel (the profiles that exposed D-01)
- Asserts fabBox.y + fabBox.height <= navBox.y (FAB bottom ≤ bar top)
- Confirms Task 1 fix resolves the seed defect — passes green on both profiles
2026-06-18 12:51:32 -04:00
Lucas Berger df578fd7b7 feat(17-04): wire favicons + theme-color in index.html, fix maskable icon + accent in manifest
- index.html: add favicon.svg (SVG, modern browsers first) + favicon.ico (legacy) links
- index.html: update theme-color from #4A90D9 to approved #e8915a (warm amber)
- vite.config.ts: fix maskable icon — reference /icon-maskable-512.png (separate file) not /icon-512.png
- vite.config.ts: update manifest theme_color to #e8915a to match index.html
- tokens.css: update --color-member-0 to #e8915a (warm amber, Variant B, operator-approved 17-02)
- --sx-color-primary follows automatically via var(--color-member-0)
2026-06-18 12:50:50 -04:00
Lucas Berger ce95aa3e6b feat(17-04): wire logo img into BrandSlot, set --brand-logo-border-radius 0
- Replace placeholder FS div with decorative <img src="/logo.svg" alt="" aria-hidden>
- Apply Surface B-1 style (width/height brand-logo-size, borderRadius token, contain/block)
- Update --brand-logo-border-radius from 50% to 0 (SVG draws its own rx=104 shape)
- h1 FamilySync and tagline p unchanged; LoginPage.tsx untouched (seam contract honored)
- No dangerouslySetInnerHTML (T-05-24 invariant maintained)
2026-06-18 12:48:55 -04:00
Lucas Berger 5e1c714894 fix(17-03): lift FAB above BottomTabBar and reserve phone content padding
- CalendarShell.tsx: FAB bottom changed from var(--space-6) to
  calc(var(--bottom-chrome-h) + var(--space-6)) — sits 24px above bar
- App.tsx: contentStyle gains phone-only paddingBottom: var(--bottom-chrome-h)
  via spread pattern ...(phone ? {...} : {}) — desktop unchanged
2026-06-18 12:48:35 -04:00
Lucas Berger 65e6222944 chore: merge executor worktree (worktree-agent-af89750bf86857964) 2026-06-18 12:44:24 -04:00
Lucas Berger 4c99470c1a feat(17-02): apply approved family-house logo + regenerate icon set
- Overwrite logo.svg with approved higher-contrast family-house SVG
  (warm peach gradient bg, amber roof, bold white walls, heart finial,
  three clearly-spaced family figures in rose/gold/blue)
- Regenerate full icon/favicon set from new art:
  favicon.svg, favicon.ico (967 B), icon-192.png (192x192),
  icon-512.png (512x512), icon-maskable-512.png (512x512 safe-zone),
  apple-touch-icon.png (180x180)
- Approved brand decisions recorded for plan 17-04:
  accent #e8915a, --brand-logo-border-radius: 0
2026-06-18 12:42:43 -04:00
Lucas Berger b364573285 feat(17-02): generate full icon/favicon set from logo.svg (Task 2)
- Run @vite-pwa/assets-generator minimal2023Preset against public/logo.svg
- Produce: favicon.ico (861 B, non-trivial), favicon.svg, icon-192.png (1617 B),
  icon-512.png (4221 B), icon-maskable-512.png (3353 B, 512x512 distinct maskable
  with safe-zone padding), apple-touch-icon.png (1110 B, 180x180)
- Replace 3 placeholder stubs with real generated assets
- Update pwa:icons script to include post-generation canonical rename step
- Add generator intermediate output filenames to .gitignore
2026-06-18 12:13:50 -04:00
Lucas Berger c2f89bd55f feat(17-01): restructure tokens.css to combined :root,[data-theme=light] selector + add --bottom-chrome-h
- Change :root { to :root, [data-theme="light"] { (D-06 combined selector)
- Add --bottom-chrome-h: calc(56px + env(safe-area-inset-bottom, 0px)) to spacing scale
- Keep all 12 --sx-color-* overrides inside the combined block (cascade unchanged)
- Keep --brand-logo-* tokens inside the combined block (verbatim, no value changes)
- Add dark-theme stub as comment-only (Phase 999.20 fills values; no live rule)
- All existing values unchanged; PWA build passes; Schedule-X colors verified via playwright-cli
2026-06-18 12:12:42 -04:00
Lucas Berger 7db9005645 feat(17-02): install @vite-pwa/assets-generator, author logo.svg, add pwa-assets.config.ts
- Add @vite-pwa/assets-generator@1.0.2 as devDependency in apps/pwa
- Add pwa:icons script (pwa-assets-generator generate) to apps/pwa/package.json
- Approve sharp build scripts in pnpm-workspace.yaml (required for @vite-pwa/assets-generator)
- Hand-author apps/pwa/public/logo.svg: warm/rounded/at-home family scene with house, two adults, child, heart — square 512x512 viewBox, self-contained, text-free
- Create apps/pwa/pwa-assets.config.ts with minimal2023Preset, images: ['public/logo.svg'], no overrideManifestIcons
2026-06-18 12:12:17 -04:00
Lucas Berger 1e2cc52659 docs: refresh project documentation against current codebase
Publish / publish (push) Successful in 26s
2026-06-18 06:44:29 -04:00
Lucas BergerandClaude Opus 4.8 b6490feff4 fix(19): satisfy CI fast-checks + secret scan
CI / changes (pull_request) Successful in 9s
CI / api (pull_request) Successful in 3m2s
CI / fast-checks (pull_request) Successful in 4m20s
CI / security (pull_request) Successful in 1m14s
CI / harness (pull_request) Successful in 6m56s
CI / gate (pull_request) Successful in 2s
Lint (eslint --max-warnings 0):
- index.ts: disable no-unsafe-argument on the type-only Context mismatch when
  delegating to the OIDC handler inside the local-session skip wrapper
- localAuth.ts: handleLogout is sync (no await) — drop async (require-await)
- devBypass.ts: disable detect-possible-timing-attacks on the public well-known
  dev-placeholder string compare (not a secret comparison)
- remove dead code / unused bindings flagged by no-unused-vars: makeTestApp
  (localSession.test), makeUnauthContext + BrowserContext import (login.spec),
  unused memberId (admin.test), unused txSelectCount counter (me.test)
- localAuthMiddleware.test / me.test: fix unused + reflow-detached
  eslint-disable directives

Format: prettier --write across the 20 Phase-19 files that were never formatted.

Secret scan (gitleaks): allowlist two false positives — the synthetic >=32-char
TEST_SECRET in localSession.test.ts, and .planning/ design prose (a generic-api-key
regex hit on "credential atomically, 409-equivalent"). Neither is a real secret.

Verified locally: format:check, lint, typecheck, md:lint, gitleaks (no leaks),
PWA 266/266, API 452/452.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 23:05:15 -04:00
Lucas Berger 2691dd0f95 fix(19): IN-04 enforce single-use OIDC-link nonce to prevent state replay 2026-06-17 20:37:09 -04:00
Lucas Berger f02521dd02 fix(19): IN-03 evict stale loginAttempts entries to bound in-memory map growth 2026-06-17 20:35:43 -04:00
Lucas Berger e392bf2eb7 fix(19): IN-02 add lockstep test pinning inlined scrypt params against canonical verifyPassword 2026-06-17 20:34:58 -04:00
Lucas Berger f2fc1404d4 fix(19): IN-01 validate LOCAL_SESSION_EXPIRES coercion (fallback on NaN/non-positive) 2026-06-17 20:34:14 -04:00
Lucas Berger 4bd6b2c057 fix(19): WR-05 add no-echo tests for admin create-member and me password hook sites 2026-06-17 20:33:46 -04:00
Lucas Berger 32bdd1e92d fix(19): WR-07 reject route ids with trailing garbage via strict integer parse 2026-06-17 20:32:26 -04:00
Lucas Berger 4cf2ad4bff fix(19): WR-06 stop sliding the rate-limit cooldown window on rejected attempts 2026-06-17 20:31:07 -04:00
Lucas Berger 30ad25c026 fix(19): WR-03 make scrypt hashing async (threadpool) to avoid event-loop starvation DoS 2026-06-17 20:30:28 -04:00
Lucas Berger 322929aebe fix(19): WR-02+WR-04 centralize OIDC config (env-or-app_config) and discover auth endpoint 2026-06-17 20:26:47 -04:00
Lucas Berger c4d8d76a4c fix(19): WR-01 parse reset-admin flags explicitly and stop echoing username 2026-06-17 20:25:18 -04:00
Lucas Berger 40666e1cc5 fix(19): BL-04 keep context oidcIss/oidcSub null for local users (no fabricated identity sentinels) 2026-06-17 20:24:01 -04:00
Lucas Berger 71537601ce fix(19): BL-03 reject OIDC-link bind on empty iss/sub or mismatched local session (takeover guard) 2026-06-17 20:22:37 -04:00
Lucas Berger cd095e5b67 fix(19): BL-02 match Secure attribute on logout cookie deletion to issue-time logic 2026-06-17 20:21:51 -04:00
Lucas Berger 3674b255b2 fix(19): BL-01 enforce LOCAL_SESSION_SECRET length floor in devSessionCookieMiddleware 2026-06-17 20:21:17 -04:00
Lucas Berger b083cb7193 fix(19): CR-04 scope login lockout to username, add TTL auto-expiry + admin-reset unlock 2026-06-17 20:20:43 -04:00
Lucas Berger 6ef8e03f8c fix(19): CR-03 return 403 for wrong current password so change-password does not log user out 2026-06-17 20:17:32 -04:00
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 82391874ee feat(19-05): add break-glass reset-admin CLI (dev-only, .dockerignore'd)
- Create apps/api/scripts/reset-admin.ts (AUTH-LOCAL-11, D-13, D-15)
- Dev-only guard as FIRST executable statement (NODE_ENV=production throws)
- Inline scrypt PHC hashPassword (cannot import compiled TS, Pitfall 11)
- Parse --username/--password from argv; never log password value (T-19-26)
- --dry-run validates args + DB connection without writing
- Upserts users (is_admin=true) + local_credentials rows idempotently
- Script excluded from prod image via .dockerignore apps/api/scripts/ (IMG-02)
- dry-run: exit=0, no password in output verified
2026-06-17 17:13:04 -04:00
Lucas Berger 3094df84c8 feat(19-05): Option C — devSessionCookieMiddleware issues real local-session cookie under bypass
- Add devSessionCookieMiddleware() to devBypass.ts (production hard-guard FIRST)
- Issues local-session JWT cookie for DEV_USER when no cookie present under bypass
- Pure no-op when NODE_ENV=production, DEV_AUTH_BYPASS!=true, or secret not set
- Mount devSessionCookieMiddleware() after devAuthBypass() in index.ts
- Existing devBypass tests: 3/3 pass; typecheck: exit 0
2026-06-17 17:11:31 -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 Berger 9b569efeab feat(19-03): wire /callback link branch, OIDC-guard skip, de-Authelia comments
- /callback: reads signed state, extracts linkUserId, calls linkOidcToUser after OIDC session set; OidcLinkConflictError redirects to /?error=oidc-link-conflict
- OIDC guard: oidcAuthMiddleware() factory called once at construction, handler invoked per-request inside skip-when-user-set wrapper (D-03)
- middleware.ts: de-Authelia-ize comments — generic OIDC identity provider language (D-06, AUTH-LOCAL-18)
- localAuthMiddleware.ts: cast to typeof DEV_USER for ContextVariableMap type compatibility
- All 446 tests pass; typecheck clean
2026-06-17 16:59:06 -04:00
Lucas Berger c437f408bb feat(19-03): implement POST /api/auth/local/login (rate-limit + lockout) + logout
- Rate-limit: per-IP in-memory Map; 5 failures → 429, 10 → 423 (lockedOut)
- Counter increments even on 429 so brute-force accumulates toward lockout
- Timing-safe: DUMMY_HASH ensures verifyPassword runs for unknown usernames (T-19-12)
- noEchoHook: Zod errors never echo submitted values (T-19-14)
- Same 401 body for wrong-password and unknown-username (no enumeration)
- POST+GET /local/logout clear the local-session cookie
2026-06-17 16:49:59 -04:00