Commit Graph
1017 Commits
Author SHA1 Message Date
Lucas Berger ecdd94a907 docs(11): UI design contract for per-event reminders 2026-06-13 20:52:33 -04:00
Lucas Berger 771608d3d4 docs(11): capture phase context for per-event reminders 2026-06-13 20:49:25 -04:00
luckberg 770e7baa32 Merge pull request 'Isolate local API tests to a dedicated familysync_test DB (quick 260613-ndv)' (#18) from quick/260613-ndv-test-db-isolation into main
Publish / publish (push) Successful in 19s
Reviewed-on: #18
2026-06-13 20:32:44 -04:00
Lucas Berger 4517432dca style(quick-260613-ndv): prettier-format global-setup.ts
CI / changes (pull_request) Successful in 2s
CI / fast-checks (pull_request) Successful in 1m24s
CI / api (pull_request) Successful in 1m3s
CI / harness (pull_request) Successful in 4m9s
CI / security (pull_request) Successful in 39s
CI / gate (pull_request) Successful in 1s
2026-06-13 17:15:45 -04:00
Lucas Berger e687cb96e7 docs(quick-260613-ndv): plan + summary + state for test-DB isolation 2026-06-13 17:15:45 -04:00
Lucas Berger 24cb7569bf fix(quick-260613-ndv): reset familysync_test each run (truncate-all)
CREATE DATABASE IF NOT EXISTS reuses the prior run's data, so the test DB
would itself accumulate users run-over-run (the same flaky list_shares
fan-out the dev DB suffered). Truncate every table (except the drizzle
migration ledger) at globalSetup start, FK-safe, so each run is a clean
deterministic slate. Verified: familysync_test users 186 -> 93 across a run
(reset, not doubled); dev familysync untouched at 3.
2026-06-13 17:14:51 -04:00
Lucas Berger f39bd308b2 chore(quick-260613-ndv-02): clean-slate comment in setup.ts + README local-test docs
- Update apps/api/test/setup.ts header: clarify tests run against familysync_test
  (provisioned by global-setup.ts), document users-cleanup decision (intact across
  tests), and note CI-vs-local env difference
- Add apps/api/README.md "Running API tests locally" section: documents the test
  DB isolation, run command, DB_ROOT_PASSWORD requirement, and CI no-op behaviour
- Fix apps/api/test/global-setup.ts: switch from drizzle({ client, mode }) to
  drizzle(pool, { mode }) — drizzle-orm@0.45.2 isConfig() has a tautological OR
  in the `mode` branch that always returns false, causing the combined-config form
  to pass the config object as the client (client.query is not a function); two-arg
  form routes correctly; 244/244 tests pass against familysync_test
2026-06-13 17:14:51 -04:00
Lucas Berger 1eda6678bc chore(quick-260613-ndv-01): add globalSetup for familysync_test isolation
- Create apps/api/test/global-setup.ts: root-provisions + grants + migrates
  familysync_test (local only); no-op when process.env.CI is truthy (T-ndv-04)
- Update apps/api/vitest.config.ts: wire globalSetup; add CI-gated test.env
  override (DB_NAME=familysync_test, DB_HOST) so workers never touch dev DB
2026-06-13 17:14:51 -04:00
luckberg 6f566f23a2 Merge pull request 'Phase 10 — Admin Role & Settings (ADMIN-01/02/03)' (#17) from gsd/phase-10-admin-role-settings into main
Publish / publish (push) Successful in 24s
Reviewed-on: #17
2026-06-13 17:10:45 -04:00
luckberg f2e228360a Merge branch 'main' into gsd/phase-10-admin-role-settings
CI / changes (pull_request) Successful in 2s
CI / fast-checks (pull_request) Successful in 1m26s
CI / api (pull_request) Successful in 1m3s
CI / harness (pull_request) Successful in 4m11s
CI / security (pull_request) Successful in 39s
CI / gate (pull_request) Successful in 1s
2026-06-13 16:50:06 -04:00
Lucas Berger 6dff7aa012 docs(roadmap): add Phase 17 (UI Optimization & Polish) + document phone bottom-bar overlap
Long-standing PWA phone-layout defect (BottomTabBar from Phase 04, not a
Phase 10 regression): the fixed bottom bar overlaps the New Event FAB and
the calendar colour legend at <=767px. Added Phase 17 to the v1.1 roadmap
with the defect as the seed item + fix sketch, and a pending todo
(resolves_phase: 17) with full repro/file detail.
2026-06-13 16:43:00 -04:00
Lucas Berger 2067b8c91c docs(phase-10): evolve PROJECT.md after phase completion
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 4m17s
CI / security (pull_request) Successful in 39s
CI / gate (pull_request) Successful in 1s
2026-06-13 15:47:15 -04:00
Lucas Berger 94c62e213e docs(phase-10): complete phase execution 2026-06-13 15:46:09 -04:00
Lucas Berger 2f347cbd98 fix(10): guard shared-calendar designation against non-existent target (CR-01)
PUT /api/admin/calendars/:id/shared cleared the current shared calendar then
set the target in two non-transactional UPDATEs without checking the target
exists — a bad/stale id wiped the family shared lane and still returned ok.
Verify the target inside a transaction; return 404 when absent. Adds a
regression test (RED→GREEN).
2026-06-13 15:38:35 -04:00
Lucas Berger ebde3e1d08 docs(10): add code review report 2026-06-13 15:35:03 -04:00
Lucas Berger 3693da6d0b docs(10-04): complete PWA admin surfaces plan
- SUMMARY.md for Plan 04 (AdminPage, CredentialSheet, SetupBanner, nav gating)
- STATE.md: plan advanced to last, progress 100%, decisions recorded
- ROADMAP.md: Phase 10 marked Complete (4/4 summaries)
2026-06-13 15:28:03 -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 0f41993a95 docs(10-03): complete admin-api-surface plan summary and state update 2026-06-13 14:57:30 -04:00
Lucas Berger d2f6d5d77b feat(10-03): implement credentialSync helper, adminRouter, and self-service /api/me/credential
credentialSync.ts:
- validateEncryptAndStoreCredential(userId, email, appPassword, providerType) — single
  shared validate→encrypt→store→initial-sync path used by BOTH admin and self-service
- createFastmailClient + fetchCalendars wrapped in ONE try/catch: any failure throws
  CredentialValidationError (routes map to { error: 'Invalid request' } 400)
- appPassword never logged or echoed (T-10-10)
- encryptPassword (AES-256-GCM) applied before DB write (T-10-11)
- fire-and-forget initial sync via loadClientForUser + syncCalendar (Pitfall 5)

admin.ts:
- adminRouter.use('*', requireAdmin) FIRST (Pitfall 9 / T-10-08)
- GET /members: users LEFT JOIN member_credentials → hasCredential boolean
- POST /credentials: noEchoHook + validateEncryptAndStoreCredential (T-10-09)
- GET /calendars: calendar list (UI-SPEC Surface 5)
- PUT /calendars/:id/shared: exclusive is_shared update (ADMIN-02, D-06)

index.ts:
- app.route('/api/admin', adminRouter) mounted in route block

me.ts:
- POST /credential: member self-service, always currentUserId (Pitfall 6 / T-10-12)
- meCredentialSchema (no userId field), meNoEchoHook, calls shared helper
- All 17 new admin tests pass; 270 total pass; tsc --noEmit clean
2026-06-13 14:54:23 -04:00
Lucas Berger 037a7ed4c1 test(10-03): add RED tests for adminRouter guard, credential no-echo, shared-calendar, self-service
RED phase: all admin.test.ts tests fail (404 — routes/mounts not yet created).
Tests cover:
- T-10-08 Pitfall 9: 403 for non-admin on every /api/admin/* route
- T-10-09 Pitfall 7: 400 with no echoed password for all credential failure modes
  (PROPFIND/auth failure, createFastmailClient throw, network error, schema mismatch)
- T-10-11: valid credential stores encrypted (AES-256-GCM), not plaintext
- ADMIN-02: PUT /api/admin/calendars/:id/shared — exclusive is_shared=1
- T-10-12 Pitfall 6: POST /api/me/credential ignores body userId, writes to session user
- D-07: non-admin member can POST /api/me/credential (no requireAdmin on self-service)
2026-06-13 14:49:35 -04:00
Lucas Berger ac36e106ef feat(10-03): export loadClientForUser and triggerTargetedResync from outboxWorker
- Add export keyword to loadClientForUser (line 271)
- Add export keyword to triggerTargetedResync (line 302)
- Function bodies unchanged (no behavior change, no node-cron reintroduced)
- Enables credentialSync.ts shared helper to call post-credential full-member sync
2026-06-13 14:46:54 -04:00
Lucas Berger a5d88f75aa docs(10-02): complete admin-role-primitives plan summary and state update 2026-06-13 14:39:49 -04:00
Lucas Berger 1adff61cec feat(10-02): extend /api/me with isAdmin + needsProviderSetup (D-03)
- dev-bypass path: DB lookup for users.isAdmin (T-10-05 bypass skips OIDC not DB)
- OIDC path: same resolveAdminAndSetupStatus helper after upsertUser
- needsProviderSetup: true when no member_credentials row, false when one exists
- no /api/me/credential POST added here (Plan 03)
2026-06-13 14:37:38 -04:00
Lucas Berger e5889df03e test(10-02): add failing /api/me isAdmin+needsProviderSetup tests (RED)
- dev-bypass path: isAdmin from DB (not hardcoded), needsProviderSetup from member_credentials
- needsProviderSetup=true when no member_credentials row exists
- needsProviderSetup=false when member_credentials row exists
2026-06-13 14:36:45 -04:00
Lucas Berger 72e0140f01 feat(10-02): add first-login-wins is_admin bootstrap in upsertUser (D-01)
- zero-admin COUNT check before INSERT: first user gets is_admin=true
- subsequent users (admin already exists) get is_admin=false
- existing-user early-return path unchanged (is_admin not modified)
- Phase-12 hook comment: tighten to first login after app_config.setup_complete
- adds 'import { sql }' from drizzle-orm
2026-06-13 14:35:11 -04:00
Lucas Berger 9e1507f7a8 test(10-02): add failing upsertUser is_admin bootstrap tests (RED)
- first user with zero admins → is_admin=true in INSERT values
- subsequent user with admin present → is_admin=false in INSERT values
- existing user re-upsert → is_admin unchanged (early-return path, no insert)
- update existing color tests to accommodate new 4-select flow order
2026-06-13 14:33:53 -04:00
Lucas Berger f9c70ab6a8 feat(10-02): implement requireAdmin DB-backed MiddlewareHandler
- reads users.isAdmin from DB (never trusts context user's isAdmin claim)
- 403 with { error: 'Forbidden' } for non-admins and missing user
- side-effect import of devBypass.js for ContextVariableMap augmentation
- bypass path skips OIDC only, not the DB check (T-10-04/T-10-05)
2026-06-13 14:32:04 -04:00
Lucas Berger 92179302a2 test(10-02): add failing requireAdmin middleware tests (RED)
- 403 for non-admin user (is_admin=false in DB)
- next() called for admin user (is_admin=true in DB)
- 403 when no user on context (no DB query)
- 403 when context user spoofs isAdmin=true but DB has is_admin=false (T-10-04)
2026-06-13 14:30:28 -04:00
Lucas Berger 6405a93742 docs(10-01): complete v1.1 DB foundation plan summary and state update 2026-06-13 14:25:39 -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 ad7ba3ae4e chore(10-01): generate + apply v1.1 DB migration (0001_famous_mad_thinker)
- Generated via drizzle-kit generate from updated schema.ts
- SQL is additive-only: CREATE TABLE app_config + ALTER TABLE ADD COLUMN (3x) + ADD CONSTRAINT UNIQUE
- Applied to live dev MariaDB via direct SQL execution (drizzle-kit migrate journal hash mismatch
  with legacy migration tracking; DDL applied + hash recorded in __drizzle_migrations manually)
- Verified: MIGRATION OK via live DB SHOW COLUMNS / SHOW TABLES query
- No DROP/TRUNCATE statements in generated SQL (grep returns 0)
2026-06-13 14:22:45 -04:00
Lucas Berger d0a4cb4e35 feat(10-01): add v1.1 schema bundle (is_admin, provider_type, reminder_lead_minutes, app_config)
- users.isAdmin: boolean NOT NULL DEFAULT false (first-login-wins admin flag, D-01)
- memberCredentials.providerType: varchar(64) NOT NULL DEFAULT 'caldav' (generic provider discriminator, D-04)
- memberCredentials: UNIQUE(user_id) constraint for one-credential-per-member + upsert support (D-05)
- calendarEvents.reminderLeadMinutes: int nullable (created now, consumed by Phase 11)
- appConfig table: key VARCHAR PK, value TEXT, updated_at (setup_complete consumed by Phase 12)
2026-06-13 14:20:10 -04:00
Lucas Berger df92bd89e8 docs(10): point STATE current-phase at Phase 10 2026-06-13 14:13:04 -04:00
Lucas Berger d27b13646c chore(graphs): regenerate knowledge graph 2026-06-13 14:11:39 -04:00
Lucas Berger 159f37fe6a docs(10): record planning completion, annotate roadmap waves, add pattern map 2026-06-13 14:11:39 -04:00
Lucas Berger a944dcd881 docs(10): revise phase plan per plan-checker feedback (3 blockers, 2 warnings) 2026-06-13 14:07:22 -04:00
Lucas Berger b24fbbfde7 docs(10): create phase plan (4 plans, 4 waves) for admin-role-settings
- 10-01 v1.1 DB foundation migration + dev-bypass admin seed
- 10-02 requireAdmin guard + first-login-wins + /api/me extension (TDD)
- 10-03 adminRouter credentials/shared-calendar + member self-service (TDD)
- 10-04 PWA /admin route + nav gating + CredentialSheet + SetupBanner
- filled 10-VALIDATION Per-Task Verification Map (Nyquist compliant)
- finalized ROADMAP Phase 10 plan list
2026-06-13 13:57:51 -04:00
Lucas Berger 7af08c97c5 docs(10): add validation strategy 2026-06-13 13:40:41 -04:00
Lucas Berger 382b346b16 docs(10): research phase — admin role & settings 2026-06-13 13:38:51 -04:00
Lucas Berger ca6947cfbf docs: add backlog item 999.18 — update dependencies as found during ci 2026-06-13 13:23:32 -04:00
Lucas BergerandClaude Sonnet 4.6 de092e84c0 docs(10): UI design contract for admin-role-settings phase
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 13:20:40 -04:00
Lucas Berger cb3ae9081f docs(10): fold backlog 999.5 self-service onboarding into CONTEXT (D-07), reconcile with ROADMAP 2026-06-13 13:17:22 -04:00
luckberg efa42801b5 Merge pull request 'ci: skip Docker publish for .gitea/.planning-only pushes' (#16) from quick/260613-fp9-publish-paths-ignore into main
Reviewed-on: #16
2026-06-13 12:17:51 -04:00
Lucas Berger 15fa89d483 docs(quick-260613-fp9): document ci.yml heavy-job paths-filter fix
CI / changes (pull_request) Successful in 3s
CI / fast-checks (pull_request) Successful in 1m36s
CI / api (pull_request) Has been skipped
CI / harness (pull_request) Has been skipped
CI / security (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 2s
2026-06-13 12:13:58 -04:00
Lucas Berger ec85fe026a ci(260613-fp9): fix changes paths-filter so doc/.gitea/.planning PRs skip heavy jobs
dorny/paths-filter combines patterns with Array.some, and picomatch compiles
'!.gitea/**' as 'any path not under .gitea'. The '- !.gitea/**' line (from
quick task 260613-dmw) therefore matched every non-.gitea file — including
.planning/** and *.md — so code=true for doc-only PRs and the heavy api/harness
jobs ran anyway. It also never excluded .gitea (the **/*.yml glob already
matched workflow files), regressing the Phase 15 doc-only skip.

Switch to predicate-quantifier 'every' with negation-only globs: a file counts
as code only if it is outside .gitea/, outside .planning/, and not Markdown.
Verified with picomatch against representative file sets.
2026-06-13 12:13:58 -04:00
Lucas Berger 4d4ffad059 docs(quick-260613-fp9): .gitea/.planning pushes should not trigger a docker image publish
CI / changes (pull_request) Successful in 2s
CI / fast-checks (pull_request) Successful in 1m24s
CI / api (pull_request) Successful in 1m0s
CI / harness (pull_request) Successful in 3m53s
CI / security (pull_request) Successful in 39s
CI / gate (pull_request) Successful in 1s
2026-06-13 11:22:16 -04:00
Lucas Berger cd5a88c8a2 ci(260613-fp9): skip Docker publish for .gitea/.planning-only pushes
Add paths-ignore ['.gitea/**', '.planning/**'] to publish.yml's push
trigger. Doc/CI-only pushes to main (planning docs push straight to main;
workflow edits merge via PR) produce a byte-identical image (.dockerignore
already excludes .planning), so the build + :latest re-push was wasted.
paths-ignore skips only when every changed file matches; mixed code+docs
pushes still publish.
2026-06-13 11:22:05 -04:00