Commit Graph
806 Commits
Author SHA1 Message Date
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
Lucas Berger 7478c0fa50 docs(15): mark Gitea branch-protection checkpoint resolved (verified on main)
Publish / publish (push) Successful in 9s
2026-06-13 11:14:20 -04:00
Lucas Berger 0a28a438a8 docs(16): confirm live-CI human-verify items (gitleaks scan + boot-smoke) from PR #15 run #51/#52
Publish / publish (push) Successful in 8s
2026-06-13 10:26:38 -04:00
luckberg 06238a9781 Merge pull request 'Phase 16: CI dependency audit & security checks' (#15) from gsd/phase-16-ci-dependency-audit-and-security-checks into main
Publish / publish (push) Successful in 1m36s
Reviewed-on: #15
2026-06-13 10:00:59 -04:00
Lucas Berger 9be3b7ba37 docs(quick-260613-dmw): exclude .gitea/** from CI heavy-job paths-filter
CI / changes (pull_request) Successful in 2s
CI / fast-checks (pull_request) Successful in 1m23s
CI / api (pull_request) Successful in 1m1s
CI / harness (pull_request) Successful in 3m51s
CI / security (pull_request) Successful in 39s
CI / gate (pull_request) Successful in 2s
2026-06-13 09:53:25 -04:00
Lucas Berger 2d329a97b2 ci(260613-dmw): exclude .gitea/** from heavy-job paths-filter (treat CI edits like docs)
Workflow-only edits shouldn't pay for the multi-minute MariaDB/integration/harness
suite. A negation '- !.gitea/**' (ordered after the yml/yaml globs) makes CI-config-
only PRs resolve code=false so api/harness skip like docs. fast-checks (format:check
still validates the workflow YAML) + gate continue to gate them. PRs that also touch
app code or lockfile still resolve code=true and run the full suite.
2026-06-13 09:51:41 -04:00
Lucas Berger 8154ba6f35 style(16): apply prettier formatting to satisfy CI format:check
CI / changes (pull_request) Successful in 2s
CI / fast-checks (pull_request) Successful in 1m23s
CI / api (pull_request) Successful in 1m0s
CI / harness (pull_request) Successful in 3m54s
CI / security (pull_request) Successful in 40s
CI / gate (pull_request) Successful in 1s
2026-06-13 09:29:02 -04:00
Lucas Berger c72e013a7b docs(phase-16): complete phase execution
CI / changes (pull_request) Successful in 3s
CI / fast-checks (pull_request) Failing after 57s
CI / api (pull_request) Successful in 58s
CI / harness (pull_request) Successful in 3m59s
CI / security (pull_request) Successful in 40s
CI / gate (pull_request) Failing after 1s
2026-06-13 09:00:12 -04:00
Lucas Berger e0ec4a40a5 docs(16): re-review clean + code review fix report 2026-06-13 08:52:12 -04:00
Lucas Berger bb1e97556d fix(16): fail closed on unparseable audit-waiver expiry date 2026-06-13 08:51:53 -04:00
Lucas Berger 3e609b2550 fix(16): WR-04 relabel/clarify outdated advisory tier (no false promise)
The AUDIT-ADVISORY tier cross-checks pnpm outdated (direct deps) against
advisory module_names (mostly transitive), so it almost never fires.
Relabel to OUTDATED-WITH-ADVISORY and document the limitation in the
header, header text, and inline comment; point to check-audit.mjs as the
authoritative advisory gate. No risky full-tree rewrite.
2026-06-13 08:43:54 -04:00
Lucas Berger 3daa351d70 fix(16): WR-02/WR-05 robust boot-smoke and active .dockerignore checks
WR-02: capture docker run exit directly (not the piped head exit) so a
chatty booting image can't SIGPIPE to 141 and false-PASS; require the
FATAL guard marker in output as a positive assertion.
WR-05: strip comment lines and use anchored fixed-string (grep -qF)
matching so a commented-out rule can't satisfy the hygiene check and
patterns aren't treated as regexes.
2026-06-13 08:43:16 -04:00