Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d27b13646c | ||
|
|
159f37fe6a | ||
|
|
a944dcd881 |
+13
-2
@@ -170,12 +170,23 @@ Plans:
|
|||||||
|
|
||||||
**Folded-in scope** (from backlog 999.5, self-service member onboarding): the credential surface this phase builds is the same one a member needs on first login. Expose a `needsProviderSetup` signal (member has no `member_credentials` row) and let a member enter/validate (CalDAV PROPFIND) + encrypt their **own** Fastmail app password — the self-service counterpart of the admin-managed flow, sharing the validation/encryption/initial-sync path. Non-technical-friendly instructions (link to Fastmail's app-password page, required Calendars/CalDAV scope) are the hard UX constraint. Member-scoped: a member can only set their own credential; never log/echo the password.
|
**Folded-in scope** (from backlog 999.5, self-service member onboarding): the credential surface this phase builds is the same one a member needs on first login. Expose a `needsProviderSetup` signal (member has no `member_credentials` row) and let a member enter/validate (CalDAV PROPFIND) + encrypt their **own** Fastmail app password — the self-service counterpart of the admin-managed flow, sharing the validation/encryption/initial-sync path. Non-technical-friendly instructions (link to Fastmail's app-password page, required Calendars/CalDAV scope) are the hard UX constraint. Member-scoped: a member can only set their own credential; never log/echo the password.
|
||||||
|
|
||||||
**Plans**: 4 plans (4 waves)
|
**Plans**: 4 plans (4 waves)Plans:
|
||||||
Plans:
|
**Wave 1**
|
||||||
|
|
||||||
- [ ] 10-01-PLAN.md — v1.1 DB foundation migration (is_admin, provider_type+unique, reminder_lead_minutes, app_config) + dev-bypass admin seed
|
- [ ] 10-01-PLAN.md — v1.1 DB foundation migration (is_admin, provider_type+unique, reminder_lead_minutes, app_config) + dev-bypass admin seed
|
||||||
|
|
||||||
|
**Wave 2** *(blocked on Wave 1 completion)*
|
||||||
|
|
||||||
- [ ] 10-02-PLAN.md — requireAdmin guard + first-login-wins bootstrap + /api/me isAdmin/needsProviderSetup (TDD)
|
- [ ] 10-02-PLAN.md — requireAdmin guard + first-login-wins bootstrap + /api/me isAdmin/needsProviderSetup (TDD)
|
||||||
|
|
||||||
|
**Wave 3** *(blocked on Wave 2 completion)*
|
||||||
|
|
||||||
- [ ] 10-03-PLAN.md — adminRouter (members/credentials/calendars/shared) + member self-service credential, validate→encrypt→sync (TDD)
|
- [ ] 10-03-PLAN.md — adminRouter (members/credentials/calendars/shared) + member self-service credential, validate→encrypt→sync (TDD)
|
||||||
|
|
||||||
|
**Wave 4** *(blocked on Wave 3 completion)*
|
||||||
|
|
||||||
- [ ] 10-04-PLAN.md — PWA /admin route + nav gating + CredentialSheet + SetupBanner (playwright-cli verified)
|
- [ ] 10-04-PLAN.md — PWA /admin route + nav gating + CredentialSheet + SetupBanner (playwright-cli verified)
|
||||||
|
|
||||||
**UI hint**: yes
|
**UI hint**: yes
|
||||||
|
|
||||||
### Phase 11: Per-Event Reminders
|
### Phase 11: Per-Event Reminders
|
||||||
|
|||||||
+8
-8
@@ -2,10 +2,10 @@
|
|||||||
gsd_state_version: 1.0
|
gsd_state_version: 1.0
|
||||||
milestone: v1.1
|
milestone: v1.1
|
||||||
milestone_name: Operability & Polish
|
milestone_name: Operability & Polish
|
||||||
status: verifying
|
status: executing
|
||||||
stopped_at: Completed 16-05-PLAN.md
|
stopped_at: Phase 10 UI-SPEC approved
|
||||||
last_updated: "2026-06-13T12:59:54.942Z"
|
last_updated: "2026-06-13T18:10:25.965Z"
|
||||||
last_activity: 2026-06-13
|
last_activity: "2026-06-13 - Completed quick task 260613-fp9: .gitea/.planning-only pushes skip the Docker publish"
|
||||||
progress:
|
progress:
|
||||||
total_phases: 19
|
total_phases: 19
|
||||||
completed_phases: 7
|
completed_phases: 7
|
||||||
@@ -27,7 +27,7 @@ See: .planning/PROJECT.md (updated 2026-06-10)
|
|||||||
|
|
||||||
Phase: 999.1
|
Phase: 999.1
|
||||||
Plan: Not started
|
Plan: Not started
|
||||||
Status: Phase complete — ready for verification
|
Status: Ready to execute
|
||||||
Last activity: 2026-06-13 - Completed quick task 260613-fp9: .gitea/.planning-only pushes skip the Docker publish
|
Last activity: 2026-06-13 - Completed quick task 260613-fp9: .gitea/.planning-only pushes skip the Docker publish
|
||||||
|
|
||||||
### ✅ Resolved Checkpoint — Phase 15 Plan 15-03 Task 2 (human-action)
|
### ✅ Resolved Checkpoint — Phase 15 Plan 15-03 Task 2 (human-action)
|
||||||
@@ -235,9 +235,9 @@ Recent decisions affecting current work:
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-06-13T12:28:17.736Z
|
Last session: 2026-06-13T17:21:38.274Z
|
||||||
Stopped at: Completed 16-05-PLAN.md
|
Stopped at: Phase 10 UI-SPEC approved
|
||||||
Resume file: None
|
Resume file: .planning/phases/10-admin-role-settings/10-UI-SPEC.md
|
||||||
|
|
||||||
## Operator Next Steps
|
## Operator Next Steps
|
||||||
|
|
||||||
|
|||||||
+510
-115
@@ -1,16 +1,16 @@
|
|||||||
# Graph Report - familysync (2026-06-11)
|
# Graph Report - familysync (2026-06-13)
|
||||||
|
|
||||||
## Corpus Check
|
## Corpus Check
|
||||||
- 419 files · ~1,042,133 words
|
- 485 files · ~924,212 words
|
||||||
- Verdict: corpus is large enough that graph structure adds value.
|
- Verdict: corpus is large enough that graph structure adds value.
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
- 5924 nodes · 6053 edges · 575 communities (522 shown, 53 thin omitted)
|
- 6798 nodes · 6885 edges · 664 communities (601 shown, 63 thin omitted)
|
||||||
- Extraction: 100% EXTRACTED · 0% INFERRED · 0% AMBIGUOUS · INFERRED: 5 edges (avg confidence: 0.8)
|
- Extraction: 100% EXTRACTED · 0% INFERRED · 0% AMBIGUOUS · INFERRED: 5 edges (avg confidence: 0.8)
|
||||||
- Token cost: 0 input · 0 output
|
- Token cost: 0 input · 0 output
|
||||||
|
|
||||||
## Graph Freshness
|
## Graph Freshness
|
||||||
- Built from commit: `63ae0c69`
|
- Built from commit: `382b346b`
|
||||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||||
- Run `graphify update .` after code changes (no API cost).
|
- Run `graphify update .` after code changes (no API cost).
|
||||||
|
|
||||||
@@ -526,8 +526,22 @@
|
|||||||
- [[_COMMUNITY_Community 523|Community 523]]
|
- [[_COMMUNITY_Community 523|Community 523]]
|
||||||
- [[_COMMUNITY_Community 524|Community 524]]
|
- [[_COMMUNITY_Community 524|Community 524]]
|
||||||
- [[_COMMUNITY_Community 525|Community 525]]
|
- [[_COMMUNITY_Community 525|Community 525]]
|
||||||
|
- [[_COMMUNITY_Community 534|Community 534]]
|
||||||
|
- [[_COMMUNITY_Community 535|Community 535]]
|
||||||
|
- [[_COMMUNITY_Community 536|Community 536]]
|
||||||
|
- [[_COMMUNITY_Community 537|Community 537]]
|
||||||
|
- [[_COMMUNITY_Community 538|Community 538]]
|
||||||
|
- [[_COMMUNITY_Community 539|Community 539]]
|
||||||
|
- [[_COMMUNITY_Community 540|Community 540]]
|
||||||
|
- [[_COMMUNITY_Community 541|Community 541]]
|
||||||
|
- [[_COMMUNITY_Community 542|Community 542]]
|
||||||
|
- [[_COMMUNITY_Community 543|Community 543]]
|
||||||
|
- [[_COMMUNITY_Community 544|Community 544]]
|
||||||
|
- [[_COMMUNITY_Community 545|Community 545]]
|
||||||
|
- [[_COMMUNITY_Community 546|Community 546]]
|
||||||
- [[_COMMUNITY_Community 547|Community 547]]
|
- [[_COMMUNITY_Community 547|Community 547]]
|
||||||
- [[_COMMUNITY_Community 548|Community 548]]
|
- [[_COMMUNITY_Community 548|Community 548]]
|
||||||
|
- [[_COMMUNITY_Community 549|Community 549]]
|
||||||
- [[_COMMUNITY_Community 550|Community 550]]
|
- [[_COMMUNITY_Community 550|Community 550]]
|
||||||
- [[_COMMUNITY_Community 551|Community 551]]
|
- [[_COMMUNITY_Community 551|Community 551]]
|
||||||
- [[_COMMUNITY_Community 552|Community 552]]
|
- [[_COMMUNITY_Community 552|Community 552]]
|
||||||
@@ -560,23 +574,96 @@
|
|||||||
- [[_COMMUNITY_Community 579|Community 579]]
|
- [[_COMMUNITY_Community 579|Community 579]]
|
||||||
- [[_COMMUNITY_Community 580|Community 580]]
|
- [[_COMMUNITY_Community 580|Community 580]]
|
||||||
- [[_COMMUNITY_Community 582|Community 582]]
|
- [[_COMMUNITY_Community 582|Community 582]]
|
||||||
|
- [[_COMMUNITY_Community 588|Community 588]]
|
||||||
|
- [[_COMMUNITY_Community 589|Community 589]]
|
||||||
|
- [[_COMMUNITY_Community 590|Community 590]]
|
||||||
|
- [[_COMMUNITY_Community 591|Community 591]]
|
||||||
|
- [[_COMMUNITY_Community 592|Community 592]]
|
||||||
- [[_COMMUNITY_Community 593|Community 593]]
|
- [[_COMMUNITY_Community 593|Community 593]]
|
||||||
|
- [[_COMMUNITY_Community 594|Community 594]]
|
||||||
|
- [[_COMMUNITY_Community 595|Community 595]]
|
||||||
|
- [[_COMMUNITY_Community 596|Community 596]]
|
||||||
|
- [[_COMMUNITY_Community 597|Community 597]]
|
||||||
|
- [[_COMMUNITY_Community 598|Community 598]]
|
||||||
|
- [[_COMMUNITY_Community 599|Community 599]]
|
||||||
|
- [[_COMMUNITY_Community 600|Community 600]]
|
||||||
|
- [[_COMMUNITY_Community 601|Community 601]]
|
||||||
|
- [[_COMMUNITY_Community 602|Community 602]]
|
||||||
|
- [[_COMMUNITY_Community 603|Community 603]]
|
||||||
|
- [[_COMMUNITY_Community 604|Community 604]]
|
||||||
|
- [[_COMMUNITY_Community 605|Community 605]]
|
||||||
|
- [[_COMMUNITY_Community 606|Community 606]]
|
||||||
|
- [[_COMMUNITY_Community 607|Community 607]]
|
||||||
|
- [[_COMMUNITY_Community 608|Community 608]]
|
||||||
|
- [[_COMMUNITY_Community 609|Community 609]]
|
||||||
|
- [[_COMMUNITY_Community 610|Community 610]]
|
||||||
|
- [[_COMMUNITY_Community 611|Community 611]]
|
||||||
|
- [[_COMMUNITY_Community 612|Community 612]]
|
||||||
|
- [[_COMMUNITY_Community 613|Community 613]]
|
||||||
|
- [[_COMMUNITY_Community 614|Community 614]]
|
||||||
|
- [[_COMMUNITY_Community 615|Community 615]]
|
||||||
|
- [[_COMMUNITY_Community 616|Community 616]]
|
||||||
|
- [[_COMMUNITY_Community 617|Community 617]]
|
||||||
|
- [[_COMMUNITY_Community 618|Community 618]]
|
||||||
|
- [[_COMMUNITY_Community 619|Community 619]]
|
||||||
|
- [[_COMMUNITY_Community 620|Community 620]]
|
||||||
|
- [[_COMMUNITY_Community 621|Community 621]]
|
||||||
|
- [[_COMMUNITY_Community 622|Community 622]]
|
||||||
|
- [[_COMMUNITY_Community 623|Community 623]]
|
||||||
|
- [[_COMMUNITY_Community 624|Community 624]]
|
||||||
|
- [[_COMMUNITY_Community 625|Community 625]]
|
||||||
|
- [[_COMMUNITY_Community 626|Community 626]]
|
||||||
|
- [[_COMMUNITY_Community 627|Community 627]]
|
||||||
|
- [[_COMMUNITY_Community 628|Community 628]]
|
||||||
|
- [[_COMMUNITY_Community 629|Community 629]]
|
||||||
|
- [[_COMMUNITY_Community 630|Community 630]]
|
||||||
|
- [[_COMMUNITY_Community 631|Community 631]]
|
||||||
|
- [[_COMMUNITY_Community 632|Community 632]]
|
||||||
|
- [[_COMMUNITY_Community 633|Community 633]]
|
||||||
|
- [[_COMMUNITY_Community 634|Community 634]]
|
||||||
|
- [[_COMMUNITY_Community 635|Community 635]]
|
||||||
|
- [[_COMMUNITY_Community 636|Community 636]]
|
||||||
|
- [[_COMMUNITY_Community 637|Community 637]]
|
||||||
|
- [[_COMMUNITY_Community 638|Community 638]]
|
||||||
|
- [[_COMMUNITY_Community 639|Community 639]]
|
||||||
|
- [[_COMMUNITY_Community 640|Community 640]]
|
||||||
|
- [[_COMMUNITY_Community 641|Community 641]]
|
||||||
|
- [[_COMMUNITY_Community 642|Community 642]]
|
||||||
|
- [[_COMMUNITY_Community 643|Community 643]]
|
||||||
|
- [[_COMMUNITY_Community 644|Community 644]]
|
||||||
|
- [[_COMMUNITY_Community 645|Community 645]]
|
||||||
|
- [[_COMMUNITY_Community 648|Community 648]]
|
||||||
|
- [[_COMMUNITY_Community 649|Community 649]]
|
||||||
|
- [[_COMMUNITY_Community 650|Community 650]]
|
||||||
|
- [[_COMMUNITY_Community 651|Community 651]]
|
||||||
|
- [[_COMMUNITY_Community 652|Community 652]]
|
||||||
|
- [[_COMMUNITY_Community 653|Community 653]]
|
||||||
|
- [[_COMMUNITY_Community 654|Community 654]]
|
||||||
|
- [[_COMMUNITY_Community 655|Community 655]]
|
||||||
|
- [[_COMMUNITY_Community 656|Community 656]]
|
||||||
|
- [[_COMMUNITY_Community 657|Community 657]]
|
||||||
|
- [[_COMMUNITY_Community 658|Community 658]]
|
||||||
|
- [[_COMMUNITY_Community 659|Community 659]]
|
||||||
|
- [[_COMMUNITY_Community 660|Community 660]]
|
||||||
|
- [[_COMMUNITY_Community 661|Community 661]]
|
||||||
|
- [[_COMMUNITY_Community 662|Community 662]]
|
||||||
|
- [[_COMMUNITY_Community 663|Community 663]]
|
||||||
|
|
||||||
## God Nodes (most connected - your core abstractions)
|
## God Nodes (most connected - your core abstractions)
|
||||||
1. `Communities (593 total, 43 thin omitted)` - 530 edges
|
1. `Communities (648 total, 62 thin omitted)` - 556 edges
|
||||||
2. `entries` - 39 edges
|
2. `entries` - 39 edges
|
||||||
3. `workflow` - 32 edges
|
3. `workflow` - 32 edges
|
||||||
4. `Phase 4: Shared Lists + Live Sync — Research` - 29 edges
|
4. `Phase 4: Shared Lists + Live Sync — Research` - 29 edges
|
||||||
5. `Phase 3: Event Write-Back + PWA Install — Research` - 27 edges
|
5. `Phase 3: Event Write-Back + PWA Install — Research` - 27 edges
|
||||||
6. `entries` - 26 edges
|
6. `entries` - 26 edges
|
||||||
7. `Phase 5: Web Push Notifications — Research` - 22 edges
|
7. `Phase 16: CI Dependency Audit, Security Checks & Image Hygiene - Research` - 24 edges
|
||||||
8. `entries` - 21 edges
|
8. `Phase 5: Web Push Notifications — Research` - 22 edges
|
||||||
9. `API Surface` - 21 edges
|
9. `entries` - 21 edges
|
||||||
10. `Phase 7: Mobile Test Harness — Research` - 21 edges
|
10. `API Surface` - 21 edges
|
||||||
|
|
||||||
## Surprising Connections (you probably didn't know these)
|
## Surprising Connections (you probably didn't know these)
|
||||||
- `runPoll()` --calls--> `createFastmailClient()` [EXTRACTED]
|
- `loadClientForUser()` --calls--> `createFastmailClient()` [EXTRACTED]
|
||||||
apps/api/src/broker/poller.ts → apps/api/src/broker/client.ts
|
apps/api/src/broker/outboxWorker.ts → apps/api/src/broker/client.ts
|
||||||
- `main()` --calls--> `createFastmailClient()` [EXTRACTED]
|
- `main()` --calls--> `createFastmailClient()` [EXTRACTED]
|
||||||
apps/api/src/broker/spike.ts → apps/api/src/broker/client.ts
|
apps/api/src/broker/spike.ts → apps/api/src/broker/client.ts
|
||||||
- `loadClientForUser()` --calls--> `decryptPassword()` [EXTRACTED]
|
- `loadClientForUser()` --calls--> `decryptPassword()` [EXTRACTED]
|
||||||
@@ -589,7 +676,7 @@
|
|||||||
## Import Cycles
|
## Import Cycles
|
||||||
- None detected.
|
- None detected.
|
||||||
|
|
||||||
## Communities (575 total, 53 thin omitted)
|
## Communities (664 total, 63 thin omitted)
|
||||||
|
|
||||||
### Community 0 - "Community 0"
|
### Community 0 - "Community 0"
|
||||||
Cohesion: 0.05
|
Cohesion: 0.05
|
||||||
@@ -641,7 +728,7 @@ Nodes (27): Alternatives Considered, Architecture, Authelia OIDC Integration, Ba
|
|||||||
|
|
||||||
### Community 12 - "Community 12"
|
### Community 12 - "Community 12"
|
||||||
Cohesion: 0.09
|
Cohesion: 0.09
|
||||||
Nodes (26): createFastmailClient(), FastmailClient, assembleRruleString(), BACKOFF_SECONDS, DispatchResult, dispatchRow(), HARD_FAIL_STATUSES, loadClientForUser() (+18 more)
|
Nodes (31): assembleRruleString(), BACKOFF_SECONDS, DispatchResult, dispatchRow(), HARD_FAIL_STATUSES, initOutboxTrigger(), loadClientForUser(), OutboxPayloadFields (+23 more)
|
||||||
|
|
||||||
### Community 13 - "Community 13"
|
### Community 13 - "Community 13"
|
||||||
Cohesion: 0.08
|
Cohesion: 0.08
|
||||||
@@ -660,12 +747,12 @@ Cohesion: 0.22
|
|||||||
Nodes (11): CreateEventPayload, RecurrencePreset, EventForm(), exclusiveEndToInclusiveDate(), initFormDateTime(), isPhoneBreakpoint(), parseDateTime(), SeriesEditPrompt() (+3 more)
|
Nodes (11): CreateEventPayload, RecurrencePreset, EventForm(), exclusiveEndToInclusiveDate(), initFormDateTime(), isPhoneBreakpoint(), parseDateTime(), SeriesEditPrompt() (+3 more)
|
||||||
|
|
||||||
### Community 17 - "Community 17"
|
### Community 17 - "Community 17"
|
||||||
Cohesion: 0.11
|
Cohesion: 0.09
|
||||||
Nodes (25): ContextVariableMap, DEV_USER, devAuthBypass(), persistSessionCookie(), claimStr(), COLOR_PALETTE, deriveDisplayName(), upsertUser() (+17 more)
|
Nodes (26): ContextVariableMap, DEV_USER, devAuthBypass(), persistSessionCookie(), claimStr(), COLOR_PALETTE, deriveDisplayName(), mockDb (+18 more)
|
||||||
|
|
||||||
### Community 18 - "Community 18"
|
### Community 18 - "Community 18"
|
||||||
Cohesion: 0.08
|
Cohesion: 0.07
|
||||||
Nodes (24): Backlog, Milestones, Phase 10: Admin Role & Settings, Phase 11: Per-Event Reminders, Phase 12: Initial Setup Wizard, Phase 13: Real Lint Gate (ESLint), Phase 14: Desktop E2E Coverage, Phase 7: Mobile Test Harness (+16 more)
|
Nodes (27): Backlog, Milestones, Phase 10: Admin Role & Settings, Phase 11: Per-Event Reminders, Phase 12: Initial Setup Wizard, Phase 13: Real Lint Gate (ESLint), Phase 14: Desktop E2E Coverage, Phase 15: Doc-Only CI Skip (+19 more)
|
||||||
|
|
||||||
### Community 19 - "Community 19"
|
### Community 19 - "Community 19"
|
||||||
Cohesion: 0.17
|
Cohesion: 0.17
|
||||||
@@ -765,7 +852,7 @@ Nodes (15): 1. Register FamilySync as an Authelia OIDC confidential client, 2. S
|
|||||||
|
|
||||||
### Community 43 - "Community 43"
|
### Community 43 - "Community 43"
|
||||||
Cohesion: 0.16
|
Cohesion: 0.16
|
||||||
Nodes (14): decryptPassword(), EncryptedPayload, encryptPassword(), getKey(), getCrypto(), runPoll(), syncCalendar(), memberCredentials (+6 more)
|
Nodes (15): createFastmailClient(), FastmailClient, decryptPassword(), EncryptedPayload, encryptPassword(), getKey(), getCrypto(), runPoll() (+7 more)
|
||||||
|
|
||||||
### Community 44 - "Community 44"
|
### Community 44 - "Community 44"
|
||||||
Cohesion: 0.13
|
Cohesion: 0.13
|
||||||
@@ -804,8 +891,8 @@ Cohesion: 0.19
|
|||||||
Nodes (9): ALLDAY_OCCURRENCE, makeQueryClient(), mockEventsServiceSet, renderWithClient(), TIMED_OCCURRENCE, CalendarOccurrence, hydrateEvents(), ScheduleXEvent (+1 more)
|
Nodes (9): ALLDAY_OCCURRENCE, makeQueryClient(), mockEventsServiceSet, renderWithClient(), TIMED_OCCURRENCE, CalendarOccurrence, hydrateEvents(), ScheduleXEvent (+1 more)
|
||||||
|
|
||||||
### Community 53 - "Community 53"
|
### Community 53 - "Community 53"
|
||||||
Cohesion: 0.17
|
Cohesion: 0.12
|
||||||
Nodes (11): Apply Database Migrations, Build and Start, Deployment Targets, Environment Setup, FamilySync — Deployment Guide, Health Check, Monitoring, Pangolin / Newt Tunnel (+3 more)
|
Nodes (15): Apply Database Migrations, Build and Start (local build), CI/CD Pipeline, Deployment Targets, Environment Setup, FamilySync — Deployment Guide, Health Check, Monitoring (+7 more)
|
||||||
|
|
||||||
### Community 54 - "Community 54"
|
### Community 54 - "Community 54"
|
||||||
Cohesion: 0.13
|
Cohesion: 0.13
|
||||||
@@ -872,8 +959,8 @@ Cohesion: 0.08
|
|||||||
Nodes (20): mockDelete, mockDeleteWhere, mockFrom, mockInsert, mockLimit, mockOnDuplicateKeyUpdate, mockSelect, mockValues (+12 more)
|
Nodes (20): mockDelete, mockDeleteWhere, mockFrom, mockInsert, mockLimit, mockOnDuplicateKeyUpdate, mockSelect, mockValues (+12 more)
|
||||||
|
|
||||||
### Community 70 - "Community 70"
|
### Community 70 - "Community 70"
|
||||||
Cohesion: 0.14
|
Cohesion: 0.13
|
||||||
Nodes (13): Accumulated Context, Blockers/Concerns, Current Position, Decisions, Deferred Items, Operator Next Steps, Pending Todos, Performance Metrics (+5 more)
|
Nodes (14): Accumulated Context, Blockers/Concerns, Current Position, Decisions, Deferred Items, Operator Next Steps, Pending Todos, Performance Metrics (+6 more)
|
||||||
|
|
||||||
### Community 71 - "Community 71"
|
### Community 71 - "Community 71"
|
||||||
Cohesion: 0.17
|
Cohesion: 0.17
|
||||||
@@ -944,8 +1031,8 @@ Cohesion: 0.19
|
|||||||
Nodes (14): addItem(), apiFetch(), createList(), deleteItem(), deleteList(), fetchListItems(), fetchLists(), ListItem (+6 more)
|
Nodes (14): addItem(), apiFetch(), createList(), deleteItem(), deleteList(), fetchListItems(), fetchLists(), ListItem (+6 more)
|
||||||
|
|
||||||
### Community 88 - "Community 88"
|
### Community 88 - "Community 88"
|
||||||
Cohesion: 0.09
|
Cohesion: 0.08
|
||||||
Nodes (22): devDependencies, eslint, eslint-config-prettier, @eslint/js, eslint-plugin-react, eslint-plugin-react-hooks, prettier, typescript-eslint (+14 more)
|
Nodes (25): devDependencies, eslint, eslint-config-prettier, @eslint/js, eslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-security, markdownlint-cli2 (+17 more)
|
||||||
|
|
||||||
### Community 89 - "Community 89"
|
### Community 89 - "Community 89"
|
||||||
Cohesion: 0.20
|
Cohesion: 0.20
|
||||||
@@ -957,7 +1044,7 @@ Nodes (9): IN-01: `RRULE_PRESETS` round-trip is lossy for any parameterized RRUL
|
|||||||
|
|
||||||
### Community 91 - "Community 91"
|
### Community 91 - "Community 91"
|
||||||
Cohesion: 0.00
|
Cohesion: 0.00
|
||||||
Nodes (530): Communities (593 total, 43 thin omitted), Community 0 - "Community 0", Community 100 - "Community 100", Community 101 - "Community 101", Community 102 - "Community 102", Community 103 - "Community 103", Community 104 - "Community 104", Community 105 - "Community 105" (+522 more)
|
Nodes (556): Communities (648 total, 62 thin omitted), Community 0 - "Community 0", Community 100 - "Community 100", Community 101 - "Community 101", Community 102 - "Community 102", Community 103 - "Community 103", Community 104 - "Community 104", Community 105 - "Community 105" (+548 more)
|
||||||
|
|
||||||
### Community 92 - "Community 92"
|
### Community 92 - "Community 92"
|
||||||
Cohesion: 0.20
|
Cohesion: 0.20
|
||||||
@@ -1080,8 +1167,8 @@ Cohesion: 0.07
|
|||||||
Nodes (26): Accessibility Requirements, Checker Sign-Off, Color, Component Inventory, Copywriting Contract, Design System, Event change — modified event (NOTIF-03, change), Event change — new event (NOTIF-03, new) (+18 more)
|
Nodes (26): Accessibility Requirements, Checker Sign-Off, Color, Component Inventory, Copywriting Contract, Design System, Event change — modified event (NOTIF-03, change), Event change — new event (NOTIF-03, new) (+18 more)
|
||||||
|
|
||||||
### Community 122 - "Community 122"
|
### Community 122 - "Community 122"
|
||||||
Cohesion: 0.22
|
Cohesion: 0.04
|
||||||
Nodes (9): checkConstraint, compositePrimaryKeys, foreignKeys, columns, name, name, uniqueConstraints, calendar_outbox_id (+1 more)
|
Nodes (48): Alternatives Considered, Anti-Patterns to Avoid, Architectural Responsibility Map, Architecture Patterns, Assumptions Log, Baseline Violation Assessment, Code Examples, Common Pitfalls (+40 more)
|
||||||
|
|
||||||
### Community 123 - "Community 123"
|
### Community 123 - "Community 123"
|
||||||
Cohesion: 0.29
|
Cohesion: 0.29
|
||||||
@@ -1271,6 +1358,10 @@ Nodes (3): Architecture Patterns, Recommended Project Structure, System Architec
|
|||||||
Cohesion: 0.67
|
Cohesion: 0.67
|
||||||
Nodes (3): Core (already installed — no new installs needed), New Dependencies (must install), Standard Stack
|
Nodes (3): Core (already installed — no new installs needed), New Dependencies (must install), Standard Stack
|
||||||
|
|
||||||
|
### Community 183 - "Community 183"
|
||||||
|
Cohesion: 0.40
|
||||||
|
Nodes (4): permissions, allow, worktree, baseRef
|
||||||
|
|
||||||
### Community 184 - "Community 184"
|
### Community 184 - "Community 184"
|
||||||
Cohesion: 0.67
|
Cohesion: 0.67
|
||||||
Nodes (3): plan_review, source_grounding, source_grounding_authority
|
Nodes (3): plan_review, source_grounding, source_grounding_authority
|
||||||
@@ -1280,12 +1371,12 @@ Cohesion: 0.67
|
|||||||
Nodes (3): graphify, auto_update, enabled
|
Nodes (3): graphify, auto_update, enabled
|
||||||
|
|
||||||
### Community 210 - "Community 210"
|
### Community 210 - "Community 210"
|
||||||
Cohesion: 0.09
|
Cohesion: 0.11
|
||||||
Nodes (15): mockDb, db, pool, calendarEvents, calendars, listItems, lists, listShares (+7 more)
|
Nodes (13): db, pool, listItems, lists, listShares, pushSubscriptions, users, getAccessibleListIds() (+5 more)
|
||||||
|
|
||||||
### Community 211 - "Community 211"
|
### Community 211 - "Community 211"
|
||||||
Cohesion: 0.33
|
Cohesion: 0.08
|
||||||
Nodes (6): autoincrement, name, notNull, primaryKey, type, color
|
Nodes (25): columns, autoincrement, name, notNull, primaryKey, type, color, display_name (+17 more)
|
||||||
|
|
||||||
### Community 212 - "Community 212"
|
### Community 212 - "Community 212"
|
||||||
Cohesion: 0.11
|
Cohesion: 0.11
|
||||||
@@ -1300,12 +1391,12 @@ Cohesion: 0.12
|
|||||||
Nodes (16): CR-01: Sharee can de-share or re-share a list — privilege escalation on `isShared` toggle, CR-02: SSE subscription scope is stale — newly shared lists never delivered to live subscribers, CR-03: `Number(c.req.param(...))` — NaN propagates silently into DB queries, Critical Issues, IN-01: `useListSSE` connects to `/api/sse/lists` — not scoped to the current `listId`, IN-02: `getAccessibleListIds` issues two sequential DB round-trips that could be one query, IN-03: The 401 test in `lists.test.ts` is a no-op assertion, Info (+8 more)
|
Nodes (16): CR-01: Sharee can de-share or re-share a list — privilege escalation on `isShared` toggle, CR-02: SSE subscription scope is stale — newly shared lists never delivered to live subscribers, CR-03: `Number(c.req.param(...))` — NaN propagates silently into DB queries, Critical Issues, IN-01: `useListSSE` connects to `/api/sse/lists` — not scoped to the current `listId`, IN-02: `getAccessibleListIds` issues two sequential DB round-trips that could be one query, IN-03: The 401 test in `lists.test.ts` is a no-op assertion, Info (+8 more)
|
||||||
|
|
||||||
### Community 215 - "Community 215"
|
### Community 215 - "Community 215"
|
||||||
Cohesion: 0.25
|
Cohesion: 0.12
|
||||||
Nodes (8): checkConstraint, columns, compositePrimaryKeys, columns, name, name, calendars_id, calendars
|
Nodes (17): checkConstraint, compositePrimaryKeys, foreignKeys, columns, name, indexes, name, uniqueConstraints (+9 more)
|
||||||
|
|
||||||
### Community 216 - "Community 216"
|
### Community 216 - "Community 216"
|
||||||
Cohesion: 0.12
|
Cohesion: 0.22
|
||||||
Nodes (17): list_items_id, list_items_list_id_lists_id_fk, checkConstraint, compositePrimaryKeys, foreignKeys, columns, name, columnsFrom (+9 more)
|
Nodes (9): list_items_id, checkConstraint, compositePrimaryKeys, foreignKeys, columns, name, name, uniqueConstraints (+1 more)
|
||||||
|
|
||||||
### Community 217 - "Community 217"
|
### Community 217 - "Community 217"
|
||||||
Cohesion: 0.10
|
Cohesion: 0.10
|
||||||
@@ -1357,15 +1448,15 @@ Nodes (32): API Reference, Authentication, Calendar Events, `DELETE /api/events/
|
|||||||
|
|
||||||
### Community 229 - "Community 229"
|
### Community 229 - "Community 229"
|
||||||
Cohesion: 0.10
|
Cohesion: 0.10
|
||||||
Nodes (20): autoincrement, default, name, notNull, primaryKey, type, checked, rank (+12 more)
|
Nodes (20): created_at, rank, text, autoincrement, default, name, notNull, primaryKey (+12 more)
|
||||||
|
|
||||||
### Community 230 - "Community 230"
|
### Community 230 - "Community 230"
|
||||||
Cohesion: 0.10
|
Cohesion: 0.10
|
||||||
Nodes (20): is_shared, name, owner_id, autoincrement, default, name, notNull, primaryKey (+12 more)
|
Nodes (21): name, owner_id, updated_at, columns, autoincrement, name, notNull, primaryKey (+13 more)
|
||||||
|
|
||||||
### Community 231 - "Community 231"
|
### Community 231 - "Community 231"
|
||||||
Cohesion: 0.09
|
Cohesion: 0.08
|
||||||
Nodes (21): 1. Install dependencies, 2. Start the dev database and Redis, 3. Configure environment variables, API dev loop, `apps/api` scripts, `apps/pwa` scripts, Build Commands, Code Style (+13 more)
|
Nodes (24): 1. Install dependencies, 2. Start the dev database and Redis, 3. Configure environment variables, API dev loop, `apps/api` scripts, `apps/pwa` scripts, Build Commands, CI Pipeline Overview (+16 more)
|
||||||
|
|
||||||
### Community 232 - "Community 232"
|
### Community 232 - "Community 232"
|
||||||
Cohesion: 0.10
|
Cohesion: 0.10
|
||||||
@@ -1384,20 +1475,20 @@ Cohesion: 0.11
|
|||||||
Nodes (17): Accomplishments, Auto-fixed Issues (all Rule 3 — blocking), CI Stack Bring-Up Order (confirmed working), Decisions Made, Dependency graph, Deviations from Plan, Files Created/Modified, Issues Encountered (+9 more)
|
Nodes (17): Accomplishments, Auto-fixed Issues (all Rule 3 — blocking), CI Stack Bring-Up Order (confirmed working), Decisions Made, Dependency graph, Deviations from Plan, Files Created/Modified, Issues Encountered (+9 more)
|
||||||
|
|
||||||
### Community 236 - "Community 236"
|
### Community 236 - "Community 236"
|
||||||
Cohesion: 0.19
|
Cohesion: 0.08
|
||||||
Nodes (12): List, ListsResponse, CreateListSheet(), itemCountLabel(), ListCard(), ListCardProps, ListDeleteDialog(), ListDeleteDialogProps (+4 more)
|
Nodes (24): afterEach env restoration in unit tests, `apps/api/Dockerfile` — add `ENV NODE_ENV=production` in production stage, `apps/api/src/index.ts` — add boot guard call, `apps/api/src/lib/bootGuards.ts` — exported guard function, `apps/api/tests/lib/bootGuards.test.ts` — unit test, `.dockerignore` — new root-level file, `eslint.config.js` — add `eslint-plugin-security`, File Classification (+16 more)
|
||||||
|
|
||||||
### Community 237 - "Community 237"
|
### Community 237 - "Community 237"
|
||||||
Cohesion: 0.12
|
Cohesion: 0.12
|
||||||
Nodes (16): Accomplishments, Auto-fixed Issues, Decisions Made, Dependency graph, Deviations from Plan, Files Created/Modified, Issues Encountered, KEY DEVIATION for all downstream workflows (+8 more)
|
Nodes (16): Accomplishments, Auto-fixed Issues, Decisions Made, Dependency graph, Deviations from Plan, Files Created/Modified, Issues Encountered, KEY DEVIATION for all downstream workflows (+8 more)
|
||||||
|
|
||||||
### Community 238 - "Community 238"
|
### Community 238 - "Community 238"
|
||||||
Cohesion: 0.17
|
Cohesion: 0.12
|
||||||
Nodes (12): list_shares_id, checkConstraint, compositePrimaryKeys, foreignKeys, columns, name, name, uniqueConstraints (+4 more)
|
Nodes (17): list_shares_id, columns, isUnique, name, idx_list_shares_user_id, checkConstraint, compositePrimaryKeys, foreignKeys (+9 more)
|
||||||
|
|
||||||
### Community 239 - "Community 239"
|
### Community 239 - "Community 239"
|
||||||
Cohesion: 0.13
|
Cohesion: 0.16
|
||||||
Nodes (16): runReminderCheck(), sentReminders, startReminderScheduler(), yyyyMmDd(), notifyListChange(), sendListChangePush(), getDispatchPushMock(), coalesceListPush() (+8 more)
|
Nodes (15): runReminderCheck(), sentReminders, startReminderScheduler(), yyyyMmDd(), buildCopy(), dispatchEventChange(), EventChangeOperation, isMeaningfulChange() (+7 more)
|
||||||
|
|
||||||
### Community 240 - "Community 240"
|
### Community 240 - "Community 240"
|
||||||
Cohesion: 0.18
|
Cohesion: 0.18
|
||||||
@@ -1405,7 +1496,7 @@ Nodes (10): Accepted Risks Log, Audit Observations (non-blocking, from 04-REVIEW
|
|||||||
|
|
||||||
### Community 241 - "Community 241"
|
### Community 241 - "Community 241"
|
||||||
Cohesion: 0.18
|
Cohesion: 0.18
|
||||||
Nodes (10): Community Hubs (Navigation), Corpus Check, God Nodes (most connected - your core abstractions), Graph Freshness, Graph Report - familysync (2026-06-10), Import Cycles, Knowledge Gaps, Suggested Questions (+2 more)
|
Nodes (10): Community Hubs (Navigation), Corpus Check, God Nodes (most connected - your core abstractions), Graph Freshness, Graph Report - familysync (2026-06-13), Import Cycles, Knowledge Gaps, Suggested Questions (+2 more)
|
||||||
|
|
||||||
### Community 242 - "Community 242"
|
### Community 242 - "Community 242"
|
||||||
Cohesion: 0.18
|
Cohesion: 0.18
|
||||||
@@ -1440,8 +1531,8 @@ Cohesion: 0.22
|
|||||||
Nodes (9): columns, isUnique, name, columns, isUnique, name, idx_list_items_list_id_checked, idx_list_items_list_id_rank (+1 more)
|
Nodes (9): columns, isUnique, name, columns, isUnique, name, idx_list_items_list_id_checked, idx_list_items_list_id_rank (+1 more)
|
||||||
|
|
||||||
### Community 250 - "Community 250"
|
### Community 250 - "Community 250"
|
||||||
Cohesion: 0.25
|
Cohesion: 0.09
|
||||||
Nodes (8): columnsFrom, columnsTo, name, onDelete, onUpdate, tableFrom, tableTo, calendar_outbox_user_id_users_id_fk
|
Nodes (22): Canonical References, Claude's Discretion / Researcher Decides, Constraints / precedent, Deferred Ideas, Dependency Audit & Outdated, Dev/Prod Image Hygiene (999.17), Established Patterns, Existing CI to extend (+14 more)
|
||||||
|
|
||||||
### Community 251 - "Community 251"
|
### Community 251 - "Community 251"
|
||||||
Cohesion: 0.12
|
Cohesion: 0.12
|
||||||
@@ -1492,12 +1583,12 @@ Cohesion: 0.10
|
|||||||
Nodes (20): Canonical References, Carried forward — locked, NOT re-discussed, Claude's Discretion (researcher / planner decide), Deferred Ideas, Established Patterns, Existing Code Insights, Implementation Decisions, Integration code (read before implementing) (+12 more)
|
Nodes (20): Canonical References, Carried forward — locked, NOT re-discussed, Claude's Discretion (researcher / planner decide), Deferred Ideas, Established Patterns, Existing Code Insights, Implementation Decisions, Integration code (read before implementing) (+12 more)
|
||||||
|
|
||||||
### Community 263 - "Community 263"
|
### Community 263 - "Community 263"
|
||||||
Cohesion: 0.22
|
Cohesion: 0.25
|
||||||
Nodes (9): foreignKeys, columnsFrom, columnsTo, name, onDelete, onUpdate, tableFrom, tableTo (+1 more)
|
Nodes (8): columnsFrom, columnsTo, name, onDelete, onUpdate, tableFrom, tableTo, calendars_user_id_users_id_fk
|
||||||
|
|
||||||
### Community 264 - "Community 264"
|
### Community 264 - "Community 264"
|
||||||
Cohesion: 0.25
|
Cohesion: 0.09
|
||||||
Nodes (8): updated_at, autoincrement, default, name, notNull, onUpdate, primaryKey, type
|
Nodes (21): Audit + Outdated Policy, CI assertion depth, Claude's Discretion, Deferred Ideas, Doc-only PR behavior, .dockerignore scope, Enforcement mechanism (multiSelect), Gating & Noise Posture (+13 more)
|
||||||
|
|
||||||
### Community 265 - "Community 265"
|
### Community 265 - "Community 265"
|
||||||
Cohesion: 0.16
|
Cohesion: 0.16
|
||||||
@@ -1552,16 +1643,16 @@ Cohesion: 0.29
|
|||||||
Nodes (7): GET /api/sse/lists, auth, description, file, method, path, response
|
Nodes (7): GET /api/sse/lists, auth, description, file, method, path, response
|
||||||
|
|
||||||
### Community 278 - "Community 278"
|
### Community 278 - "Community 278"
|
||||||
Cohesion: 0.29
|
Cohesion: 0.10
|
||||||
Nodes (7): autoincrement, default, name, notNull, primaryKey, type, all_day
|
Nodes (20): columns, autoincrement, name, notNull, primaryKey, type, calendar_id, dtstart_date (+12 more)
|
||||||
|
|
||||||
### Community 279 - "Community 279"
|
### Community 279 - "Community 279"
|
||||||
Cohesion: 0.11
|
Cohesion: 0.10
|
||||||
Nodes (19): autoincrement, name, notNull, primaryKey, type, columns, autoincrement, name (+11 more)
|
Nodes (20): autoincrement, name, notNull, primaryKey, type, columns, autoincrement, name (+12 more)
|
||||||
|
|
||||||
### Community 280 - "Community 280"
|
### Community 280 - "Community 280"
|
||||||
Cohesion: 0.11
|
Cohesion: 0.19
|
||||||
Nodes (19): columns, autoincrement, name, notNull, primaryKey, type, calendar_id, dtstart_date (+11 more)
|
Nodes (12): List, ListsResponse, CreateListSheet(), itemCountLabel(), ListCard(), ListCardProps, ListDeleteDialog(), ListDeleteDialogProps (+4 more)
|
||||||
|
|
||||||
### Community 281 - "Community 281"
|
### Community 281 - "Community 281"
|
||||||
Cohesion: 0.29
|
Cohesion: 0.29
|
||||||
@@ -1572,24 +1663,24 @@ Cohesion: 0.10
|
|||||||
Nodes (20): Backlog, Overview, Phase 1: Foundation + Broker Spike, Phase 2: Calendar Display, Phase 3: Event Write-Back + PWA Install, Phase 4: Shared Lists + Live Sync, Phase 5: Web Push Notifications, Phase 6: UX Polish (+12 more)
|
Nodes (20): Backlog, Overview, Phase 1: Foundation + Broker Spike, Phase 2: Calendar Display, Phase 3: Event Write-Back + PWA Install, Phase 4: Shared Lists + Live Sync, Phase 5: Web Push Notifications, Phase 6: UX Polish (+12 more)
|
||||||
|
|
||||||
### Community 283 - "Community 283"
|
### Community 283 - "Community 283"
|
||||||
Cohesion: 0.29
|
Cohesion: 0.18
|
||||||
Nodes (7): has_rrule, autoincrement, default, name, notNull, primaryKey, type
|
Nodes (6): BeforeInstallPromptEvent, InstallPrompt(), IOS_STEPS, isIOSSafariNonStandalone(), useAndroidInstallPrompt(), WalkthroughSheetProps
|
||||||
|
|
||||||
### Community 284 - "Community 284"
|
### Community 284 - "Community 284"
|
||||||
Cohesion: 0.29
|
Cohesion: 0.10
|
||||||
Nodes (7): created_at, autoincrement, default, name, notNull, primaryKey, type
|
Nodes (20): Admin bootstrap (ADMIN-03), Admin UI entry & gating, Canonical References, Claude's Discretion, Codebase maps, Credential model (ADMIN-01), Deferred Ideas, Established Patterns (+12 more)
|
||||||
|
|
||||||
### Community 285 - "Community 285"
|
### Community 285 - "Community 285"
|
||||||
Cohesion: 0.29
|
Cohesion: 0.10
|
||||||
Nodes (7): next_attempt_at, autoincrement, default, name, notNull, primaryKey, type
|
Nodes (20): Docs content fixes — 13 baseline violations (MD040 / MD031), File Classification, `.gitea/workflows/ci.yml` — `api` job modification, `.gitea/workflows/ci.yml` — `changes` job (new), `.gitea/workflows/ci.yml` — `gate` job (new), `.gitea/workflows/ci.yml` — `harness` job modification, `.gitea/workflows/ci.yml` — Markdown lint step in `fast-checks`, `if: always()` usage (+12 more)
|
||||||
|
|
||||||
### Community 286 - "Community 286"
|
### Community 286 - "Community 286"
|
||||||
Cohesion: 0.29
|
Cohesion: 0.29
|
||||||
Nodes (7): status, autoincrement, default, name, notNull, primaryKey, type
|
Nodes (7): status, autoincrement, default, name, notNull, primaryKey, type
|
||||||
|
|
||||||
### Community 287 - "Community 287"
|
### Community 287 - "Community 287"
|
||||||
Cohesion: 0.18
|
Cohesion: 0.11
|
||||||
Nodes (6): BeforeInstallPromptEvent, InstallPrompt(), IOS_STEPS, isIOSSafariNonStandalone(), useAndroidInstallPrompt(), WalkthroughSheetProps
|
Nodes (18): Canonical References, CI entry point (no plumbing change expected, but verify), Claude's Discretion, Deferred Ideas, Desktop Assertion Depth, Desktop Gate Severity, Desktop Profile / Viewport, Established Patterns (+10 more)
|
||||||
|
|
||||||
### Community 288 - "Community 288"
|
### Community 288 - "Community 288"
|
||||||
Cohesion: 0.15
|
Cohesion: 0.15
|
||||||
@@ -1748,8 +1839,8 @@ Cohesion: 0.33
|
|||||||
Nodes (5): entries, _meta, commit, updated_at, version
|
Nodes (5): entries, _meta, commit, updated_at, version
|
||||||
|
|
||||||
### Community 327 - "Community 327"
|
### Community 327 - "Community 327"
|
||||||
Cohesion: 0.14
|
Cohesion: 0.10
|
||||||
Nodes (13): Broker Encryption, Config File Reference, Database, Defaults Summary, Environment Variables, FamilySync — Configuration Reference, Local Development (host-side), OIDC / Authelia Authentication (+5 more)
|
Nodes (20): Application Config Files, Broker Encryption, CI Database Credentials, CI Secrets, Config File Reference, Database, Defaults Summary, Environment Variables (+12 more)
|
||||||
|
|
||||||
### Community 328 - "Community 328"
|
### Community 328 - "Community 328"
|
||||||
Cohesion: 0.11
|
Cohesion: 0.11
|
||||||
@@ -1820,8 +1911,8 @@ Cohesion: 0.20
|
|||||||
Nodes (9): Claude's Discretion, Deferred Ideas, Existing-Violation Strategy, Lint File Coverage, Phase 13: Real Lint Gate (ESLint) - Discussion Log, Prettier Scope, Prettier wiring (follow-up), Rule Strictness — gate threshold (+1 more)
|
Nodes (9): Claude's Discretion, Deferred Ideas, Existing-Violation Strategy, Lint File Coverage, Phase 13: Real Lint Gate (ESLint) - Discussion Log, Prettier Scope, Prettier wiring (follow-up), Rule Strictness — gate threshold (+1 more)
|
||||||
|
|
||||||
### Community 345 - "Community 345"
|
### Community 345 - "Community 345"
|
||||||
Cohesion: 0.27
|
Cohesion: 0.11
|
||||||
Nodes (5): PermissionDeniedBanner(), fetchVapidKey(), prefetchVapidKey(), readNotificationsEnabled(), UsePushSubscriptionReturn
|
Nodes (18): `apps/pwa/e2e/calendar.spec.ts` — no structural changes needed, `apps/pwa/e2e/layout.spec.ts` — spec-compat pass (primary target), `apps/pwa/e2e/lists.spec.ts` — no structural changes needed, `apps/pwa/e2e/README.md` — docs update only, `apps/pwa/playwright.config.ts` — add `desktop` project entry, `describe` block header comments to update, Desktop "New Event" button locator (CalendarShell.tsx:436–457), File Classification (+10 more)
|
||||||
|
|
||||||
### Community 346 - "Community 346"
|
### Community 346 - "Community 346"
|
||||||
Cohesion: 0.20
|
Cohesion: 0.20
|
||||||
@@ -1852,8 +1943,8 @@ Cohesion: 0.12
|
|||||||
Nodes (15): 1. iOS/Standalone Cold-Load and OIDC Redirect (D-10/D-11), 2. PushPermissionPrompt Spinner on iOS Device (CP-04.3), Anti-Patterns Found, Behavioral Spot-Checks, Data-Flow Trace (Level 4), Gaps Summary, Goal Achievement, Human Verification Required (+7 more)
|
Nodes (15): 1. iOS/Standalone Cold-Load and OIDC Redirect (D-10/D-11), 2. PushPermissionPrompt Spinner on iOS Device (CP-04.3), Anti-Patterns Found, Behavioral Spot-Checks, Data-Flow Trace (Level 4), Gaps Summary, Goal Achievement, Human Verification Required (+7 more)
|
||||||
|
|
||||||
### Community 353 - "Community 353"
|
### Community 353 - "Community 353"
|
||||||
Cohesion: 0.15
|
Cohesion: 0.14
|
||||||
Nodes (12): Calendar Integration, Commands, Deployment, FamilySync, Installation, License, Monorepo Structure, Prerequisites (+4 more)
|
Nodes (13): Calendar Integration, CI, Commands, Deployment, FamilySync, Installation, License, Monorepo Structure (+5 more)
|
||||||
|
|
||||||
### Community 354 - "Community 354"
|
### Community 354 - "Community 354"
|
||||||
Cohesion: 0.13
|
Cohesion: 0.13
|
||||||
@@ -1868,8 +1959,8 @@ Cohesion: 0.13
|
|||||||
Nodes (14): Auto-fixed Issues, Checkpoint Verification (Task 4), Deviations from Plan, Follow-Up Fix After Checkpoint, Phase 06 Plan 05: Auth-Flow Gating + Session-Expiry (D-10/D-11) Summary, Residual Device-Only Item, Self-Check: PASSED, Task 1: SessionExpiredError + handleAuthResponse (TDD) (+6 more)
|
Nodes (14): Auto-fixed Issues, Checkpoint Verification (Task 4), Deviations from Plan, Follow-Up Fix After Checkpoint, Phase 06 Plan 05: Auth-Flow Gating + Session-Expiry (D-10/D-11) Summary, Residual Device-Only Item, Self-Check: PASSED, Task 1: SessionExpiredError + handleAuthResponse (TDD) (+6 more)
|
||||||
|
|
||||||
### Community 357 - "Community 357"
|
### Community 357 - "Community 357"
|
||||||
Cohesion: 0.20
|
Cohesion: 0.12
|
||||||
Nodes (9): CI integration, Coverage requirements, File naming and location, Integration tests requiring a real database, Running tests, Test framework and setup, Test helpers, Testing (+1 more)
|
Nodes (16): `api`, CI integration, Coverage requirements, End-to-end tests (Playwright), `fast-checks`, File naming and location, `harness`, Integration tests requiring a real database (+8 more)
|
||||||
|
|
||||||
### Community 358 - "Community 358"
|
### Community 358 - "Community 358"
|
||||||
Cohesion: 0.22
|
Cohesion: 0.22
|
||||||
@@ -2184,12 +2275,12 @@ Cohesion: 0.22
|
|||||||
Nodes (9): Common Pitfalls, Pitfall 1: ESLint 10 + eslint-plugin-react Runtime Error, Pitfall 2: API test files excluded from tsconfig project, Pitfall 3: `vitest.config.ts` / `drizzle.config.ts` not in any tsconfig project, Pitfall 4: `no-floating-promises` on setInterval callbacks, Pitfall 5: `no-misused-promises` on event listener callbacks, Pitfall 6: `sw.ts` — service worker file type context, Pitfall 7: `eslint-config-prettier` import path in ESM flat config (+1 more)
|
Nodes (9): Common Pitfalls, Pitfall 1: ESLint 10 + eslint-plugin-react Runtime Error, Pitfall 2: API test files excluded from tsconfig project, Pitfall 3: `vitest.config.ts` / `drizzle.config.ts` not in any tsconfig project, Pitfall 4: `no-floating-promises` on setInterval callbacks, Pitfall 5: `no-misused-promises` on event listener callbacks, Pitfall 6: `sw.ts` — service worker file type context, Pitfall 7: `eslint-config-prettier` import path in ESM flat config (+1 more)
|
||||||
|
|
||||||
### Community 436 - "Community 436"
|
### Community 436 - "Community 436"
|
||||||
Cohesion: 0.28
|
Cohesion: 0.11
|
||||||
Nodes (6): BottomTabBar(), isPhone(), tabActiveOverride, tabBase, App(), isPhone()
|
Nodes (17): Burst behavior — trailing re-drain (the main decision this discussion locked), Canonical References, Claude's Discretion, Deferred Ideas, Established Patterns, Existing Code Insights, Existing implementation to extend (read before changing), Fallback interval (+9 more)
|
||||||
|
|
||||||
### Community 437 - "Community 437"
|
### Community 437 - "Community 437"
|
||||||
Cohesion: 0.28
|
Cohesion: 0.15
|
||||||
Nodes (7): LiveSyncIndicator(), LiveSyncIndicatorProps, BACKOFF_STEPS_MS, SyncState, useListSSE(), UseListSSEOptions, ListDetail()
|
Nodes (14): BLOCKING_SEVERITIES, __filename, isMain, isWaived(), partitionAdvisories(), selectBlocking(), allowlist, emptyAllowlist (+6 more)
|
||||||
|
|
||||||
### Community 438 - "Community 438"
|
### Community 438 - "Community 438"
|
||||||
Cohesion: 0.22
|
Cohesion: 0.22
|
||||||
@@ -2344,24 +2435,24 @@ Cohesion: 0.25
|
|||||||
Nodes (7): Deviations from Plan, Known Stubs, Phase quick-260610-jlp Plan 01: Fix Broken "How to Enable" Link in SettingsSheet, Quality Gate Results, Self-Check: PASSED, Tasks Completed, Threat Flags
|
Nodes (7): Deviations from Plan, Known Stubs, Phase quick-260610-jlp Plan 01: Fix Broken "How to Enable" Link in SettingsSheet, Quality Gate Results, Self-Check: PASSED, Tasks Completed, Threat Flags
|
||||||
|
|
||||||
### Community 476 - "Community 476"
|
### Community 476 - "Community 476"
|
||||||
Cohesion: 0.33
|
Cohesion: 0.12
|
||||||
Nodes (6): display_name, autoincrement, name, notNull, primaryKey, type
|
Nodes (15): Accessibility Contracts, Checker Sign-Off, Color, Copywriting Contract, Design System, Phase 10 — Admin Role & Settings UI Design Contract, Registry Safety, Spacing Scale (+7 more)
|
||||||
|
|
||||||
### Community 477 - "Community 477"
|
### Community 477 - "Community 477"
|
||||||
Cohesion: 0.33
|
Cohesion: 0.12
|
||||||
Nodes (6): raw_vevent, autoincrement, name, notNull, primaryKey, type
|
Nodes (15): Accomplishments, Decisions Made, Dependency graph, Deviations from Plan, Files Created/Modified, Issues Encountered, Known Stubs, Metrics (+7 more)
|
||||||
|
|
||||||
### Community 478 - "Community 478"
|
### Community 478 - "Community 478"
|
||||||
Cohesion: 0.33
|
Cohesion: 0.13
|
||||||
Nodes (6): id, autoincrement, name, notNull, primaryKey, type
|
Nodes (14): `apps/api/src/broker/outboxWorker.ts` (MODIFIED — service/broker, event-driven + batch), `apps/api/src/index.ts` (MODIFIED — config/startup, request-response), `apps/api/src/lib/outboxTrigger.ts` (NEW — utility, event-driven), `apps/api/src/routes/events.ts` (MODIFIED — route/controller, request-response), `apps/api/tests/broker/outboxWorker.test.ts` (MODIFIED — test, batch), Error swallowing in setInterval workers, File Classification, `isMainModule()` startup gate (+6 more)
|
||||||
|
|
||||||
### Community 479 - "Community 479"
|
### Community 479 - "Community 479"
|
||||||
Cohesion: 0.33
|
Cohesion: 0.14
|
||||||
Nodes (6): last_synced_at, autoincrement, name, notNull, primaryKey, type
|
Nodes (13): CR-01: Audit-waiver `expires` field is decorative — expired waivers never re-block, Fixed Issues, IN-02: `pnpm audit --json` is run twice per CI security job, IN-03: `outdated-pins.json` reasons are not cross-checked against the audit allowlist, IN-04: `expand.test.ts` is in scope but unrelated to this CI/security phase, Phase 16: Code Review Fix Report, Skipped Issues, Validation (+5 more)
|
||||||
|
|
||||||
### Community 480 - "Community 480"
|
### Community 480 - "Community 480"
|
||||||
Cohesion: 0.33
|
Cohesion: 0.15
|
||||||
Nodes (6): sync_token, autoincrement, name, notNull, primaryKey, type
|
Nodes (12): Auto-fixed Issues, Deviations from Plan, Known Stubs, Phase 09 Plan 01: Outbox Drain Trigger Wiring Summary, Self-Check: PASSED, Task 1: `apps/api/src/lib/outboxTrigger.ts` (new file), Task 2: RED test block in `apps/api/tests/broker/outboxWorker.test.ts`, Task 3: GREEN implementation in `apps/api/src/broker/outboxWorker.ts` (+4 more)
|
||||||
|
|
||||||
### Community 481 - "Community 481"
|
### Community 481 - "Community 481"
|
||||||
Cohesion: 0.33
|
Cohesion: 0.33
|
||||||
@@ -2384,19 +2475,12 @@ Cohesion: 0.40
|
|||||||
Nodes (5): Deliberate-Violation Test (ROADMAP success criterion 1), Phase Requirements → Test Map, Test Framework, Validation Architecture, Wave 0 Gaps
|
Nodes (5): Deliberate-Violation Test (ROADMAP success criterion 1), Phase Requirements → Test Map, Test Framework, Validation Architecture, Wave 0 Gaps
|
||||||
|
|
||||||
### Community 486 - "Community 486"
|
### Community 486 - "Community 486"
|
||||||
Cohesion: 0.50
|
Cohesion: 0.15
|
||||||
Nodes (4): DeleteConfirmationDialog(), makeQueryClient(), {
|
Nodes (12): Architectural Responsibility Map, Assumptions Log, Don't Hand-Roll, Environment Availability, Metadata, Open Questions, Package Legitimacy Audit, Phase 9: Faster Write-Back - Research (+4 more)
|
||||||
mockDeleteEvent,
|
|
||||||
mockSetDeleteDialog,
|
|
||||||
mockSetLastSyncedUid,
|
|
||||||
mockSetOpenEventId,
|
|
||||||
mockDeleteDialogOpen,
|
|
||||||
mockDeleteDialogUid,
|
|
||||||
}, renderDialog()
|
|
||||||
|
|
||||||
### Community 487 - "Community 487"
|
### Community 487 - "Community 487"
|
||||||
Cohesion: 0.40
|
Cohesion: 0.15
|
||||||
Nodes (5): columns, isUnique, name, idx_list_shares_user_id, indexes
|
Nodes (12): Anti-Patterns Found, Behavioral Spot-Checks, Data-Flow Trace (Level 4), Gaps Summary, Goal Achievement, Human Verification Required, Key Link Verification, Observable Truths (+4 more)
|
||||||
|
|
||||||
### Community 488 - "Community 488"
|
### Community 488 - "Community 488"
|
||||||
Cohesion: 0.40
|
Cohesion: 0.40
|
||||||
@@ -2471,20 +2555,20 @@ Cohesion: 0.50
|
|||||||
Nodes (4): Package Registry Verification, Primary (MEDIUM confidence — Context7 official docs), Secondary (LOW confidence — WebSearch verified against multiple sources), Sources
|
Nodes (4): Package Registry Verification, Primary (MEDIUM confidence — Context7 official docs), Secondary (LOW confidence — WebSearch verified against multiple sources), Sources
|
||||||
|
|
||||||
### Community 506 - "Community 506"
|
### Community 506 - "Community 506"
|
||||||
Cohesion: 0.50
|
Cohesion: 0.15
|
||||||
Nodes (4): uniqueConstraints, columns, name, uniq_calendar_user_url
|
Nodes (12): Anti-Patterns Found, Behavioral Spot-Checks, Data-Flow Trace (Level 4), Gaps Summary, Goal Achievement, Human Verification Required, Key Link Verification, Observable Truths (+4 more)
|
||||||
|
|
||||||
### Community 511 - "Community 511"
|
### Community 511 - "Community 511"
|
||||||
Cohesion: 0.33
|
Cohesion: 0.33
|
||||||
Nodes (5): Fix verification detail, IN-04: `recurrenceCount` default of `1` is send-eligible the instant bound flips to "count", Info, Phase 6: Code Review Report (re-review), Summary
|
Nodes (5): Fix verification detail, IN-04: `recurrenceCount` default of `1` is send-eligible the instant bound flips to "count", Info, Phase 6: Code Review Report (re-review), Summary
|
||||||
|
|
||||||
### Community 512 - "Community 512"
|
### Community 512 - "Community 512"
|
||||||
Cohesion: 0.33
|
Cohesion: 0.17
|
||||||
Nodes (6): last_error, autoincrement, name, notNull, primaryKey, type
|
Nodes (11): Anti-Patterns Found, Behavioral Spot-Checks, Gaps Summary, Goal Achievement, Human Verification Required, Key Link Verification, Observable Truths (Success Criteria), Phase 15: CI doc-only skip + markdown lint gate — Verification Report (+3 more)
|
||||||
|
|
||||||
### Community 513 - "Community 513"
|
### Community 513 - "Community 513"
|
||||||
Cohesion: 0.15
|
Cohesion: 0.11
|
||||||
Nodes (13): list_id, user_id, autoincrement, name, notNull, primaryKey, type, columns (+5 more)
|
Nodes (19): id, list_id, user_id, autoincrement, name, notNull, primaryKey, type (+11 more)
|
||||||
|
|
||||||
### Community 514 - "Community 514"
|
### Community 514 - "Community 514"
|
||||||
Cohesion: 0.67
|
Cohesion: 0.67
|
||||||
@@ -2510,6 +2594,62 @@ Nodes (3): Applicable ASVS Categories, Known Threat Patterns for This Phase, Sec
|
|||||||
Cohesion: 0.33
|
Cohesion: 0.33
|
||||||
Nodes (6): object_url, autoincrement, name, notNull, primaryKey, type
|
Nodes (6): object_url, autoincrement, name, notNull, primaryKey, type
|
||||||
|
|
||||||
|
### Community 525 - "Community 525"
|
||||||
|
Cohesion: 0.17
|
||||||
|
Nodes (11): Deviations from Plan, Known Stubs, Phase 16 Plan 02: Dependency Audit Gate + Outdated Report Summary, Self-Check: PASSED, Task 1 — Audit allowlist + pin reasons (chore, `0f101bd`), Task 2 — check-audit.mjs blocking wrapper, TDD (`7ac8b19` RED → `6eb5107` GREEN), Task 3 — check-outdated.mjs tiered report (`baf2e3a`), TDD Gate Compliance (+3 more)
|
||||||
|
|
||||||
|
### Community 534 - "Community 534"
|
||||||
|
Cohesion: 0.17
|
||||||
|
Nodes (11): Accomplishments, Decisions Made, Deviations from Plan, Files Created/Modified, Issues Encountered, Next Phase Readiness, Performance, Phase 16 Plan 04: Gitleaks Config, Full-History Baseline, and .dockerignore Summary (+3 more)
|
||||||
|
|
||||||
|
### Community 535 - "Community 535"
|
||||||
|
Cohesion: 0.17
|
||||||
|
Nodes (11): Anti-Patterns Found, Behavioral Spot-Checks, Gaps Summary, Goal Achievement, Human Verification Required, Key Link Verification, Observable Truths, Phase 16: CI Dependency Audit, Security Checks & Image Hygiene — Verification Report (+3 more)
|
||||||
|
|
||||||
|
### Community 536 - "Community 536"
|
||||||
|
Cohesion: 0.17
|
||||||
|
Nodes (11): Architectural Responsibility Map, Assumptions Log, Don't Hand-Roll, Environment Availability, Metadata, Open Questions, Phase 10: Admin Role & Settings - Research, Phase Requirements (+3 more)
|
||||||
|
|
||||||
|
### Community 537 - "Community 537"
|
||||||
|
Cohesion: 0.18
|
||||||
|
Nodes (10): IN-01: `serviceWorkers: 'block'` is duplicated across all three projects instead of hoisted to shared `use`, IN-02: New desktop parity test has no explicit auth/mount wait before locating the toolbar button, IN-03: Desktop project lacks an explicit viewport assertion / pin, Info, Phase 14: Code Review Report, Summary, Warnings, WR-01: Two "PhoneNav" tests silently re-target DesktopNav on desktop and no longer test what their names claim (+2 more)
|
||||||
|
|
||||||
|
### Community 538 - "Community 538"
|
||||||
|
Cohesion: 0.18
|
||||||
|
Nodes (10): Auto-fixed Issues, Deviations from Plan, Known Stubs, Phase 15 Plan 01: Markdown Lint Gate Summary, Self-Check: PASSED, Task 1: markdownlint-cli2 install + config, Task 2: Baseline fixes + CI wiring, Tasks Completed (+2 more)
|
||||||
|
|
||||||
|
### Community 539 - "Community 539"
|
||||||
|
Cohesion: 0.18
|
||||||
|
Nodes (10): Decisions Made, Deviations from Plan, Known Stubs, Phase 15 Plan 02: CI doc-only skip + gate aggregate Summary, Self-Check: PASSED, Tasks Completed, Threat Flags, Threat Model Coverage (+2 more)
|
||||||
|
|
||||||
|
### Community 540 - "Community 540"
|
||||||
|
Cohesion: 0.18
|
||||||
|
Nodes (10): Deviations from Plan, Known Stubs, Phase 16 Plan 01: Boot-time Dev-Bypass Guard Summary, Self-Check: PASSED, Task 1 — RED (test commit 8414e89), Task 2 — GREEN (feat commit c2ffd1c), Task 3 — Dockerfile ENV (chore commit 5b4f32a), TDD Gate Compliance (+2 more)
|
||||||
|
|
||||||
|
### Community 541 - "Community 541"
|
||||||
|
Cohesion: 0.18
|
||||||
|
Nodes (8): __dirname, __filename, outdatedData, outdatedOutput, pins, pinsPath, tiers, vulnerableModules
|
||||||
|
|
||||||
|
### Community 542 - "Community 542"
|
||||||
|
Cohesion: 0.20
|
||||||
|
Nodes (9): Deviations from Plan, Known Stubs, Phase 09 Plan 02: Enqueue-Site Signal Wiring Summary, Self-Check: PASSED, Task 1: `apps/api/src/routes/events.ts` — Four publish sites, Task 2: `apps/api/src/index.ts` — Startup wiring, Threat Flags, Verification Evidence (+1 more)
|
||||||
|
|
||||||
|
### Community 543 - "Community 543"
|
||||||
|
Cohesion: 0.20
|
||||||
|
Nodes (9): Admin bootstrap, Admin UI entry, App-password / credential scope, Claude's Discretion, Credential ownership (follow-up), Deferred Ideas, Phase 10: Admin Role & Settings - Discussion Log, Provider scope (follow-up) (+1 more)
|
||||||
|
|
||||||
|
### Community 544 - "Community 544"
|
||||||
|
Cohesion: 0.20
|
||||||
|
Nodes (9): Decisions Made, Deviations from Plan, Known Stubs, Phase 14 Plan 01: Desktop E2E Coverage Summary, Self-Check: PASSED, Tasks Completed, Threat Flags, Verification Results (+1 more)
|
||||||
|
|
||||||
|
### Community 545 - "Community 545"
|
||||||
|
Cohesion: 0.20
|
||||||
|
Nodes (9): Deviations from Plan, Known Stubs, Phase 16 Plan 06: Image Hygiene CI Assertions Summary, Self-Check, Self-Check: PASSED, Task Commits, Threat Coverage, Verification (+1 more)
|
||||||
|
|
||||||
|
### Community 546 - "Community 546"
|
||||||
|
Cohesion: 0.20
|
||||||
|
Nodes (9): Architectural Responsibility Map, Assumptions Log, Environment Availability, Metadata, Open Questions, Package Legitimacy Audit, Phase 16: CI Dependency Audit, Security Checks & Image Hygiene - Research, State of the Art (+1 more)
|
||||||
|
|
||||||
### Community 547 - "Community 547"
|
### Community 547 - "Community 547"
|
||||||
Cohesion: 0.40
|
Cohesion: 0.40
|
||||||
Nodes (5): Phase Requirements → Test Map, Sampling Rate, Test Framework, Validation Architecture, Wave 0 Gaps
|
Nodes (5): Phase Requirements → Test Map, Sampling Rate, Test Framework, Validation Architecture, Wave 0 Gaps
|
||||||
@@ -2518,6 +2658,10 @@ Nodes (5): Phase Requirements → Test Map, Sampling Rate, Test Framework, Valid
|
|||||||
Cohesion: 0.40
|
Cohesion: 0.40
|
||||||
Nodes (5): API route validation (`events.ts:100–109`), API write path — ical.js RRULE serialization (`vevent.ts:49–54`, `outboxWorker.ts`), Expansion path — `expand.ts` per-occurrence duration, Focus 1: Recurrence Bounding — D-06 (RRULE UNTIL/COUNT), PWA side — `RecurrencePreset` extension (`client.ts:130`, `EventForm.tsx`)
|
Nodes (5): API route validation (`events.ts:100–109`), API write path — ical.js RRULE serialization (`vevent.ts:49–54`, `outboxWorker.ts`), Expansion path — `expand.ts` per-occurrence duration, Focus 1: Recurrence Bounding — D-06 (RRULE UNTIL/COUNT), PWA side — `RecurrencePreset` extension (`client.ts:130`, `EventForm.tsx`)
|
||||||
|
|
||||||
|
### Community 549 - "Community 549"
|
||||||
|
Cohesion: 0.22
|
||||||
|
Nodes (8): Decisions Made, Deviations from Plan, Phase 15 Plan 03: Finalize gating surface Summary, Self-Check: PASSED, Tasks Completed, Threat Model Coverage, Verification, What Was Built
|
||||||
|
|
||||||
### Community 550 - "Community 550"
|
### Community 550 - "Community 550"
|
||||||
Cohesion: 0.50
|
Cohesion: 0.50
|
||||||
Nodes (4): Claude's Discretion, Deferred Ideas (OUT OF SCOPE), Locked Decisions, User Constraints (from CONTEXT.md)
|
Nodes (4): Claude's Discretion, Deferred Ideas (OUT OF SCOPE), Locked Decisions, User Constraints (from CONTEXT.md)
|
||||||
@@ -2566,26 +2710,277 @@ Nodes (3): Context to bring into the session, Kick off FamilySync with /gsd:new-
|
|||||||
Cohesion: 0.67
|
Cohesion: 0.67
|
||||||
Nodes (3): Applicable ASVS Categories (Level 1), Known Threat Patterns for this Stack, Security Domain
|
Nodes (3): Applicable ASVS Categories (Level 1), Known Threat Patterns for this Stack, Security Domain
|
||||||
|
|
||||||
|
### Community 588 - "Community 588"
|
||||||
|
Cohesion: 0.22
|
||||||
|
Nodes (8): Commits, Deviations from Plan, Known Stubs, Phase 16 Plan 05: CI Security Job — Summary, Self-Check: PASSED, Threat Coverage, Threat Flags, What Was Built
|
||||||
|
|
||||||
|
### Community 589 - "Community 589"
|
||||||
|
Cohesion: 0.22
|
||||||
|
Nodes (8): Behavior, Follow-up, Follow-up fix — ci.yml heavy-job paths-filter (added after PR opened), Isolation note, Quick Task 260613-fp9 — Summary, Verification, What changed, Why
|
||||||
|
|
||||||
|
### Community 590 - "Community 590"
|
||||||
|
Cohesion: 0.25
|
||||||
|
Nodes (7): Manual-Only Verifications, Per-Task Verification Map, Phase 9 — Validation Strategy, Sampling Rate, Test Infrastructure, Validation Sign-Off, Wave 0 Requirements
|
||||||
|
|
||||||
|
### Community 591 - "Community 591"
|
||||||
|
Cohesion: 0.25
|
||||||
|
Nodes (7): Claude's Discretion, Deferred Ideas, Desktop Assertion Depth, Desktop Gate Severity, Desktop Profile / Viewport, Phase 14: Desktop E2E Coverage - Discussion Log, Spec-Compat Strategy
|
||||||
|
|
||||||
|
### Community 592 - "Community 592"
|
||||||
|
Cohesion: 0.25
|
||||||
|
Nodes (7): Manual-Only Verifications, Per-Task Verification Map, Phase 15 — Validation Strategy, Sampling Rate, Test Infrastructure, Validation Sign-Off, Wave 0 Requirements
|
||||||
|
|
||||||
### Community 593 - "Community 593"
|
### Community 593 - "Community 593"
|
||||||
|
Cohesion: 0.25
|
||||||
|
Nodes (8): Common Pitfalls, Pitfall 1: `pnpm audit --audit-level high` Filters JSON Output, Pitfall 2: gitleaks `detect`/`protect` Are Deprecated, Pitfall 3: `fetch-depth: 1` Makes gitleaks PR Range Scan Fail, Pitfall 4: The Existing esbuild High Advisory Will Immediately Fail the Gate, Pitfall 5: docker run Boot-Smoke Needs DB + Other Env Vars to NOT Crash Before the Guard, Pitfall 6: `detect-object-injection` Will Fire on Drizzle ORM Patterns, Pitfall 7: `auditConfig.ignoreCves` Is Removed in pnpm v11
|
||||||
|
|
||||||
|
### Community 594 - "Community 594"
|
||||||
|
Cohesion: 0.25
|
||||||
|
Nodes (7): Manual-Only Verifications, Per-Task Verification Map, Phase 16 — Validation Strategy, Sampling Rate, Test Infrastructure, Validation Sign-Off, Wave 0 Requirements
|
||||||
|
|
||||||
|
### Community 595 - "Community 595"
|
||||||
|
Cohesion: 0.18
|
||||||
|
Nodes (11): Anti-Patterns to Avoid, Architecture Patterns, Pattern 1: requireAdmin Middleware Inside adminRouter (Pitfall 9), Pattern 2: Zod Validator Hook — No Password Echo (Pitfall 7), Pattern 3: Drizzle-Kit Generate + Migrate Workflow, Pattern 4: Validate → Encrypt → Initial-Sync (Shared Code Path), Pattern 5: First-Login-Wins Admin Bootstrap, Pattern 6: Provider Discriminator on member_credentials (+3 more)
|
||||||
|
|
||||||
|
### Community 596 - "Community 596"
|
||||||
|
Cohesion: 0.25
|
||||||
|
Nodes (8): list_items_list_id_lists_id_fk, columnsFrom, columnsTo, name, onDelete, onUpdate, tableFrom, tableTo
|
||||||
|
|
||||||
|
### Community 597 - "Community 597"
|
||||||
|
Cohesion: 0.29
|
||||||
|
Nodes (6): Burst handling, Claude's Discretion, Deferred Ideas, Fallback interval, Phase 9: Faster Write-Back - Discussion Log, Verification
|
||||||
|
|
||||||
|
### Community 598 - "Community 598"
|
||||||
|
Cohesion: 0.29
|
||||||
|
Nodes (6): IN-01: Unparseable `expires` in the audit allowlist waives indefinitely, Info, Phase 16: Code Review Report, Prior-finding verification, Regression check on the fixes, Summary
|
||||||
|
|
||||||
|
### Community 599 - "Community 599"
|
||||||
|
Cohesion: 0.29
|
||||||
|
Nodes (6): Deviations from Plan, Phase quick-260613-dmw Plan 01: Exclude .gitea/** from CI code paths-filter Summary, Self-Check: PASSED, Tasks Completed, Verification Results, What Was Done
|
||||||
|
|
||||||
|
### Community 600 - "Community 600"
|
||||||
|
Cohesion: 0.29
|
||||||
|
Nodes (5): notifyListChange(), coalesceListPush(), DispatchFn, pending, PendingEntry
|
||||||
|
|
||||||
|
### Community 601 - "Community 601"
|
||||||
|
Cohesion: 0.29
|
||||||
|
Nodes (7): autoincrement, default, name, notNull, primaryKey, type, checked
|
||||||
|
|
||||||
|
### Community 602 - "Community 602"
|
||||||
|
Cohesion: 0.29
|
||||||
|
Nodes (7): is_shared, autoincrement, default, name, notNull, primaryKey, type
|
||||||
|
|
||||||
|
### Community 603 - "Community 603"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (6): Code Examples, Pattern 1: Module-level EventEmitter singleton (from `listEmitter.ts`) [VERIFIED: codebase read], Pattern 2: `scheduleOutboxDrain` wrapper with trailing-re-drain loop [VERIFIED: codebase read + logic derivation from D-05], Pattern 3: Post-commit signal publish in route handler [VERIFIED: codebase read — events.ts], Pattern 4: Startup wiring in `index.ts` [VERIFIED: codebase read — index.ts L112–L146], Pattern 5: Vitest async flush without fake timers [ASSUMED — standard Node.js async behavior]
|
||||||
|
|
||||||
|
### Community 604 - "Community 604"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (6): Common Pitfalls, Pitfall 1: Publishing the signal inside the `db.transaction()` callback (edit-as-move), Pitfall 2: Calling `runOutboxDrain()` directly from the signal listener (bypassing `isDraining`), Pitfall 3: `drainRequested` flag not reset before the trailing drain call, Pitfall 4: Registering the EventEmitter listener at module import time (timer/listener leakage in tests), Pitfall 5: Using `setMaxListeners` too low on the drain emitter
|
||||||
|
|
||||||
|
### Community 605 - "Community 605"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (6): Key Open Questions — Answers for the Planner, Q1: Where does `drainRequested` and the trailing-re-drain loop belong?, Q2: Module/singleton shape for `outboxTrigger.ts`, Q3: Exact post-commit publish points in `events.ts`, Q4: Wiring in `index.ts` under `isMainModule()`, Q5: Vitest test strategy for trigger wiring (D-09)
|
||||||
|
|
||||||
|
### Community 606 - "Community 606"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (5): IN-01: `__resetDrainState()` is a production export with no environment guard, IN-02: Signal arriving in the `isDraining=false` → `.finally` window starts a fresh drain rather than collapsing via `drainRequested`, Narrative Findings (AI reviewer), Phase 09: Code Review Report, Summary
|
||||||
|
|
||||||
|
### Community 607 - "Community 607"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (5): Fixed Issues, IN-01: `__resetDrainState()` is a production export with no environment guard, IN-02: Signal arriving in the `isDraining=false` → `.finally` window starts a fresh drain rather than collapsing via `drainRequested`, Phase 09: Code Review Fix Report, Skipped Issues
|
||||||
|
|
||||||
|
### Community 608 - "Community 608"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (6): Full-History Baseline Scan (one-time), `.gitleaks.toml` Config, Install Approach (no actions/cache), PR Diff Scan (blocking), Secret Scanning: gitleaks Confirmed, Tool Confirmation: gitleaks over trufflehog
|
||||||
|
|
||||||
|
### Community 609 - "Community 609"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (5): Change, must_haves, Problem, Quick Task 260613-fp9: Skip Docker publish for `.gitea`/`.planning`-only pushes, Tasks
|
||||||
|
|
||||||
|
### Community 610 - "Community 610"
|
||||||
|
Cohesion: 0.27
|
||||||
|
Nodes (5): PermissionDeniedBanner(), fetchVapidKey(), prefetchVapidKey(), readNotificationsEnabled(), UsePushSubscriptionReturn
|
||||||
|
|
||||||
|
### Community 611 - "Community 611"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (6): group_id, autoincrement, name, notNull, primaryKey, type
|
||||||
|
|
||||||
|
### Community 612 - "Community 612"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (6): uid, autoincrement, name, notNull, primaryKey, type
|
||||||
|
|
||||||
|
### Community 613 - "Community 613"
|
||||||
Cohesion: 0.40
|
Cohesion: 0.40
|
||||||
Nodes (5): indexes, columns, isUnique, name, idx_calendars_user_id
|
Nodes (5): Phase Requirements → Test Map, Sampling Rate, Test Framework, Validation Architecture, Wave 0 Gaps
|
||||||
|
|
||||||
|
### Community 614 - "Community 614"
|
||||||
|
Cohesion: 0.40
|
||||||
|
Nodes (5): All 15 Rules (v4.0.1), eslint-plugin-security Integration (D-03), Flat Config Wiring, Triage Strategy for Existing Codebase, Version and Flat Config
|
||||||
|
|
||||||
|
### Community 615 - "Community 615"
|
||||||
|
Cohesion: 0.40
|
||||||
|
Nodes (5): Core (new additions), Installation, No New npm Packages for Audit/Outdated, Standard Stack, Verified Package State
|
||||||
|
|
||||||
|
### Community 616 - "Community 616"
|
||||||
|
Cohesion: 0.40
|
||||||
|
Nodes (5): Phase Requirements → Test Map, Sampling Rate, Test Framework, Validation Architecture (Nyquist), Wave 0 Gaps
|
||||||
|
|
||||||
|
### Community 617 - "Community 617"
|
||||||
|
Cohesion: 0.40
|
||||||
|
Nodes (4): GHSA-gv7w-rqvm-qjhr, expires, reason, reviewer
|
||||||
|
|
||||||
|
### Community 618 - "Community 618"
|
||||||
|
Cohesion: 0.40
|
||||||
|
Nodes (4): eslint, @eslint/js, @types/node, zod
|
||||||
|
|
||||||
|
### Community 619 - "Community 619"
|
||||||
|
Cohesion: 0.50
|
||||||
|
Nodes (4): Alternatives Considered, Core, Standard Stack, Supporting
|
||||||
|
|
||||||
|
### Community 620 - "Community 620"
|
||||||
|
Cohesion: 0.50
|
||||||
|
Nodes (4): Claude's Discretion, Deferred Ideas (OUT OF SCOPE), Locked Decisions, User Constraints (from CONTEXT.md)
|
||||||
|
|
||||||
|
### Community 621 - "Community 621"
|
||||||
|
Cohesion: 0.50
|
||||||
|
Nodes (3): Artifacts this phase produces, STRIDE Threat Register, Trust Boundaries
|
||||||
|
|
||||||
|
### Community 622 - "Community 622"
|
||||||
|
Cohesion: 0.50
|
||||||
|
Nodes (4): CI Invocation, Current Advisory State (MUST ADDRESS BEFORE GATE GOES LIVE), Mechanism Comparison, pnpm audit Allowlist/Waiver Mechanism (D-04 / D-05)
|
||||||
|
|
||||||
|
### Community 623 - "Community 623"
|
||||||
|
Cohesion: 0.50
|
||||||
|
Nodes (4): Claude's Discretion, Deferred Ideas (OUT OF SCOPE), Locked Decisions, User Constraints (from CONTEXT.md)
|
||||||
|
|
||||||
|
### Community 624 - "Community 624"
|
||||||
|
Cohesion: 0.50
|
||||||
|
Nodes (4): Current State (verified 2026-06-13), OQ-01: pnpm outdated — Advisory-Only Mechanism Respecting Intentional Pins, Recommended Mechanism: Node Wrapper Script, The Problem
|
||||||
|
|
||||||
|
### Community 625 - "Community 625"
|
||||||
|
Cohesion: 0.50
|
||||||
|
Nodes (4): Primary (MEDIUM confidence — WebSearch + official site reads), Sources, Tertiary (LOW confidence — training knowledge), Verified via Live CLI Runs (HIGH confidence)
|
||||||
|
|
||||||
|
### Community 628 - "Community 628"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): Applicable ASVS Categories, Known Threat Patterns for this phase, Security Domain
|
||||||
|
|
||||||
|
### Community 629 - "Community 629"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): Architecture Patterns, Recommended Project Structure, System Architecture Diagram
|
||||||
|
|
||||||
|
### Community 630 - "Community 630"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): Primary (HIGH confidence), Secondary (MEDIUM confidence), Sources
|
||||||
|
|
||||||
|
### Community 640 - "Community 640"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): Analysis of Current Repo Tree, Exact `.dockerignore` Line List (D-09), Recommended `.dockerignore`
|
||||||
|
|
||||||
|
### Community 641 - "Community 641"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): Applicable ASVS Categories (Phase 16 — CI tooling phase), Security Domain, Threat Model for This Phase
|
||||||
|
|
||||||
|
### Community 642 - "Community 642"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): Architecture Patterns, Recommended File Structure, System Architecture Diagram
|
||||||
|
|
||||||
|
### Community 643 - "Community 643"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): Boot-Smoke (D-08 verification in the actual image), D-10: CI Image-Hygiene Assertions (Static + Boot-Smoke), Static Assertions
|
||||||
|
|
||||||
|
### Community 644 - "Community 644"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): D-07: Add `ENV NODE_ENV=production` to Dockerfile, D-07 / D-08: Image Hygiene Code Changes, D-08: Boot-Time Refuse-to-Boot Guard
|
||||||
|
|
||||||
|
### Community 645 - "Community 645"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): D-15: Job Decomposition, Full `security` Job Skeleton, Recommended Layout
|
||||||
|
|
||||||
|
### Community 648 - "Community 648"
|
||||||
|
Cohesion: 0.28
|
||||||
|
Nodes (6): BottomTabBar(), isPhone(), tabActiveOverride, tabBase, App(), isPhone()
|
||||||
|
|
||||||
|
### Community 649 - "Community 649"
|
||||||
|
Cohesion: 0.28
|
||||||
|
Nodes (7): LiveSyncIndicator(), LiveSyncIndicatorProps, BACKOFF_STEPS_MS, SyncState, useListSSE(), UseListSSEOptions, ListDetail()
|
||||||
|
|
||||||
|
### Community 650 - "Community 650"
|
||||||
|
Cohesion: 0.22
|
||||||
|
Nodes (9): checkConstraint, compositePrimaryKeys, foreignKeys, columns, name, name, uniqueConstraints, calendar_outbox_id (+1 more)
|
||||||
|
|
||||||
|
### Community 651 - "Community 651"
|
||||||
|
Cohesion: 0.25
|
||||||
|
Nodes (8): columnsFrom, columnsTo, name, onDelete, onUpdate, tableFrom, tableTo, calendar_outbox_user_id_users_id_fk
|
||||||
|
|
||||||
|
### Community 652 - "Community 652"
|
||||||
|
Cohesion: 0.29
|
||||||
|
Nodes (7): Common Pitfalls, Pitfall 1: App Password Echoed in Error Response (Pitfall 7), Pitfall 2: requireAdmin Only at Parent Mount (Pitfall 9), Pitfall 3: DEV_AUTH_BYPASS User Not Admin in DB, Pitfall 4: drizzle-kit push on Populated MariaDB, Pitfall 5: Initial Sync Not Triggered After Credential Save, Pitfall 6: Member Self-Service Endpoint Allows Cross-Member Write
|
||||||
|
|
||||||
|
### Community 653 - "Community 653"
|
||||||
|
Cohesion: 0.29
|
||||||
|
Nodes (7): autoincrement, default, name, notNull, primaryKey, type, all_day
|
||||||
|
|
||||||
|
### Community 654 - "Community 654"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (6): /api/me Response Extension, Code Examples, createFastmailClient + fetchCalendars (CalDAV PROPFIND Validation), Drizzle Upsert Pattern for member_credentials, encryptPassword Reuse, Resolved User ID Pattern (existing, for requireAdmin)
|
||||||
|
|
||||||
|
### Community 655 - "Community 655"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (6): last_error, autoincrement, name, notNull, primaryKey, type
|
||||||
|
|
||||||
|
### Community 656 - "Community 656"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (6): raw_vevent, autoincrement, name, notNull, primaryKey, type
|
||||||
|
|
||||||
|
### Community 657 - "Community 657"
|
||||||
|
Cohesion: 0.40
|
||||||
|
Nodes (5): Phase Requirements → Test Map, Sampling Rate, Test Framework, Validation Architecture, Wave 0 Gaps
|
||||||
|
|
||||||
|
### Community 658 - "Community 658"
|
||||||
|
Cohesion: 0.50
|
||||||
|
Nodes (4): DeleteConfirmationDialog(), makeQueryClient(), {
|
||||||
|
mockDeleteEvent,
|
||||||
|
mockSetDeleteDialog,
|
||||||
|
mockSetLastSyncedUid,
|
||||||
|
mockSetOpenEventId,
|
||||||
|
mockDeleteDialogOpen,
|
||||||
|
mockDeleteDialogUid,
|
||||||
|
}, renderDialog()
|
||||||
|
|
||||||
|
### Community 659 - "Community 659"
|
||||||
|
Cohesion: 0.50
|
||||||
|
Nodes (4): Claude's Discretion, Deferred Ideas (OUT OF SCOPE), Locked Decisions, User Constraints (from CONTEXT.md)
|
||||||
|
|
||||||
|
### Community 660 - "Community 660"
|
||||||
|
Cohesion: 0.50
|
||||||
|
Nodes (4): Primary (HIGH confidence), Secondary (MEDIUM confidence), Sources, Tertiary (LOW confidence)
|
||||||
|
|
||||||
|
### Community 661 - "Community 661"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): Applicable ASVS Categories, Known Threat Patterns, Security Domain
|
||||||
|
|
||||||
|
### Community 662 - "Community 662"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): Core (already installed), Package Legitimacy Audit, Standard Stack
|
||||||
|
|
||||||
## Knowledge Gaps
|
## Knowledge Gaps
|
||||||
- **4429 isolated node(s):** `allow`, `version`, `timestamp`, `phase`, `phase_name` (+4424 more)
|
- **5083 isolated node(s):** `allow`, `baseRef`, `version`, `timestamp`, `phase` (+5078 more)
|
||||||
These have ≤1 connection - possible missing edges or undocumented components.
|
These have ≤1 connection - possible missing edges or undocumented components.
|
||||||
- **53 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
- **63 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||||
|
|
||||||
## Suggested Questions
|
## Suggested Questions
|
||||||
_Questions this graph is uniquely positioned to answer:_
|
_Questions this graph is uniquely positioned to answer:_
|
||||||
|
|
||||||
- **Why does `Communities (593 total, 43 thin omitted)` connect `Community 91` to `Community 241`?**
|
- **Why does `Communities (648 total, 62 thin omitted)` connect `Community 91` to `Community 241`?**
|
||||||
_High betweenness centrality (0.008) - this node is a cross-community bridge._
|
_High betweenness centrality (0.005) - this node is a cross-community bridge._
|
||||||
- **Why does `columns` connect `Community 279` to `Community 512`, `Community 513`, `Community 264`, `Community 332`, `Community 336`, `Community 337`, `Community 280`, `Community 281`, `Community 122`, `Community 284`, `Community 285`, `Community 478`, `Community 286`?**
|
- **Why does `columns` connect `Community 278` to `Community 513`, `Community 481`, `Community 227`, `Community 612`, `Community 230`, `Community 331`, `Community 524`, `Community 332`, `Community 653`, `Community 656`?**
|
||||||
|
_High betweenness centrality (0.002) - this node is a cross-community bridge._
|
||||||
|
- **Why does `tables` connect `Community 401` to `Community 227`, `Community 650`, `Community 238`, `Community 215`, `Community 216`?**
|
||||||
_High betweenness centrality (0.001) - this node is a cross-community bridge._
|
_High betweenness centrality (0.001) - this node is a cross-community bridge._
|
||||||
- **Why does `uid` connect `Community 280` to `Community 279`?**
|
- **What connects `allow`, `baseRef`, `version` to the rest of the system?**
|
||||||
_High betweenness centrality (0.001) - this node is a cross-community bridge._
|
_5083 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||||
- **What connects `allow`, `version`, `timestamp` to the rest of the system?**
|
|
||||||
_4429 weakly-connected nodes found - possible documentation gaps or missing edges._
|
|
||||||
- **Should `Community 0` be split into smaller, more focused modules?**
|
- **Should `Community 0` be split into smaller, more focused modules?**
|
||||||
_Cohesion score 0.045454545454545456 - nodes in this community are weakly interconnected._
|
_Cohesion score 0.045454545454545456 - nodes in this community are weakly interconnected._
|
||||||
- **Should `Community 1` be split into smaller, more focused modules?**
|
- **Should `Community 1` be split into smaller, more focused modules?**
|
||||||
|
|||||||
+22528
-5444
File diff suppressed because it is too large
Load Diff
@@ -94,27 +94,27 @@ Output: Edited `schema.ts`, a generated `0001_v1_1_foundation.sql` migration fil
|
|||||||
<read_first>
|
<read_first>
|
||||||
- apps/api/src/db/migrations/0000_baseline.sql (existing migration format — `--> statement-breakpoint` between DDL statements; the format the generated file must follow)
|
- apps/api/src/db/migrations/0000_baseline.sql (existing migration format — `--> statement-breakpoint` between DDL statements; the format the generated file must follow)
|
||||||
- apps/api/drizzle.config.ts (migration output dir + env-driven DB credentials)
|
- apps/api/drizzle.config.ts (migration output dir + env-driven DB credentials)
|
||||||
- apps/api/package.json (the `db:generate` / `db:migrate` scripts)
|
- apps/api/package.json (the `db:generate` / `db:migrate` scripts — `db:generate` = `drizzle-kit generate`, `db:migrate` = `drizzle-kit migrate`)
|
||||||
- .planning/phases/10-admin-role-settings/10-RESEARCH.md §Pattern 3 (generate+migrate workflow, exact commands, DB_HOST=127.0.0.1 dev override) + §Pitfall 4 (why never push)
|
- .planning/phases/10-admin-role-settings/10-RESEARCH.md §Pattern 3 (generate+migrate workflow, exact commands, DB_HOST=127.0.0.1 dev override) + §Pitfall 4 (why never push)
|
||||||
- MEMORY note [[drizzle-mariadb-push-unsafe]] context in 10-CONTEXT.md Claude's Discretion (generate+migrate, never push)
|
- MEMORY note [[drizzle-mariadb-push-unsafe]] context in 10-CONTEXT.md Claude's Discretion (generate+migrate, never push) + [[api-integration-test-db]] (DB_HOST=127.0.0.1 + .env creds for a reachable dev MariaDB)
|
||||||
</read_first>
|
</read_first>
|
||||||
<action>
|
<action>
|
||||||
BLOCKING — this must run AFTER Task 1 (schema.ts complete) and BEFORE any plan that reads the new columns. Bring up the dev MariaDB if not already bound on 3306 (per [[dev-stack-bringup]]: dev compose override exposes 3306). Then:
|
BLOCKING — this must run AFTER Task 1 (schema.ts complete) and BEFORE any plan that reads the new columns. You MUST actually RUN both commands in this task; describing them is not enough, and tsc/build passing is NOT sufficient proof that the migration ran (Drizzle types come from schema.ts regardless of whether the DB was migrated). Bring up the dev MariaDB if not already bound on 3306 (per [[dev-stack-bringup]]: dev compose override exposes 3306). Then:
|
||||||
1. Run `pnpm --filter @familysync/api db:generate` to produce `apps/api/src/db/migrations/0001_v1_1_foundation.sql` (drizzle-kit names it; the actual filename may differ — commit whatever drizzle-kit emits as the next sequential migration) and update `meta/_journal.json`. DO NOT hand-write the SQL.
|
1. RUN `pnpm --filter @familysync/api db:generate` to produce the next sequential migration `.sql` under `apps/api/src/db/migrations/` (drizzle-kit names it `0001_v1_1_foundation.sql` or a similar sequential name — commit whatever drizzle-kit emits) and update `meta/_journal.json`. DO NOT hand-write the SQL.
|
||||||
2. INSPECT the generated SQL: it MUST be only `ALTER TABLE ... ADD COLUMN` / `ADD UNIQUE` / `CREATE TABLE` statements (additive). If it contains any `DROP TABLE`, `DROP COLUMN`, or `TRUNCATE`, STOP — that is the false-destructive-diff trap; do NOT apply it, and do NOT fall back to `db:push`. Re-derive from schema.ts.
|
2. INSPECT the generated SQL: it MUST be only `ALTER TABLE ... ADD COLUMN` / `ADD UNIQUE` / `CREATE TABLE` statements (additive). If it contains any `DROP TABLE`, `DROP COLUMN`, or `TRUNCATE`, STOP — that is the false-destructive-diff trap ([[drizzle-mariadb-push-unsafe]]); do NOT apply it, and do NOT fall back to `db:push`. Re-derive from schema.ts.
|
||||||
3. Apply with `DB_HOST=127.0.0.1` (+ dev DB_USER/DB_PASSWORD/DB_NAME from .env): `cd apps/api && set -a; source ../../.env; set +a; DB_HOST=127.0.0.1 pnpm db:migrate`. NEVER `db:push`.
|
3. RUN `pnpm --filter @familysync/api db:migrate` against the reachable dev DB with `DB_HOST=127.0.0.1` + the dev DB_USER/DB_PASSWORD/DB_NAME/DB_PORT from `.env` (per [[api-integration-test-db]]): `set -a; source .env; set +a; DB_HOST=127.0.0.1 pnpm --filter @familysync/api db:migrate`. NEVER `db:push` / `drizzle-kit push`.
|
||||||
4. Verify the live columns/table exist via a mysql2 query (not just tsc): assert `is_admin` on `users`, `provider_type` + the unique index on `member_credentials`, `reminder_lead_minutes` on `calendar_events`, and the `app_config` table.
|
4. Verify the live columns/table exist via a mysql2 query (not just tsc): assert `is_admin` on `users`, `provider_type` + the unique index on `member_credentials`, `reminder_lead_minutes` on `calendar_events`, and the `app_config` table each return a row.
|
||||||
</action>
|
</action>
|
||||||
<verify>
|
<verify>
|
||||||
<automated>cd /home/luc/Projects/familysync && set -a; source .env 2>/dev/null; set +a; DB_HOST=127.0.0.1 node -e "const m=require('mysql2/promise');(async()=>{const c=await m.createConnection({host:'127.0.0.1',port:Number(process.env.DB_PORT||3306),user:process.env.DB_USER||'familysync',password:process.env.DB_PASSWORD||'',database:process.env.DB_NAME||'familysync'});const[u]=await c.query(\"SHOW COLUMNS FROM users LIKE 'is_admin'\");const[mc]=await c.query(\"SHOW COLUMNS FROM member_credentials LIKE 'provider_type'\");const[ce]=await c.query(\"SHOW COLUMNS FROM calendar_events LIKE 'reminder_lead_minutes'\");const[ac]=await c.query(\"SHOW TABLES LIKE 'app_config'\");if(u.length&&mc.length&&ce.length&&ac.length){console.log('MIGRATION OK');process.exit(0)}console.error('MISSING',{u:u.length,mc:mc.length,ce:ce.length,ac:ac.length});process.exit(1)})().catch(e=>{console.error(e.message);process.exit(1)})"</automated>
|
<automated>cd /home/luc/Projects/familysync && set -a; source .env 2>/dev/null; set +a; DB_HOST=127.0.0.1 node -e "const m=require('mysql2/promise');(async()=>{const c=await m.createConnection({host:'127.0.0.1',port:Number(process.env.DB_PORT||3306),user:process.env.DB_USER||'familysync',password:process.env.DB_PASSWORD||'',database:process.env.DB_NAME||'familysync'});const[u]=await c.query(\"SHOW COLUMNS FROM users LIKE 'is_admin'\");const[mc]=await c.query(\"SHOW COLUMNS FROM member_credentials LIKE 'provider_type'\");const[ce]=await c.query(\"SHOW COLUMNS FROM calendar_events LIKE 'reminder_lead_minutes'\");const[ac]=await c.query(\"SHOW TABLES LIKE 'app_config'\");if(u.length&&mc.length&&ce.length&&ac.length){console.log('MIGRATION OK');process.exit(0)}console.error('MISSING',{u:u.length,mc:mc.length,ce:ce.length,ac:ac.length});process.exit(1)})().catch(e=>{console.error(e.message);process.exit(1)})"</automated>
|
||||||
</verify>
|
</verify>
|
||||||
<acceptance_criteria>
|
<acceptance_criteria>
|
||||||
- A new migration `.sql` file exists under `apps/api/src/db/migrations/` (sequential after `0000_baseline.sql`) and `meta/_journal.json` references it.
|
- Both commands were actually RUN this task: `pnpm --filter @familysync/api db:generate` produced a new migration `.sql` file under `apps/api/src/db/migrations/` (sequential after `0000_baseline.sql`) with a matching `meta/_journal.json` entry, and `pnpm --filter @familysync/api db:migrate` (with `DB_HOST=127.0.0.1` + `.env` creds) applied it to the live dev DB. tsc/build passing is explicitly NOT accepted as proof.
|
||||||
- The generated SQL contains NO `DROP TABLE`, `DROP COLUMN`, or `TRUNCATE` statement: `grep -v '^--' <migration.sql> | grep -ciE 'drop (table|column)|truncate'` returns 0.
|
- The generated migration `.sql` contains NO `DROP`/`TRUNCATE` statement: `grep -v '^--' <migration.sql> | grep -ciE 'drop (table|column)|truncate'` returns 0 (guards the false-destructive-diff trap).
|
||||||
- The live dev MariaDB query above prints `MIGRATION OK` and exits 0 — `users.is_admin`, `member_credentials.provider_type`, the `member_credentials` unique on `user_id`, `calendar_events.reminder_lead_minutes`, and the `app_config` table all exist.
|
- The live dev MariaDB now has the columns/table: the mysql2 query above prints `MIGRATION OK` and exits 0 — `SHOW COLUMNS FROM users LIKE 'is_admin'`, `SHOW COLUMNS FROM member_credentials LIKE 'provider_type'`, `SHOW COLUMNS FROM calendar_events LIKE 'reminder_lead_minutes'`, and `SHOW TABLES LIKE 'app_config'` each return a row, plus the `member_credentials` unique on `user_id` exists.
|
||||||
- `db:push` was NOT run (no push in command history for this task).
|
- `db:push` / `drizzle-kit push` was NOT run (no push in command history for this task).
|
||||||
</acceptance_criteria>
|
</acceptance_criteria>
|
||||||
<done>The v1.1 migration is generated (additive-only), committed, and applied to the live dev DB; all new columns/table verified present by a real DB query.</done>
|
<done>The v1.1 migration is generated (additive-only) by an actual db:generate run, committed, and applied to the live dev DB by an actual db:migrate run; all new columns/table verified present by a real DB query (not tsc).</done>
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
<task type="execute">
|
<task type="execute">
|
||||||
@@ -175,7 +175,7 @@ This plan creates the following new symbols/files (excluded from drift verificat
|
|||||||
|
|
||||||
<verification>
|
<verification>
|
||||||
- `pnpm --filter @familysync/api exec tsc --noEmit` passes (schema typechecks).
|
- `pnpm --filter @familysync/api exec tsc --noEmit` passes (schema typechecks).
|
||||||
- The live DB query in Task 2 prints `MIGRATION OK`.
|
- The live DB query in Task 2 prints `MIGRATION OK` (proof the migration actually ran — tsc alone is NOT proof).
|
||||||
- The generated migration SQL is additive-only (no DROP/TRUNCATE).
|
- The generated migration SQL is additive-only (no DROP/TRUNCATE).
|
||||||
- `grep -c is_admin apps/pwa/e2e/global-setup.ts` >= 1.
|
- `grep -c is_admin apps/pwa/e2e/global-setup.ts` >= 1.
|
||||||
</verification>
|
</verification>
|
||||||
@@ -190,3 +190,5 @@ This plan creates the following new symbols/files (excluded from drift verificat
|
|||||||
<output>
|
<output>
|
||||||
Create `.planning/phases/10-admin-role-settings/10-01-SUMMARY.md` when done.
|
Create `.planning/phases/10-admin-role-settings/10-01-SUMMARY.md` when done.
|
||||||
</output>
|
</output>
|
||||||
|
</content>
|
||||||
|
</invoke>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ wave: 3
|
|||||||
depends_on: ["10-01", "10-02"]
|
depends_on: ["10-01", "10-02"]
|
||||||
files_modified:
|
files_modified:
|
||||||
- apps/api/src/broker/outboxWorker.ts
|
- apps/api/src/broker/outboxWorker.ts
|
||||||
|
- apps/api/src/broker/credentialSync.ts
|
||||||
- apps/api/src/routes/admin.ts
|
- apps/api/src/routes/admin.ts
|
||||||
- apps/api/src/routes/me.ts
|
- apps/api/src/routes/me.ts
|
||||||
- apps/api/src/index.ts
|
- apps/api/src/index.ts
|
||||||
@@ -18,11 +19,16 @@ must_haves:
|
|||||||
- "POST /api/admin/credentials validates against CalDAV (PROPFIND), 400 on bad credential with NO submitted password in the body, 200 + encrypted store on success; never logs/echoes the password"
|
- "POST /api/admin/credentials validates against CalDAV (PROPFIND), 400 on bad credential with NO submitted password in the body, 200 + encrypted store on success; never logs/echoes the password"
|
||||||
- "PUT /api/admin/calendars/:id/shared sets exactly one calendar is_shared=1 and clears any prior shared calendar"
|
- "PUT /api/admin/calendars/:id/shared sets exactly one calendar is_shared=1 and clears any prior shared calendar"
|
||||||
- "POST /api/me/credential sets only the current user's credential (ignores any userId in the body); a non-admin cannot POST /api/admin/credentials"
|
- "POST /api/me/credential sets only the current user's credential (ignores any userId in the body); a non-admin cannot POST /api/admin/credentials"
|
||||||
|
- "Both POST /api/admin/credentials and POST /api/me/credential call ONE shared validateEncryptAndStoreCredential helper (no duplicated validate/encrypt/store logic)"
|
||||||
artifacts:
|
artifacts:
|
||||||
- path: "apps/api/src/routes/admin.ts"
|
- path: "apps/api/src/routes/admin.ts"
|
||||||
provides: "adminRouter guarded by requireAdmin (.use('*', ...) first); GET /members, POST /credentials, GET /calendars, PUT /calendars/:id/shared"
|
provides: "adminRouter guarded by requireAdmin (.use('*', ...) first); GET /members, POST /credentials, GET /calendars, PUT /calendars/:id/shared"
|
||||||
exports: ["adminRouter"]
|
exports: ["adminRouter"]
|
||||||
min_lines: 60
|
min_lines: 60
|
||||||
|
- path: "apps/api/src/broker/credentialSync.ts"
|
||||||
|
provides: "shared validateEncryptAndStoreCredential(userId, fastmailEmail, appPassword, providerType) helper used by BOTH admin + self-service paths"
|
||||||
|
exports: ["validateEncryptAndStoreCredential"]
|
||||||
|
contains: "validateEncryptAndStoreCredential"
|
||||||
- path: "apps/api/src/routes/me.ts"
|
- path: "apps/api/src/routes/me.ts"
|
||||||
provides: "POST /api/me/credential member-scoped self-service (currentUserId only)"
|
provides: "POST /api/me/credential member-scoped self-service (currentUserId only)"
|
||||||
contains: "credential"
|
contains: "credential"
|
||||||
@@ -35,9 +41,13 @@ must_haves:
|
|||||||
via: "adminRouter.use('*', requireAdmin) as the first statement (Pitfall 9)"
|
via: "adminRouter.use('*', requireAdmin) as the first statement (Pitfall 9)"
|
||||||
pattern: "adminRouter\\.use\\('\\*', requireAdmin\\)"
|
pattern: "adminRouter\\.use\\('\\*', requireAdmin\\)"
|
||||||
- from: "apps/api/src/routes/admin.ts"
|
- from: "apps/api/src/routes/admin.ts"
|
||||||
to: "broker validate→encrypt→sync"
|
to: "validateEncryptAndStoreCredential"
|
||||||
via: "createFastmailClient + fetchCalendars, encryptPassword, exported triggerTargetedResync/loadClientForUser"
|
via: "import from ../broker/credentialSync.js (shared validate→encrypt→sync path)"
|
||||||
pattern: "encryptPassword"
|
pattern: "validateEncryptAndStoreCredential"
|
||||||
|
- from: "apps/api/src/routes/me.ts"
|
||||||
|
to: "validateEncryptAndStoreCredential"
|
||||||
|
via: "import from ../broker/credentialSync.js (same helper, currentUserId)"
|
||||||
|
pattern: "validateEncryptAndStoreCredential"
|
||||||
- from: "apps/api/src/index.ts"
|
- from: "apps/api/src/index.ts"
|
||||||
to: "adminRouter"
|
to: "adminRouter"
|
||||||
via: "app.route('/api/admin', adminRouter)"
|
via: "app.route('/api/admin', adminRouter)"
|
||||||
@@ -45,10 +55,10 @@ must_haves:
|
|||||||
---
|
---
|
||||||
|
|
||||||
<objective>
|
<objective>
|
||||||
Build the admin API surface (ADMIN-01 credential rotation + ADMIN-02 shared-calendar designation), gated by `requireAdmin` (ADMIN-03), plus the member-scoped self-service credential endpoint (D-07), all sharing one validate→encrypt→initial-sync path. Promote the broker's private resync helpers to exports so both the admin and self-service paths reuse them. TDD: the credential and guard contracts have precise input→output behavior (403 / 400-no-echo / 200), so write the failing tests first.
|
Build the admin API surface (ADMIN-01 credential rotation + ADMIN-02 shared-calendar designation), gated by `requireAdmin` (ADMIN-03), plus the member-scoped self-service credential endpoint (D-07), all sharing ONE `validateEncryptAndStoreCredential` validate→encrypt→initial-sync helper. Promote the broker's private resync helpers to exports so the shared helper can reuse them. TDD: the credential and guard contracts have precise input→output behavior (403 / 400-no-echo / 200), so write the failing tests first.
|
||||||
|
|
||||||
Purpose: This is the single shared credential + shared-calendar surface (`/api/admin/credentials`, `/api/admin/calendars/:id/shared`) — Phase 12 MUST reuse it, not duplicate it into `/api/setup/*`. The self-service endpoint is the member-scoped counterpart of admin rotation.
|
Purpose: This is the single shared credential + shared-calendar surface (`/api/admin/credentials`, `/api/admin/calendars/:id/shared`) — Phase 12 MUST reuse it, not duplicate it into `/api/setup/*`. The self-service endpoint is the member-scoped counterpart of admin rotation, and it MUST call the exact same credential helper to avoid divergence.
|
||||||
Output: Exported broker helpers, new `admin.ts` router, the `/api/me/credential` self-service endpoint, the index.ts mount, and integration tests covering the Pitfall 7 (no-echo) and Pitfall 9 (403) hard checks.
|
Output: Exported broker helpers, a new shared `credentialSync.ts` helper, the new `admin.ts` router, the `/api/me/credential` self-service endpoint, the index.ts mount, and integration tests covering the Pitfall 7 (no-echo) and Pitfall 9 (403) hard checks.
|
||||||
</objective>
|
</objective>
|
||||||
|
|
||||||
<execution_context>
|
<execution_context>
|
||||||
@@ -79,7 +89,7 @@ Output: Exported broker helpers, new `admin.ts` router, the `/api/me/credential`
|
|||||||
- .planning/phases/10-admin-role-settings/10-PATTERNS.md §`apps/api/src/broker/outboxWorker.ts` (add `export` to both functions; the post-credential-save full sync uses loadClientForUser → fetchCalendars → syncCalendar per davCal) + 10-RESEARCH.md §Pattern 4 + §Open Questions #2 (full per-member poll after a fresh credential save — no known calendarUrl yet) + Assumptions A1
|
- .planning/phases/10-admin-role-settings/10-PATTERNS.md §`apps/api/src/broker/outboxWorker.ts` (add `export` to both functions; the post-credential-save full sync uses loadClientForUser → fetchCalendars → syncCalendar per davCal) + 10-RESEARCH.md §Pattern 4 + §Open Questions #2 (full per-member poll after a fresh credential save — no known calendarUrl yet) + Assumptions A1
|
||||||
</read_first>
|
</read_first>
|
||||||
<action>
|
<action>
|
||||||
Add the `export` keyword to `loadClientForUser` and `triggerTargetedResync` in `apps/api/src/broker/outboxWorker.ts` so the admin + self-service credential routes can reuse them (per 10-PATTERNS.md). Do NOT change their bodies or the outbox drain cycle (A1: standalone fetch+sync helpers, no coupling to the drain loop). After a FRESH credential save there is no known calendarUrl, so the credential routes will call `loadClientForUser(userId)` → `client.fetchCalendars()` → `syncCalendar(...)` per returned DAV calendar (the full per-member poll, mirroring poller.ts) rather than `triggerTargetedResync` — but export both for flexibility. Confirm existing broker tests still pass (no behavior change). NEVER reintroduce node-cron ([[node-cron-skips-in-long-running-process]]) — these helpers are setInterval-driven callers' utilities, untouched.
|
Add the `export` keyword to `loadClientForUser` and `triggerTargetedResync` in `apps/api/src/broker/outboxWorker.ts` so the shared `credentialSync.ts` helper (Task 2) can reuse them (per 10-PATTERNS.md). Do NOT change their bodies or the outbox drain cycle (A1: standalone fetch+sync helpers, no coupling to the drain loop). After a FRESH credential save there is no known calendarUrl, so the shared helper will call `loadClientForUser(userId)` → `client.fetchCalendars()` → `syncCalendar(...)` per returned DAV calendar (the full per-member poll, mirroring poller.ts) rather than `triggerTargetedResync` — but export both for flexibility. Confirm existing broker tests still pass (no behavior change). NEVER reintroduce node-cron ([[node-cron-skips-in-long-running-process]]) — these helpers are setInterval-driven callers' utilities, untouched.
|
||||||
</action>
|
</action>
|
||||||
<verify>
|
<verify>
|
||||||
<automated>cd /home/luc/Projects/familysync && grep -E "^export (async )?function (loadClientForUser|triggerTargetedResync)" apps/api/src/broker/outboxWorker.ts && pnpm --filter @familysync/api test -- outbox 2>&1 | tail -8</automated>
|
<automated>cd /home/luc/Projects/familysync && grep -E "^export (async )?function (loadClientForUser|triggerTargetedResync)" apps/api/src/broker/outboxWorker.ts && pnpm --filter @familysync/api test -- outbox 2>&1 | tail -8</automated>
|
||||||
@@ -93,8 +103,8 @@ Output: Exported broker helpers, new `admin.ts` router, the `/api/me/credential`
|
|||||||
</task>
|
</task>
|
||||||
|
|
||||||
<task type="tdd" tdd="true">
|
<task type="tdd" tdd="true">
|
||||||
<name>Task 2: adminRouter — guard + members + credentials + shared-calendar (RED→GREEN→REFACTOR)</name>
|
<name>Task 2: Shared credentialSync helper + adminRouter (guard + members + credentials + shared-calendar) (RED→GREEN→REFACTOR)</name>
|
||||||
<files>apps/api/src/routes/admin.ts, apps/api/src/index.ts, apps/api/tests/routes/admin.test.ts</files>
|
<files>apps/api/src/broker/credentialSync.ts, apps/api/src/routes/admin.ts, apps/api/src/index.ts, apps/api/tests/routes/admin.test.ts</files>
|
||||||
<read_first>
|
<read_first>
|
||||||
- apps/api/src/routes/push.ts (closest analog: Hono sub-router, zValidator, resolveUserId, ContextVariableMap side-effect import; the subscribeSchema zValidator usage lines ~60–67)
|
- apps/api/src/routes/push.ts (closest analog: Hono sub-router, zValidator, resolveUserId, ContextVariableMap side-effect import; the subscribeSchema zValidator usage lines ~60–67)
|
||||||
- apps/api/src/routes/events.ts (Drizzle leftJoin + where SELECT lines 165–177; the 400-not-422 zValidator convention; onDuplicateKeyUpdate upsert idiom)
|
- apps/api/src/routes/events.ts (Drizzle leftJoin + where SELECT lines 165–177; the 400-not-422 zValidator convention; onDuplicateKeyUpdate upsert idiom)
|
||||||
@@ -107,34 +117,43 @@ Output: Exported broker helpers, new `admin.ts` router, the `/api/me/credential`
|
|||||||
</read_first>
|
</read_first>
|
||||||
<behavior>
|
<behavior>
|
||||||
- Test (RED, Pitfall 9): GET /api/admin/members as a non-admin authenticated user → 403. As an admin → 200 with a list of members each carrying credential status (has credential / not). Integration test imports `app`, never adminRouter directly.
|
- Test (RED, Pitfall 9): GET /api/admin/members as a non-admin authenticated user → 403. As an admin → 200 with a list of members each carrying credential status (has credential / not). Integration test imports `app`, never adminRouter directly.
|
||||||
- Test (Pitfall 7): POST /api/admin/credentials with an INVALID app password (CalDAV PROPFIND fails) → 400, and the response body contains NONE of the submitted password value (assert the exact submitted string is absent from the body) and no Zod `received`/`issues`/`value` field.
|
- Test (Pitfall 7, validation→400 mapping): POST /api/admin/credentials with an INVALID app password (CalDAV PROPFIND fails) → 400, and the response body contains NONE of the submitted password value (assert the exact submitted string is absent from the body) and no Zod `received`/`issues`/`value` field.
|
||||||
|
- Test (Pitfall 7, all failure modes map to one generic 400): a malformed/bad-email payload that makes `createFastmailClient` throw, AND a network/connection error before PROPFIND, BOTH return `{ error: 'Invalid request' }` with status 400 (same generic shape as a PROPFIND auth failure) and never echo the submitted password.
|
||||||
- Test: POST /api/admin/credentials with a VALID credential (CalDAV PROPFIND succeeds) → 200; the stored member_credentials.encrypted_password is NOT the plaintext (encryptPassword applied); response never echoes the password; initial sync is triggered (fire-and-forget).
|
- Test: POST /api/admin/credentials with a VALID credential (CalDAV PROPFIND succeeds) → 200; the stored member_credentials.encrypted_password is NOT the plaintext (encryptPassword applied); response never echoes the password; initial sync is triggered (fire-and-forget).
|
||||||
- Test (Pitfall 9): POST /api/admin/credentials as a non-admin → 403.
|
- Test (Pitfall 9): POST /api/admin/credentials as a non-admin → 403.
|
||||||
- Test (ADMIN-02, Pitfall 7-adjacent): PUT /api/admin/calendars/:id/shared as admin → exactly one calendar has is_shared=1 afterward (the target), any prior shared calendar cleared. As non-admin → 403.
|
- Test (ADMIN-02, Pitfall 7-adjacent): PUT /api/admin/calendars/:id/shared as admin → exactly one calendar has is_shared=1 afterward (the target), any prior shared calendar cleared. As non-admin → 403.
|
||||||
- Test: GET /api/admin/calendars as admin → 200 list of synced calendars (id, name, is_shared). As non-admin → 403.
|
- Test: GET /api/admin/calendars as admin → 200 list of synced calendars (id, name, is_shared). As non-admin → 403.
|
||||||
</behavior>
|
</behavior>
|
||||||
<action>
|
<action>
|
||||||
Create `apps/api/src/routes/admin.ts` exporting `adminRouter = new Hono()` with `adminRouter.use('*', requireAdmin)` as the VERY FIRST statement (Pitfall 9). Add the side-effect import `'../auth/devBypass.js'`. Routes (paths are planner's call per D — use these):
|
First create the SHARED helper `apps/api/src/broker/credentialSync.ts` exporting ONE function `validateEncryptAndStoreCredential(userId: number, fastmailEmail: string, appPassword: string, providerType: string)`. This is the single source of the validate→encrypt→store→initial-sync path; both `/api/admin/credentials` (Task 2) and `/api/me/credential` (Task 3) MUST import and call it — do NOT inline this logic in admin.ts or me.ts. The helper:
|
||||||
|
1. Wraps BOTH `createFastmailClient(fastmailEmail, appPassword)` AND `await client.fetchCalendars()` in ONE try/catch. ANY throw — bad email, malformed input, network/connection error, PROPFIND/auth failure — is treated identically as a credential-validation failure. Signal this to the caller as a single generic outcome (throw a typed `CredentialValidationError` or return a discriminated failure) that the routes map to `{ error: 'Invalid request' }` 400. NEVER include the submitted password (or any Zod/error detail) in the failure path.
|
||||||
|
2. On success: `encryptPassword(appPassword)` → upsert `member_credentials` via `onDuplicateKeyUpdate` (uses the Plan-01 UNIQUE(user_id)) with the given `providerType`.
|
||||||
|
3. Then fire-and-forget the initial full per-member sync (`loadClientForUser(userId)` → `fetchCalendars()` → `syncCalendar` per davCal — the helpers exported in Task 1).
|
||||||
|
NEVER `console.log` the password, the request body, or `c.req.valid('json')` from anywhere in this path.
|
||||||
|
|
||||||
|
Then create `apps/api/src/routes/admin.ts` exporting `adminRouter = new Hono()` with `adminRouter.use('*', requireAdmin)` as the VERY FIRST statement (Pitfall 9). Add the side-effect import `'../auth/devBypass.js'`. Routes (paths are planner's call per D — use these):
|
||||||
- `GET /members`: SELECT users LEFT JOIN member_credentials → return id, displayName, color, hasCredential (boolean). Feeds UI-SPEC Surface 2.
|
- `GET /members`: SELECT users LEFT JOIN member_credentials → return id, displayName, color, hasCredential (boolean). Feeds UI-SPEC Surface 2.
|
||||||
- `POST /credentials`: `zValidator('json', credentialSchema, noEchoHook)` where credentialSchema = `{ userId: number().int().positive(), providerType: literal('caldav'), fastmailEmail: string().email().max(256), appPassword: string().min(1).max(500) }` and noEchoHook returns `c.json({ error: 'Invalid request' }, 400)` (NEVER `c.json(result.error, ...)`). Handler: validate via `createFastmailClient(email, appPassword)` + `client.fetchCalendars()` (throws on auth failure → return 400 generic, NO password in body); on success `encryptPassword(appPassword)` → upsert member_credentials via `onDuplicateKeyUpdate` (uses the Plan-01 UNIQUE(user_id)) with providerType 'caldav'; then fire-and-forget the initial full per-member sync (loadClientForUser → fetchCalendars → syncCalendar per davCal) and return 200. NEVER `console.log` the body or `c.req.valid('json')`.
|
- `POST /credentials`: `zValidator('json', credentialSchema, noEchoHook)` where credentialSchema = `{ userId: number().int().positive(), providerType: literal('caldav'), fastmailEmail: string().email().max(256), appPassword: string().min(1).max(500) }` and noEchoHook returns `c.json({ error: 'Invalid request' }, 400)` (NEVER `c.json(result.error, ...)`). Handler: call `validateEncryptAndStoreCredential(body.userId, body.fastmailEmail, body.appPassword, body.providerType)`; on the helper's validation-failure outcome return `c.json({ error: 'Invalid request' }, 400)` (no password in body); on success return 200. NEVER duplicate the createFastmailClient/fetchCalendars/encrypt logic here.
|
||||||
- `GET /calendars`: SELECT calendars (id, displayName, isShared). Feeds UI-SPEC Surface 5.
|
- `GET /calendars`: SELECT calendars (id, displayName, isShared). Feeds UI-SPEC Surface 5.
|
||||||
- `PUT /calendars/:id/shared`: exclusive update (Pattern 7) — `db.update(calendars).set({isShared:false}).where(eq(calendars.isShared,true))` then `db.update(calendars).set({isShared:true}).where(eq(calendars.id, targetId))` (D-06 single-select). Return 200.
|
- `PUT /calendars/:id/shared`: exclusive update (Pattern 7) — `db.update(calendars).set({isShared:false}).where(eq(calendars.isShared,true))` then `db.update(calendars).set({isShared:true}).where(eq(calendars.id, targetId))` (D-06 single-select). Return 200.
|
||||||
Mount in `apps/api/src/index.ts`: `app.route('/api/admin', adminRouter)` after the existing route block (no extra app-level middleware — the guard lives inside the router). Write `apps/api/tests/routes/admin.test.ts` FIRST with all six behaviors (import `app`), confirm RED, implement to GREEN. Mock/stub CalDAV (createFastmailClient/fetchCalendars) for the validation outcomes to avoid live Fastmail calls in CI (per [[dev-data-user1-no-calendars]] — route-mocks for credential paths).
|
Mount in `apps/api/src/index.ts`: `app.route('/api/admin', adminRouter)` after the existing route block (no extra app-level middleware — the guard lives inside the router). Write `apps/api/tests/routes/admin.test.ts` FIRST with all the behaviors above (import `app`), confirm RED, implement to GREEN. Mock/stub CalDAV (createFastmailClient/fetchCalendars) for the validation outcomes — including the throw-on-createFastmailClient and network-error cases — to avoid live Fastmail calls in CI (per [[dev-data-user1-no-calendars]] — route-mocks for credential paths).
|
||||||
</action>
|
</action>
|
||||||
<verify>
|
<verify>
|
||||||
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/api test -- admin 2>&1 | tail -20</automated>
|
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/api test -- admin 2>&1 | tail -20</automated>
|
||||||
</verify>
|
</verify>
|
||||||
<acceptance_criteria>
|
<acceptance_criteria>
|
||||||
|
- `apps/api/src/broker/credentialSync.ts` exports exactly one `validateEncryptAndStoreCredential` and is the only place the createFastmailClient + fetchCalendars + encryptPassword + upsert + initial-sync sequence appears (`grep -rl "createFastmailClient" apps/api/src/routes/` returns nothing — that logic lives only in credentialSync.ts).
|
||||||
|
- createFastmailClient failures (bad email / malformed input / network error) AND fetchCalendars (PROPFIND/auth) failures BOTH return 400 with body `{ error: 'Invalid request' }`, and the submitted password string appears nowhere in the response or logs.
|
||||||
- `apps/api/src/routes/admin.ts` first statement after router creation is `adminRouter.use('*', requireAdmin)` — `grep -nA1 "new Hono()" apps/api/src/routes/admin.ts` shows the `.use('*', requireAdmin)` immediately after.
|
- `apps/api/src/routes/admin.ts` first statement after router creation is `adminRouter.use('*', requireAdmin)` — `grep -nA1 "new Hono()" apps/api/src/routes/admin.ts` shows the `.use('*', requireAdmin)` immediately after.
|
||||||
- `apps/api/src/index.ts` contains `app.route('/api/admin', adminRouter)`.
|
- `apps/api/src/index.ts` contains `app.route('/api/admin', adminRouter)`.
|
||||||
- GET /api/admin/members returns 403 for a non-admin authenticated user (integration test importing `app`).
|
- GET /api/admin/members returns 403 for a non-admin authenticated user (integration test importing `app`).
|
||||||
- A 400 response from POST /api/admin/credentials with a bad credential contains NO submitted password value and no Zod `received`/`issues` field (test asserts the exact submitted string absent).
|
- A 400 response from POST /api/admin/credentials with a bad credential contains NO submitted password value and no Zod `received`/`issues` field (test asserts the exact submitted string absent).
|
||||||
- On a valid credential, the persisted member_credentials.encrypted_password != the plaintext (encryptPassword applied) and 200 is returned.
|
- On a valid credential, the persisted member_credentials.encrypted_password != the plaintext (encryptPassword applied) and 200 is returned.
|
||||||
- After PUT /api/admin/calendars/:id/shared, exactly one calendar row has is_shared=1.
|
- After PUT /api/admin/calendars/:id/shared, exactly one calendar row has is_shared=1.
|
||||||
- No `console.log`/`console.error` of request bodies in `apps/api/src/routes/admin.ts` (`grep -ciE "console\.(log|error)\(.*(body|valid|password)" apps/api/src/routes/admin.ts` returns 0).
|
- No `console.log`/`console.error` of request bodies in `apps/api/src/routes/admin.ts` or `apps/api/src/broker/credentialSync.ts` (`grep -ciE "console\.(log|error)\(.*(body|valid|password)" apps/api/src/routes/admin.ts apps/api/src/broker/credentialSync.ts` returns 0).
|
||||||
- `pnpm --filter @familysync/api test -- admin` passes all six behaviors.
|
- `pnpm --filter @familysync/api test -- admin` passes all behaviors.
|
||||||
</acceptance_criteria>
|
</acceptance_criteria>
|
||||||
<done>adminRouter exists, guard-first, mounted; members/credentials/calendars/shared routes behave per contract; no-echo + 403 hard checks green.</done>
|
<done>credentialSync.ts holds the single shared validate→encrypt→sync helper; adminRouter exists, guard-first, mounted; members/credentials/calendars/shared routes behave per contract; all credential-validation failures map to one generic 400; no-echo + 403 hard checks green.</done>
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
<task type="tdd" tdd="true">
|
<task type="tdd" tdd="true">
|
||||||
@@ -142,31 +161,31 @@ Output: Exported broker helpers, new `admin.ts` router, the `/api/me/credential`
|
|||||||
<files>apps/api/src/routes/me.ts, apps/api/tests/routes/admin.test.ts</files>
|
<files>apps/api/src/routes/me.ts, apps/api/tests/routes/admin.test.ts</files>
|
||||||
<read_first>
|
<read_first>
|
||||||
- apps/api/src/routes/me.ts (the file being modified — the meRouter export, the resolveUserId/dev-bypass + OIDC user resolution already present; the isAdmin/needsProviderSetup response added in Plan 02)
|
- apps/api/src/routes/me.ts (the file being modified — the meRouter export, the resolveUserId/dev-bypass + OIDC user resolution already present; the isAdmin/needsProviderSetup response added in Plan 02)
|
||||||
- apps/api/src/routes/admin.ts (from Task 2 — reuse the SAME credentialSchema shape minus userId, the SAME noEchoHook, the SAME validate→encrypt→sync sequence; extract any shared helper rather than duplicating the validate/encrypt logic)
|
- apps/api/src/broker/credentialSync.ts (from Task 2 — the SHARED validateEncryptAndStoreCredential helper this route MUST call; do NOT re-implement validate/encrypt/store)
|
||||||
- apps/api/src/broker/crypto.ts + client.ts + outboxWorker.ts (the shared path)
|
- apps/api/src/routes/admin.ts (from Task 2 — reuse the SAME credentialSchema shape minus userId, the SAME noEchoHook, and the SAME 400-mapping convention)
|
||||||
- .planning/phases/10-admin-role-settings/10-PATTERNS.md §Shared Patterns "resolveUserId" + 10-RESEARCH.md §Pattern 4, §Pitfall 6 (cross-member write — endpoint MUST use currentUserId from session, NEVER a body userId), §Architectural Responsibility Map (needsProviderSetup) + §UI-SPEC Surface 4 (self-service onboarding)
|
- .planning/phases/10-admin-role-settings/10-PATTERNS.md §Shared Patterns "resolveUserId" + 10-RESEARCH.md §Pattern 4, §Pitfall 6 (cross-member write — endpoint MUST use currentUserId from session, NEVER a body userId), §Architectural Responsibility Map (needsProviderSetup) + §UI-SPEC Surface 4 (self-service onboarding)
|
||||||
</read_first>
|
</read_first>
|
||||||
<behavior>
|
<behavior>
|
||||||
- Test (RED, Pitfall 6): POST /api/me/credential as user A with a body that includes `userId` for user B → the credential is written to user A (currentUserId), NOT user B; the body userId is ignored.
|
- Test (RED, Pitfall 6): POST /api/me/credential as user A with a body that includes `userId` for user B → the credential is written to user A (currentUserId), NOT user B; the body userId is ignored.
|
||||||
- Test: POST /api/me/credential with a valid credential → 200, stored encrypted for the current user, needsProviderSetup becomes false on the next /api/me; initial sync triggered.
|
- Test: POST /api/me/credential with a valid credential → 200, stored encrypted for the current user, needsProviderSetup becomes false on the next /api/me; initial sync triggered.
|
||||||
- Test (Pitfall 7): POST /api/me/credential with a bad credential → 400 generic, no password echoed.
|
- Test (Pitfall 7): POST /api/me/credential with a bad credential → 400 generic `{ error: 'Invalid request' }`, no password echoed (same helper, same 400 mapping as admin).
|
||||||
- Test: the endpoint does NOT require admin (a normal member can set their own credential) but is still behind the auth guard (unauthenticated → 401 from the outer band).
|
- Test: the endpoint does NOT require admin (a normal member can set their own credential) but is still behind the auth guard (unauthenticated → 401 from the outer band).
|
||||||
</behavior>
|
</behavior>
|
||||||
<action>
|
<action>
|
||||||
Add `POST /credential` to the meRouter in `apps/api/src/routes/me.ts` (final path `/api/me/credential`), member-scoped. Schema = the admin credentialSchema WITHOUT `userId` (`{ providerType: literal('caldav'), fastmailEmail, appPassword }`) + the SAME `noEchoHook`. The handler resolves `currentUserId` via the existing resolveUserId/dev-bypass pattern and ALWAYS writes to that id — it MUST NOT read a userId from the body (Pitfall 6). Reuse the SAME validate→encrypt→initial-sync path as admin Task 2 (extract a shared helper, e.g. `validateEncryptAndStoreCredential(userId, email, password)`, to avoid divergence — D-07 "identical path"). Add the self-service test cases to `apps/api/tests/routes/admin.test.ts` (or a sibling me-credential test — planner's call; keep them with the credential-surface tests). Write tests FIRST, confirm RED, implement to GREEN.
|
Add `POST /credential` to the meRouter in `apps/api/src/routes/me.ts` (final path `/api/me/credential`), member-scoped. Schema = the admin credentialSchema WITHOUT `userId` (`{ providerType: literal('caldav'), fastmailEmail, appPassword }`) + the SAME `noEchoHook`. The handler resolves `currentUserId` via the existing resolveUserId/dev-bypass pattern and ALWAYS writes to that id — it MUST NOT read a userId from the body (Pitfall 6). It MUST call the SAME shared helper from Task 2: `validateEncryptAndStoreCredential(currentUserId, body.fastmailEmail, body.appPassword, body.providerType)` (import from `../broker/credentialSync.js`). Do NOT duplicate the validate/encrypt/store/sync sequence — admin passes the target member's userId from the route/body, self-service passes the authenticated currentUserId, but both call the identical helper with identical argument order (D-07 "identical path"). Map the helper's validation-failure outcome to `c.json({ error: 'Invalid request' }, 400)`; on success 200. Add the self-service test cases to `apps/api/tests/routes/admin.test.ts` (keep them with the credential-surface tests). Write tests FIRST, confirm RED, implement to GREEN.
|
||||||
</action>
|
</action>
|
||||||
<verify>
|
<verify>
|
||||||
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/api test -- credential 2>&1 | tail -15</automated>
|
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/api test -- credential 2>&1 | tail -15</automated>
|
||||||
</verify>
|
</verify>
|
||||||
<acceptance_criteria>
|
<acceptance_criteria>
|
||||||
- `apps/api/src/routes/me.ts` adds a `POST /credential` route; the final mounted path is `/api/me/credential`.
|
- `apps/api/src/routes/me.ts` adds a `POST /credential` route; the final mounted path is `/api/me/credential`.
|
||||||
|
- `validateEncryptAndStoreCredential` is defined once in `apps/api/src/broker/credentialSync.ts`; BOTH `apps/api/src/routes/admin.ts` (POST /credentials) and `apps/api/src/routes/me.ts` (POST /credential) import and call it with identical arguments — admin passes the target member's userId from the route/body, self-service passes the authenticated currentUserId, never a body userId. `grep -rc "validateEncryptAndStoreCredential" apps/api/src/routes/admin.ts apps/api/src/routes/me.ts` shows a call in each (and the function body exists only in credentialSync.ts).
|
||||||
- A POST to /api/me/credential with a body `userId` for another user writes ONLY to the current session user (test proves the other user's credential is untouched).
|
- A POST to /api/me/credential with a body `userId` for another user writes ONLY to the current session user (test proves the other user's credential is untouched).
|
||||||
- The self-service path reuses the same validate→encrypt→store logic as the admin path (shared helper; no duplicated encrypt/PROPFIND block) — `grep` shows a single shared function called by both routes.
|
- A bad credential returns 400 generic `{ error: 'Invalid request' }` with no echoed password.
|
||||||
- A bad credential returns 400 generic with no echoed password.
|
|
||||||
- A normal (non-admin) member can succeed on /api/me/credential (no requireAdmin on this route).
|
- A normal (non-admin) member can succeed on /api/me/credential (no requireAdmin on this route).
|
||||||
- `pnpm --filter @familysync/api test -- credential` passes all cases.
|
- `pnpm --filter @familysync/api test -- credential` passes all cases.
|
||||||
</acceptance_criteria>
|
</acceptance_criteria>
|
||||||
<done>Member self-service credential endpoint exists, member-scoped (no cross-member write), shares the admin validate→encrypt→sync path, tests green.</done>
|
<done>Member self-service credential endpoint exists, member-scoped (no cross-member write), calls the SAME shared validateEncryptAndStoreCredential helper as the admin path, tests green.</done>
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
</tasks>
|
</tasks>
|
||||||
@@ -174,11 +193,11 @@ Output: Exported broker helpers, new `admin.ts` router, the `/api/me/credential`
|
|||||||
<artifacts_this_phase_produces>
|
<artifacts_this_phase_produces>
|
||||||
New symbols/files created by this plan (excluded from drift verification):
|
New symbols/files created by this plan (excluded from drift verification):
|
||||||
- `export` on `loadClientForUser` + `triggerTargetedResync` in `apps/api/src/broker/outboxWorker.ts`
|
- `export` on `loadClientForUser` + `triggerTargetedResync` in `apps/api/src/broker/outboxWorker.ts`
|
||||||
|
- `apps/api/src/broker/credentialSync.ts` exporting the single shared `validateEncryptAndStoreCredential(userId, fastmailEmail, appPassword, providerType)` helper (validate→encrypt→store→initial-sync; the only place createFastmailClient + fetchCalendars + encryptPassword + upsert live)
|
||||||
- `apps/api/src/routes/admin.ts` exporting `adminRouter` with `GET /members`, `POST /credentials`, `GET /calendars`, `PUT /calendars/:id/shared`
|
- `apps/api/src/routes/admin.ts` exporting `adminRouter` with `GET /members`, `POST /credentials`, `GET /calendars`, `PUT /calendars/:id/shared`
|
||||||
- `requireAdmin` applied as `adminRouter.use('*', requireAdmin)` (consumes the Plan-02 guard)
|
- `requireAdmin` applied as `adminRouter.use('*', requireAdmin)` (consumes the Plan-02 guard)
|
||||||
- `app.route('/api/admin', adminRouter)` mount in `apps/api/src/index.ts`
|
- `app.route('/api/admin', adminRouter)` mount in `apps/api/src/index.ts`
|
||||||
- `POST /api/me/credential` member-scoped self-service endpoint in `apps/api/src/routes/me.ts`
|
- `POST /api/me/credential` member-scoped self-service endpoint in `apps/api/src/routes/me.ts` (calls the shared helper)
|
||||||
- shared `validateEncryptAndStoreCredential` helper (admin + self-service)
|
|
||||||
- `apps/api/tests/routes/admin.test.ts` (+ self-service credential test cases)
|
- `apps/api/tests/routes/admin.test.ts` (+ self-service credential test cases)
|
||||||
</artifacts_this_phase_produces>
|
</artifacts_this_phase_produces>
|
||||||
|
|
||||||
@@ -197,10 +216,10 @@ New symbols/files created by this plan (excluded from drift verification):
|
|||||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|-----------|----------|-----------|-------------|-----------------|
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
| T-10-08 | Elevation of Privilege | non-admin hitting /api/admin/* | mitigate | adminRouter.use('*', requireAdmin) FIRST (Pitfall 9); integration tests import `app` and assert 403 on every admin route for a non-admin |
|
| T-10-08 | Elevation of Privilege | non-admin hitting /api/admin/* | mitigate | adminRouter.use('*', requireAdmin) FIRST (Pitfall 9); integration tests import `app` and assert 403 on every admin route for a non-admin |
|
||||||
| T-10-09 | Information Disclosure | app password echoed in a Zod/validation error | mitigate | noEchoHook returns `{ error: 'Invalid request' }` with no result.error; test asserts the submitted password string is absent from any 400 body (Pitfall 7) |
|
| T-10-09 | Information Disclosure | app password echoed in a Zod/validation error | mitigate | noEchoHook returns `{ error: 'Invalid request' }` with no result.error; ALL credential-validation failures (createFastmailClient throw, network error, PROPFIND/auth failure) map to one generic 400 in the shared helper; test asserts the submitted password string is absent from any 400 body (Pitfall 7) |
|
||||||
| T-10-10 | Information Disclosure | app password logged | mitigate | No console.log of body/valid()/password in admin.ts or the me credential route (acceptance grep == 0) |
|
| T-10-10 | Information Disclosure | app password logged | mitigate | No console.log of body/valid()/password in admin.ts, me.ts, or credentialSync.ts (acceptance grep == 0) |
|
||||||
| T-10-11 | Information Disclosure | plaintext credential at rest | mitigate | encryptPassword (AES-256-GCM via crypto.ts) applied before the DB write; test asserts stored value != plaintext; no new crypto written |
|
| T-10-11 | Information Disclosure | plaintext credential at rest | mitigate | encryptPassword (AES-256-GCM via crypto.ts) applied in the shared helper before the DB write; test asserts stored value != plaintext; no new crypto written |
|
||||||
| T-10-12 | Elevation of Privilege / IDOR | member self-service writes another member's credential | mitigate | /api/me/credential always uses currentUserId from the session and ignores any body userId (Pitfall 6); test proves the other user's row is untouched |
|
| T-10-12 | Elevation of Privilege / IDOR | member self-service writes another member's credential | mitigate | /api/me/credential always passes currentUserId from the session to the shared helper and ignores any body userId (Pitfall 6); test proves the other user's row is untouched |
|
||||||
| T-10-13 | IDOR | admin rotating an arbitrary member's credential | accept | D-05 explicitly allows an admin to rotate ANY member's credential; this is gated by requireAdmin and is the intended capability (the self-service path remains member-scoped) |
|
| T-10-13 | IDOR | admin rotating an arbitrary member's credential | accept | D-05 explicitly allows an admin to rotate ANY member's credential; this is gated by requireAdmin and is the intended capability (the self-service path remains member-scoped) |
|
||||||
| T-10-SC | Tampering | npm/pip/cargo installs | mitigate | No new packages this phase (RESEARCH Package Legitimacy Audit); no install task |
|
| T-10-SC | Tampering | npm/pip/cargo installs | mitigate | No new packages this phase (RESEARCH Package Legitimacy Audit); no install task |
|
||||||
</threat_model>
|
</threat_model>
|
||||||
@@ -209,17 +228,20 @@ New symbols/files created by this plan (excluded from drift verification):
|
|||||||
- `pnpm --filter @familysync/api test -- admin && pnpm --filter @familysync/api test -- credential && pnpm --filter @familysync/api test -- outbox` all pass.
|
- `pnpm --filter @familysync/api test -- admin && pnpm --filter @familysync/api test -- credential && pnpm --filter @familysync/api test -- outbox` all pass.
|
||||||
- `pnpm --filter @familysync/api exec tsc --noEmit` passes (run tsc separately per [[vitest-passes-tsc-fails]]).
|
- `pnpm --filter @familysync/api exec tsc --noEmit` passes (run tsc separately per [[vitest-passes-tsc-fails]]).
|
||||||
- `grep -A1 "new Hono()" apps/api/src/routes/admin.ts` shows `.use('*', requireAdmin)` first.
|
- `grep -A1 "new Hono()" apps/api/src/routes/admin.ts` shows `.use('*', requireAdmin)` first.
|
||||||
- `grep -ciE "console\.(log|error)\(.*(body|valid|password)" apps/api/src/routes/admin.ts` == 0.
|
- `grep -ciE "console\.(log|error)\(.*(body|valid|password)" apps/api/src/routes/admin.ts apps/api/src/broker/credentialSync.ts` == 0.
|
||||||
|
- `validateEncryptAndStoreCredential` is imported and called by both admin.ts and me.ts; its body exists only in credentialSync.ts (no duplicated createFastmailClient/encrypt block in the routes).
|
||||||
</verification>
|
</verification>
|
||||||
|
|
||||||
<success_criteria>
|
<success_criteria>
|
||||||
- ADMIN-01: admin can rotate any member's credential, CalDAV-validated, encrypted, never echoed/logged (Success Criterion 2).
|
- ADMIN-01: admin can rotate any member's credential, CalDAV-validated, encrypted, never echoed/logged (Success Criterion 2).
|
||||||
- ADMIN-02: admin sets exactly one shared calendar via the API (Success Criterion 3).
|
- ADMIN-02: admin sets exactly one shared calendar via the API (Success Criterion 3).
|
||||||
- ADMIN-03: every /api/admin/* route 403s non-admins (Success Criterion 1); guard inside the sub-router (Pitfall 9).
|
- ADMIN-03: every /api/admin/* route 403s non-admins (Success Criterion 1); guard inside the sub-router (Pitfall 9).
|
||||||
- D-07: member self-service credential, member-scoped, same path.
|
- D-07: member self-service credential, member-scoped, SAME shared helper path (no divergence).
|
||||||
- Single shared surface — no /api/setup/* duplication (Phase 12 reuses these routes).
|
- Single shared surface — no /api/setup/* duplication (Phase 12 reuses these routes + the shared helper).
|
||||||
</success_criteria>
|
</success_criteria>
|
||||||
|
|
||||||
<output>
|
<output>
|
||||||
Create `.planning/phases/10-admin-role-settings/10-03-SUMMARY.md` when done.
|
Create `.planning/phases/10-admin-role-settings/10-03-SUMMARY.md` when done.
|
||||||
</output>
|
</output>
|
||||||
|
</content>
|
||||||
|
</invoke>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ must_haves:
|
|||||||
- "An admin sees an Admin nav entry, reaches /admin, can list members + credential status, rotate a member credential via the sheet, and pick the shared calendar"
|
- "An admin sees an Admin nav entry, reaches /admin, can list members + credential status, rotate a member credential via the sheet, and pick the shared calendar"
|
||||||
- "A non-admin never sees the Admin nav entry and is redirected from /admin to /calendar"
|
- "A non-admin never sees the Admin nav entry and is redirected from /admin to /calendar"
|
||||||
- "A member with needsProviderSetup=true sees the self-service SetupBanner and can add their own credential via the same sheet (member-scoped)"
|
- "A member with needsProviderSetup=true sees the self-service SetupBanner and can add their own credential via the same sheet (member-scoped)"
|
||||||
|
- "After a successful credential save the SetupBanner clears (needsProviderSetup→false via ['me'] invalidation), there being no dismiss button"
|
||||||
artifacts:
|
artifacts:
|
||||||
- path: "apps/pwa/src/routes/AdminPage.tsx"
|
- path: "apps/pwa/src/routes/AdminPage.tsx"
|
||||||
provides: "/admin page: Members section + Shared-Calendar picker, wired to /api/admin/*"
|
provides: "/admin page: Members section + Shared-Calendar picker, wired to /api/admin/*"
|
||||||
@@ -108,8 +109,11 @@ Output: Extended client.ts, AdminPage.tsx, CredentialSheet.tsx, SetupBanner.tsx,
|
|||||||
- .planning/phases/10-admin-role-settings/10-UI-SPEC.md Surface 3 (credential sheet: heading variants, member subtitle, password field type="password"/autocomplete="new-password"/never pre-filled, helper text + Fastmail app-password link target="_blank" rel="noopener noreferrer", "Validating against CalDAV…" Loader2 spinner, failure copy, Save/Cancel actions) + Surface 4 (self-service banner: KeyRound icon, copy, "Set up now" CTA, no X/dismiss) + the Copywriting Contract (exact strings) + Accessibility Contracts + Color/Typography/Spacing (all via var(--token), 44px touch targets)
|
- .planning/phases/10-admin-role-settings/10-UI-SPEC.md Surface 3 (credential sheet: heading variants, member subtitle, password field type="password"/autocomplete="new-password"/never pre-filled, helper text + Fastmail app-password link target="_blank" rel="noopener noreferrer", "Validating against CalDAV…" Loader2 spinner, failure copy, Save/Cancel actions) + Surface 4 (self-service banner: KeyRound icon, copy, "Set up now" CTA, no X/dismiss) + the Copywriting Contract (exact strings) + Accessibility Contracts + Color/Typography/Spacing (all via var(--token), 44px touch targets)
|
||||||
- .planning/phases/10-admin-role-settings/10-PATTERNS.md §`apps/pwa/src/components/CredentialSheet.tsx` + §`apps/pwa/src/components/SetupBanner.tsx` (structural + mutation + style excerpts) + §Shared Patterns "CSS token inline style pattern"
|
- .planning/phases/10-admin-role-settings/10-PATTERNS.md §`apps/pwa/src/components/CredentialSheet.tsx` + §`apps/pwa/src/components/SetupBanner.tsx` (structural + mutation + style excerpts) + §Shared Patterns "CSS token inline style pattern"
|
||||||
</read_first>
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- SetupBanner dismissal (success-only): there is NO dismiss/X button per UI-SPEC Surface 4 — the ONLY way the banner clears is a successful credential save. After a successful save the CredentialSheet mutation's `onSuccess` invalidates the `['me']` query → /api/me refetches → `needsProviderSetup` becomes `false` → SetupBanner unmounts on the next rerender. A test/behavior assertion: given `needsProviderSetup=true` the banner renders; after a successful save (mocked) that flips /api/me to `needsProviderSetup=false`, the banner is no longer in the DOM. (No interaction other than success clears it.)
|
||||||
|
</behavior>
|
||||||
<action>
|
<action>
|
||||||
Build `CredentialSheet.tsx` (shared by admin rotation AND self-service per D-07) following the SettingsSheet bottom-sheet pattern: props for mode (admin-rotate / admin-add / self-service), target member (admin) or current user (self-service), open/close. Render the heading variant per UI-SPEC Copywriting Contract ("Rotate Credential" / "Add Credential" / "Add your calendar credential"), the member-name subtitle, a `type="password" autoComplete="new-password"` field NEVER pre-filled, the helper text with the Fastmail app-password link (exact URL + copy from UI-SPEC, opens in new tab), the "Validating against CalDAV…" inline state (Loader2 size 16) during the mutation, the failure error copy on a CalDAV 400, and Cancel (ghost) + Save Credential (accent-filled) actions. Use a TanStack `useMutation` that calls `saveCredential` (admin) or `saveMyCredential` (self-service) and on success invalidates `['admin','members']` + `['me']` (so needsProviderSetup refreshes) and closes the sheet. Build `SetupBanner.tsx` following PermissionDeniedBanner: render only when `meQuery.data?.user.needsProviderSetup === true`, `role="status" aria-live="polite"`, KeyRound icon, the exact heading/body/CTA copy, "Set up now" opening the CredentialSheet in self-service mode; NO dismiss button (it clears when needsProviderSetup becomes false). All styling via `var(--token)`; every interactive element minWidth/minHeight 44px. Never log/echo the password.
|
Build `CredentialSheet.tsx` (shared by admin rotation AND self-service per D-07) following the SettingsSheet bottom-sheet pattern: props for mode (admin-rotate / admin-add / self-service), target member (admin) or current user (self-service), open/close. Render the heading variant per UI-SPEC Copywriting Contract ("Rotate Credential" / "Add Credential" / "Add your calendar credential"), the member-name subtitle, a `type="password" autoComplete="new-password"` field NEVER pre-filled, the helper text with the Fastmail app-password link (exact URL + copy from UI-SPEC, opens in new tab), the "Validating against CalDAV…" inline state (Loader2 size 16) during the mutation, the failure error copy on a CalDAV 400, and Cancel (ghost) + Save Credential (accent-filled) actions. Use a TanStack `useMutation` that calls `saveCredential` (admin) or `saveMyCredential` (self-service) and on success invalidates `['admin','members']` + `['me']` (so needsProviderSetup refreshes and the SetupBanner clears) and closes the sheet. Build `SetupBanner.tsx` following PermissionDeniedBanner: render only when `meQuery.data?.user.needsProviderSetup === true`, `role="status" aria-live="polite"`, KeyRound icon, the exact heading/body/CTA copy, "Set up now" opening the CredentialSheet in self-service mode; NO dismiss button (it clears ONLY when needsProviderSetup becomes false after a successful save — the success-only dismissal behavior above). All styling via `var(--token)`; every interactive element minWidth/minHeight 44px. Never log/echo the password.
|
||||||
</action>
|
</action>
|
||||||
<verify>
|
<verify>
|
||||||
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/pwa exec tsc --noEmit 2>&1 | tail -8 && grep -l "autocomplete=\"new-password\"\|autoComplete=\"new-password\"" apps/pwa/src/components/CredentialSheet.tsx</automated>
|
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/pwa exec tsc --noEmit 2>&1 | tail -8 && grep -l "autocomplete=\"new-password\"\|autoComplete=\"new-password\"" apps/pwa/src/components/CredentialSheet.tsx</automated>
|
||||||
@@ -119,11 +123,12 @@ Output: Extended client.ts, AdminPage.tsx, CredentialSheet.tsx, SetupBanner.tsx,
|
|||||||
- The helper text contains the Fastmail app-password link with `target="_blank"` and `rel="noopener noreferrer"`.
|
- The helper text contains the Fastmail app-password link with `target="_blank"` and `rel="noopener noreferrer"`.
|
||||||
- The success mutation invalidates both `['admin','members']` and `['me']` (`grep -c "invalidateQueries" apps/pwa/src/components/CredentialSheet.tsx` >= 2).
|
- The success mutation invalidates both `['admin','members']` and `['me']` (`grep -c "invalidateQueries" apps/pwa/src/components/CredentialSheet.tsx` >= 2).
|
||||||
- `SetupBanner.tsx` renders conditionally on `needsProviderSetup`, uses `role="status"`/`aria-live`, and has NO dismiss/X button.
|
- `SetupBanner.tsx` renders conditionally on `needsProviderSetup`, uses `role="status"`/`aria-live`, and has NO dismiss/X button.
|
||||||
|
- SetupBanner success-only dismissal holds: when /api/me reports `needsProviderSetup=false` (the state after a successful save invalidates `['me']`), the banner does not render — there is no code path that hides it other than the `needsProviderSetup` flag flipping to false.
|
||||||
- Exact UI-SPEC Copywriting Contract strings are present (e.g. "Set up your calendar", "Validating against CalDAV…", "Save Credential").
|
- Exact UI-SPEC Copywriting Contract strings are present (e.g. "Set up your calendar", "Validating against CalDAV…", "Save Credential").
|
||||||
- No hard-coded color/spacing px except the 44px/48px touch-target minimums; values reference `var(--...)`.
|
- No hard-coded color/spacing px except the 44px/48px touch-target minimums; values reference `var(--...)`.
|
||||||
- `pnpm --filter @familysync/pwa exec tsc --noEmit` exits 0.
|
- `pnpm --filter @familysync/pwa exec tsc --noEmit` exits 0.
|
||||||
</acceptance_criteria>
|
</acceptance_criteria>
|
||||||
<done>CredentialSheet (admin + self-service) and SetupBanner match the UI-SPEC contract, accessible, token-styled, typecheck clean.</done>
|
<done>CredentialSheet (admin + self-service) and SetupBanner match the UI-SPEC contract, accessible, token-styled, success-only banner dismissal wired via ['me'] invalidation, typecheck clean.</done>
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
<task type="execute">
|
<task type="execute">
|
||||||
@@ -134,12 +139,18 @@ Output: Extended client.ts, AdminPage.tsx, CredentialSheet.tsx, SetupBanner.tsx,
|
|||||||
- apps/pwa/src/routes/ListsIndex.tsx (page-level component analog for AdminPage: TanStack Query + sections layout)
|
- apps/pwa/src/routes/ListsIndex.tsx (page-level component analog for AdminPage: TanStack Query + sections layout)
|
||||||
- apps/pwa/src/components/AppNav.tsx (NavLink + Lucide pattern lines 14–15, the DesktopNav section) + apps/pwa/src/components/BottomTabBar.tsx (tab pattern lines 27–98) — add the conditional Admin entry (ShieldCheck) to both
|
- apps/pwa/src/components/AppNav.tsx (NavLink + Lucide pattern lines 14–15, the DesktopNav section) + apps/pwa/src/components/BottomTabBar.tsx (tab pattern lines 27–98) — add the conditional Admin entry (ShieldCheck) to both
|
||||||
- apps/pwa/e2e/calendar.spec.ts + layout.spec.ts + e2e/README.md (existing spec idioms, selectors, the dev-bypass admin user seeded in global-setup as id=1 is_admin=true from Plan 01)
|
- apps/pwa/e2e/calendar.spec.ts + layout.spec.ts + e2e/README.md (existing spec idioms, selectors, the dev-bypass admin user seeded in global-setup as id=1 is_admin=true from Plan 01)
|
||||||
- .claude/skills/playwright-cli/SKILL.md (how to drive the global playwright-cli binary for the browser verification)
|
- .claude/skills/playwright-cli/SKILL.md (how to drive the global playwright-cli binary for the supplementary browser verification)
|
||||||
- .planning/phases/10-admin-role-settings/10-UI-SPEC.md Surface 1 (/admin page: AppNav persistent, ShieldCheck size 18, content maxWidth 640px centered desktop, var(--space-12) vertical padding, "Admin Settings" heading) + Surface 2 (Members section) + Surface 5 (Shared Calendar picker: radio group, "Currently shared" label, two-tap Save, empty state) + Copywriting Contract
|
- .planning/phases/10-admin-role-settings/10-UI-SPEC.md Surface 1 (/admin page: AppNav persistent, ShieldCheck size 18, content maxWidth 640px centered desktop, var(--space-12) vertical padding, "Admin Settings" heading) + Surface 2 (Members section) + Surface 5 (Shared Calendar picker: radio group, "Currently shared" label, two-tap Save, empty state) + Copywriting Contract
|
||||||
- .planning/phases/10-admin-role-settings/10-PATTERNS.md §`apps/pwa/src/routes/AdminPage.tsx` + §`apps/pwa/src/App.tsx` + §Shared Patterns "NavLink + Lucide icon"
|
- .planning/phases/10-admin-role-settings/10-PATTERNS.md §`apps/pwa/src/routes/AdminPage.tsx` + §`apps/pwa/src/App.tsx` + §Shared Patterns "NavLink + Lucide icon"
|
||||||
</read_first>
|
</read_first>
|
||||||
<action>
|
<action>
|
||||||
Build `AdminPage.tsx` (analog ListsIndex): "Admin Settings" heading (18px/600), a MEMBERS section listing members from `fetchAdminMembers` (avatar swatch + name + credential status badge per UI-SPEC Surface 2 + a "Rotate"/"Add credential" button opening CredentialSheet in admin mode for that member), and a SHARED CALENDAR section (Surface 5) using `fetchAdminCalendars`: an exclusive single-select radio group (D-06), the "Currently shared" label on the active one, a two-tap "Save" button (disabled until selection differs) calling `setSharedCalendar`, and the empty state ("No calendars synced yet") when none synced. Desktop: centered column maxWidth 640px. In `App.tsx`: add the `/admin` Route (D-02: new dedicated gated route) gated by `meQuery.data?.user.isAdmin ? <AdminPage/> : <Navigate to="/calendar" replace/>` — the client redirect is UX-only per D-03 (the server-side 403 from Plan 03 is the real boundary); add a loading gate so an in-flight meQuery doesn't flash-redirect (planner's call per 10-PATTERNS.md note); mount `<SetupBanner/>` above the calendar content (renders only on needsProviderSetup). In `AppNav.tsx` (desktop) and `BottomTabBar.tsx` (mobile): add an Admin entry (ShieldCheck icon, `aria-label="Admin settings"`) rendered ONLY when `meQuery.data?.user.isAdmin === true` (D-03 UX gating). Write `apps/pwa/e2e/admin.spec.ts`: with the dev-bypass admin user (seeded id=1 is_admin=true), assert the Admin nav entry is visible and /admin renders "Admin Settings" + the Members section. Add a non-admin assertion by route-mocking GET /api/me to `isAdmin:false` (per [[dev-data-user1-no-calendars]] route-mock idiom and the lists.spec page.route precedent) and asserting the Admin nav entry is absent and /admin redirects to /calendar. THEN run a playwright-cli browser check against the dev stack to confirm the guard + nav gating + sheet open behaviors interactively (use the playwright-cli skill — this replaces a human-verify checkpoint since it is desktop-Chromium-drivable per CLAUDE.md). All styling via `var(--token)`; 44px touch targets.
|
Build `AdminPage.tsx` (analog ListsIndex): "Admin Settings" heading (18px/600), a MEMBERS section listing members from `fetchAdminMembers` (avatar swatch + name + credential status badge per UI-SPEC Surface 2 + a "Rotate"/"Add credential" button opening CredentialSheet in admin mode for that member), and a SHARED CALENDAR section (Surface 5) using `fetchAdminCalendars`: an exclusive single-select radio group (D-06), the "Currently shared" label on the active one, a two-tap "Save" button (disabled until selection differs) calling `setSharedCalendar`, and the empty state ("No calendars synced yet") when none synced. Desktop: centered column maxWidth 640px. In `App.tsx`: add the `/admin` Route (D-02: new dedicated gated route) gated by `meQuery.data?.user.isAdmin ? <AdminPage/> : <Navigate to="/calendar" replace/>` — the client redirect is UX-only per D-03 (the server-side 403 from Plan 03 is the real boundary); add a loading gate so an in-flight meQuery doesn't flash-redirect (planner's call per 10-PATTERNS.md note); mount `<SetupBanner/>` above the calendar content (renders only on needsProviderSetup). In `AppNav.tsx` (desktop) and `BottomTabBar.tsx` (mobile): add an Admin entry (ShieldCheck icon, `aria-label="Admin settings"`) rendered ONLY when `meQuery.data?.user.isAdmin === true` (D-03 UX gating).
|
||||||
|
|
||||||
|
Verification has two layers, kept distinct:
|
||||||
|
1. AUTOMATED GATE (the verify command): write `apps/pwa/e2e/admin.spec.ts` and run `pnpm --filter @familysync/pwa test:e2e -- admin`. With the dev-bypass admin user (seeded id=1 is_admin=true), assert the Admin nav entry is visible and /admin renders "Admin Settings" + the Members section. Add a non-admin assertion by route-mocking GET /api/me to `isAdmin:false` (per [[dev-data-user1-no-calendars]] route-mock idiom and the lists.spec page.route precedent) and asserting the Admin nav entry is absent and /admin redirects to /calendar. THIS e2e SPEC IS THE GATE.
|
||||||
|
2. SUPPLEMENTARY (not the gate): run an interactive `playwright-cli` check against the running dev stack at the `/admin` route to confirm the guard redirect + nav gating + CredentialSheet opens — drive the global `/usr/local/bin/playwright-cli` binary per `.claude/skills/playwright-cli/SKILL.md` (navigate to /admin as the dev-bypass admin, confirm "Admin Settings" + open the credential sheet; then with a route-mocked non-admin confirm the redirect to /calendar). Record the playwright-cli observations in the SUMMARY. This is an optional supplementary confirmation; if the dev stack is not up it does not block the plan — the e2e spec is the binding proof.
|
||||||
|
|
||||||
|
All styling via `var(--token)`; 44px touch targets.
|
||||||
</action>
|
</action>
|
||||||
<verify>
|
<verify>
|
||||||
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/pwa exec tsc --noEmit 2>&1 | tail -6 && pnpm --filter @familysync/pwa test:e2e -- admin 2>&1 | tail -20</automated>
|
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/pwa exec tsc --noEmit 2>&1 | tail -6 && pnpm --filter @familysync/pwa test:e2e -- admin 2>&1 | tail -20</automated>
|
||||||
@@ -149,10 +160,10 @@ Output: Extended client.ts, AdminPage.tsx, CredentialSheet.tsx, SetupBanner.tsx,
|
|||||||
- `AppNav.tsx` and `BottomTabBar.tsx` render the Admin entry (ShieldCheck, `aria-label="Admin settings"`) ONLY when isAdmin is true (`grep -c "ShieldCheck" apps/pwa/src/components/AppNav.tsx apps/pwa/src/components/BottomTabBar.tsx` >= 2).
|
- `AppNav.tsx` and `BottomTabBar.tsx` render the Admin entry (ShieldCheck, `aria-label="Admin settings"`) ONLY when isAdmin is true (`grep -c "ShieldCheck" apps/pwa/src/components/AppNav.tsx apps/pwa/src/components/BottomTabBar.tsx` >= 2).
|
||||||
- `AdminPage.tsx` renders "Admin Settings", a MEMBERS list, and a SHARED CALENDAR exclusive radio group with a two-tap Save and an empty state.
|
- `AdminPage.tsx` renders "Admin Settings", a MEMBERS list, and a SHARED CALENDAR exclusive radio group with a two-tap Save and an empty state.
|
||||||
- `apps/pwa/e2e/admin.spec.ts` asserts: admin sees the nav entry + reaches /admin; a non-admin (route-mocked isAdmin:false) does NOT see it and /admin redirects to /calendar.
|
- `apps/pwa/e2e/admin.spec.ts` asserts: admin sees the nav entry + reaches /admin; a non-admin (route-mocked isAdmin:false) does NOT see it and /admin redirects to /calendar.
|
||||||
- `pnpm --filter @familysync/pwa test:e2e -- admin` passes; the playwright-cli interactive check confirms the guard + nav gating (recorded in the SUMMARY).
|
- The e2e spec (`pnpm --filter @familysync/pwa test:e2e -- admin`) passes and IS the gate; the playwright-cli interactive check is a supplementary confirmation (guard + nav gating + sheet open) recorded in the SUMMARY, not the binding proof.
|
||||||
- `pnpm --filter @familysync/pwa exec tsc --noEmit` exits 0.
|
- `pnpm --filter @familysync/pwa exec tsc --noEmit` exits 0.
|
||||||
</acceptance_criteria>
|
</acceptance_criteria>
|
||||||
<done>/admin route gated, AdminPage wired to the admin API, conditional nav entries + SetupBanner mounted, e2e + playwright-cli verification green.</done>
|
<done>/admin route gated, AdminPage wired to the admin API, conditional nav entries + SetupBanner mounted, e2e gate green; playwright-cli supplementary check recorded in the SUMMARY.</done>
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
</tasks>
|
</tasks>
|
||||||
@@ -189,17 +200,19 @@ New symbols/files created by this plan (excluded from drift verification):
|
|||||||
|
|
||||||
<verification>
|
<verification>
|
||||||
- `pnpm --filter @familysync/pwa exec tsc --noEmit` passes.
|
- `pnpm --filter @familysync/pwa exec tsc --noEmit` passes.
|
||||||
- `pnpm --filter @familysync/pwa test:e2e -- admin` passes (admin sees nav + /admin; non-admin redirected, no nav entry).
|
- `pnpm --filter @familysync/pwa test:e2e -- admin` passes (admin sees nav + /admin; non-admin redirected, no nav entry) — this is the binding gate.
|
||||||
- playwright-cli interactive check confirms the guard + nav gating + sheet open (recorded in SUMMARY).
|
- playwright-cli interactive check confirms the guard + nav gating + sheet open (supplementary, recorded in SUMMARY).
|
||||||
- Run the full CI fast-checks gate locally before declaring done (lint + typecheck + test + format:check + md:lint + PWA tests) per [[feedback-run-full-ci-gate-before-push]].
|
- Run the full CI fast-checks gate locally before declaring done (lint + typecheck + test + format:check + md:lint + PWA tests) per [[feedback-run-full-ci-gate-before-push]].
|
||||||
</verification>
|
</verification>
|
||||||
|
|
||||||
<success_criteria>
|
<success_criteria>
|
||||||
- Admin sees the Admin section, lists members + credential status, rotates a credential via the sheet, picks the shared calendar (Success Criteria 1, 2, 3).
|
- Admin sees the Admin section, lists members + credential status, rotates a credential via the sheet, picks the shared calendar (Success Criteria 1, 2, 3).
|
||||||
- Non-admin never sees the entry and is redirected from /admin (Success Criterion 1; client-side UX over the server 403).
|
- Non-admin never sees the entry and is redirected from /admin (Success Criterion 1; client-side UX over the server 403).
|
||||||
- needsProviderSetup member sees the SetupBanner and can self-serve their own credential (D-07).
|
- needsProviderSetup member sees the SetupBanner and can self-serve their own credential; the banner clears on a successful save (no dismiss button) (D-07).
|
||||||
</success_criteria>
|
</success_criteria>
|
||||||
|
|
||||||
<output>
|
<output>
|
||||||
Create `.planning/phases/10-admin-role-settings/10-04-SUMMARY.md` when done.
|
Create `.planning/phases/10-admin-role-settings/10-04-SUMMARY.md` when done.
|
||||||
</output>
|
</output>
|
||||||
|
</content>
|
||||||
|
</invoke>
|
||||||
|
|||||||
@@ -0,0 +1,710 @@
|
|||||||
|
# Phase 10: Admin Role & Settings - Pattern Map
|
||||||
|
|
||||||
|
**Mapped:** 2026-06-13
|
||||||
|
**Files analyzed:** 14 new/modified files
|
||||||
|
**Analogs found:** 13 / 14
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Classification
|
||||||
|
|
||||||
|
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `apps/api/src/db/schema.ts` | model | CRUD | self (existing schema.ts) | exact — extend in place |
|
||||||
|
| `apps/api/src/db/migrations/0001_v1_1_foundation.sql` | config | batch | `0000_baseline.sql` | exact |
|
||||||
|
| `apps/api/src/routes/admin.ts` (NEW) | controller | request-response | `apps/api/src/routes/push.ts` | role-match |
|
||||||
|
| `apps/api/src/lib/requireAdmin.ts` (NEW) | middleware | request-response | `apps/api/src/auth/devBypass.ts` | role-match |
|
||||||
|
| `apps/api/src/index.ts` | config | request-response | self (existing index.ts) | exact — extend in place |
|
||||||
|
| `apps/api/src/routes/me.ts` | controller | request-response | self (existing me.ts) | exact — extend in place |
|
||||||
|
| `apps/api/src/auth/user.ts` | service | CRUD | self (existing user.ts) | exact — extend in place |
|
||||||
|
| `apps/api/src/broker/crypto.ts` | utility | transform | — | reuse only, no changes |
|
||||||
|
| `apps/api/src/broker/client.ts` | utility | request-response | — | reuse only, no changes |
|
||||||
|
| `apps/api/src/broker/outboxWorker.ts` | service | event-driven | — | reuse `loadClientForUser` / `triggerTargetedResync` (promote to export) |
|
||||||
|
| `apps/pwa/src/App.tsx` | component | request-response | self (existing App.tsx) | exact — extend in place |
|
||||||
|
| `apps/pwa/src/api/client.ts` | utility | request-response | self (existing client.ts) | exact — extend in place |
|
||||||
|
| `apps/pwa/src/routes/AdminPage.tsx` (NEW) | component | request-response | `apps/pwa/src/routes/ListsIndex.tsx` | role-match |
|
||||||
|
| `apps/pwa/src/components/CredentialSheet.tsx` (NEW) | component | request-response | `apps/pwa/src/components/SettingsSheet.tsx` | exact |
|
||||||
|
| `apps/pwa/src/components/SetupBanner.tsx` (NEW) | component | event-driven | `apps/pwa/src/components/PermissionDeniedBanner.tsx` | role-match |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pattern Assignments
|
||||||
|
|
||||||
|
### `apps/api/src/db/schema.ts` — add columns + new table
|
||||||
|
|
||||||
|
**Analog:** self — extend in place.
|
||||||
|
|
||||||
|
**Existing import pattern** (lines 1–14):
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
mysqlTable,
|
||||||
|
mysqlEnum,
|
||||||
|
varchar,
|
||||||
|
text,
|
||||||
|
int,
|
||||||
|
date,
|
||||||
|
timestamp,
|
||||||
|
boolean,
|
||||||
|
index,
|
||||||
|
unique,
|
||||||
|
customType,
|
||||||
|
} from 'drizzle-orm/mysql-core';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Existing column patterns to copy for new columns:**
|
||||||
|
|
||||||
|
`boolean` with NOT NULL DEFAULT false — copy from `calendarEvents.allDay` (line 127):
|
||||||
|
```typescript
|
||||||
|
allDay: boolean('all_day').default(false).notNull(),
|
||||||
|
```
|
||||||
|
|
||||||
|
`varchar` with length + notNull + default — copy from `memberCredentials.fastmailEmail` (line 64):
|
||||||
|
```typescript
|
||||||
|
fastmailEmail: varchar('fastmail_email', { length: 256 }).notNull(),
|
||||||
|
```
|
||||||
|
|
||||||
|
`int` nullable — copy from `calendarEvents.dtstartUtc` (line 125) but use `int`:
|
||||||
|
```typescript
|
||||||
|
dtstartUtc: timestamp('dtstart_utc'), // nullable = no .notNull()
|
||||||
|
```
|
||||||
|
|
||||||
|
**New `app_config` table — follow `pushSubscriptions` single-table pattern** (lines 236–257):
|
||||||
|
```typescript
|
||||||
|
export const pushSubscriptions = mysqlTable(
|
||||||
|
'push_subscriptions',
|
||||||
|
{
|
||||||
|
id: int().primaryKey().autoincrement(),
|
||||||
|
userId: int('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||||
|
endpoint: varchar('endpoint', { length: 2048 }).notNull(),
|
||||||
|
...
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
unique('uniq_push_endpoint').on(t.endpoint),
|
||||||
|
index('idx_push_subscriptions_user_id').on(t.userId),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**`memberCredentials` `unique` constraint pattern** — copy from `calendars` (lines 91–100):
|
||||||
|
```typescript
|
||||||
|
unique('uniq_calendar_user_url').on(t.userId, t.url),
|
||||||
|
```
|
||||||
|
Apply as `unique('uniq_member_credential_user').on(t.userId)` to enforce one-credential-per-member and enable `onDuplicateKeyUpdate`.
|
||||||
|
|
||||||
|
**Changes to make:**
|
||||||
|
1. `users` table: add `isAdmin: boolean('is_admin').default(false).notNull()`
|
||||||
|
2. `memberCredentials` table: add `providerType: varchar('provider_type', { length: 64 }).notNull().default('caldav')` + add `unique('uniq_member_credential_user').on(t.userId)` to the index array
|
||||||
|
3. `calendarEvents` table: add `reminderLeadMinutes: int('reminder_lead_minutes')` (nullable — no `.notNull()`)
|
||||||
|
4. New `appConfig` table: `key VARCHAR PK, value TEXT, updatedAt timestamp`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `apps/api/src/db/migrations/0001_v1_1_foundation.sql` (NEW, generated)
|
||||||
|
|
||||||
|
**Analog:** `apps/api/src/db/migrations/0000_baseline.sql` lines 1–16.
|
||||||
|
|
||||||
|
**Migration file format** — each DDL statement separated by `--> statement-breakpoint`:
|
||||||
|
```sql
|
||||||
|
ALTER TABLE `users` ADD COLUMN `is_admin` boolean NOT NULL DEFAULT false;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `member_credentials` ADD COLUMN `provider_type` varchar(64) NOT NULL DEFAULT 'caldav';
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `member_credentials` ADD UNIQUE `uniq_member_credential_user`(`user_id`);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `calendar_events` ADD COLUMN `reminder_lead_minutes` int;
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `app_config` ( ... );
|
||||||
|
```
|
||||||
|
|
||||||
|
**Do not hand-write.** Run `pnpm --filter @familysync/api db:generate` after editing schema.ts; the file is generated automatically. Commit the output.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `apps/api/src/routes/admin.ts` (NEW) — admin sub-router
|
||||||
|
|
||||||
|
**Analog:** `apps/api/src/routes/push.ts` (closest: Hono sub-router + zValidator + resolveUserId pattern)
|
||||||
|
|
||||||
|
**Imports pattern** — copy from `push.ts` lines 15–26, substitute admin-specific imports:
|
||||||
|
```typescript
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
import type { Context, MiddlewareHandler } from 'hono';
|
||||||
|
import { zValidator } from '@hono/zod-validator';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { eq, and } from 'drizzle-orm';
|
||||||
|
import { db } from '../db/client.js';
|
||||||
|
import { users, memberCredentials, calendars } from '../db/schema.js';
|
||||||
|
import { encryptPassword } from '../broker/crypto.js';
|
||||||
|
import { createFastmailClient } from '../broker/client.js';
|
||||||
|
import { requireAdmin } from '../lib/requireAdmin.js';
|
||||||
|
// Side-effect import for ContextVariableMap augmentation
|
||||||
|
import '../auth/devBypass.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Router + guard pattern** (Pitfall 9 — guard FIRST inside the sub-router):
|
||||||
|
```typescript
|
||||||
|
export const adminRouter = new Hono();
|
||||||
|
adminRouter.use('*', requireAdmin); // ← MUST be first; guards every sub-route
|
||||||
|
```
|
||||||
|
|
||||||
|
**zValidator with no-echo hook** (Pitfall 7) — adapt from `push.ts` lines 60–67 (subscribeSchema):
|
||||||
|
```typescript
|
||||||
|
const credentialSchema = z.object({
|
||||||
|
userId: z.number().int().positive(),
|
||||||
|
providerType: z.literal('caldav'),
|
||||||
|
fastmailEmail: z.string().email().max(256),
|
||||||
|
appPassword: z.string().min(1).max(500),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hook MUST never echo Zod issues (which contain .received = the password value)
|
||||||
|
const noEchoHook = (result: { success: boolean }, c: Context) => {
|
||||||
|
if (!result.success) return c.json({ error: 'Invalid request' }, 400);
|
||||||
|
};
|
||||||
|
|
||||||
|
adminRouter.post(
|
||||||
|
'/credentials',
|
||||||
|
zValidator('json', credentialSchema, noEchoHook),
|
||||||
|
async (c) => {
|
||||||
|
const { userId, fastmailEmail, appPassword } = c.req.valid('json');
|
||||||
|
// NEVER log appPassword or c.req.valid('json')
|
||||||
|
// validate → encrypt → upsert → trigger-sync
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Drizzle SELECT pattern** — copy from `events.ts` lines 165–177 (join + where):
|
||||||
|
```typescript
|
||||||
|
const rows = await db
|
||||||
|
.select({ ... })
|
||||||
|
.from(users)
|
||||||
|
.leftJoin(memberCredentials, eq(memberCredentials.userId, users.id))
|
||||||
|
.where(/* ... */);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Drizzle upsert pattern** — copy from `events.ts` `onDuplicateKeyUpdate` usage (found in outboxWorker):
|
||||||
|
```typescript
|
||||||
|
await db.insert(memberCredentials)
|
||||||
|
.values({ userId, encryptedPassword: encrypted, fastmailEmail, providerType: 'caldav' })
|
||||||
|
.onDuplicateKeyUpdate({ set: { encryptedPassword: encrypted, fastmailEmail, providerType: 'caldav' } });
|
||||||
|
// Requires UNIQUE(user_id) added by v1.1 migration
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exclusive is_shared update** — two sequential Drizzle UPDATEs (RESEARCH.md Pattern 7):
|
||||||
|
```typescript
|
||||||
|
await db.update(calendars).set({ isShared: false }).where(eq(calendars.isShared, true));
|
||||||
|
await db.update(calendars).set({ isShared: true }).where(eq(calendars.id, targetId));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `apps/api/src/lib/requireAdmin.ts` (NEW) — role middleware
|
||||||
|
|
||||||
|
**Analog:** `apps/api/src/auth/devBypass.ts` (MiddlewareHandler pattern)
|
||||||
|
|
||||||
|
**Import + type pattern** (devBypass.ts lines 27–28):
|
||||||
|
```typescript
|
||||||
|
import type { MiddlewareHandler } from 'hono';
|
||||||
|
```
|
||||||
|
|
||||||
|
**MiddlewareHandler signature** (devBypass.ts lines 58–76):
|
||||||
|
```typescript
|
||||||
|
export function devAuthBypass(): MiddlewareHandler {
|
||||||
|
return async (c, next) => {
|
||||||
|
c.set('user', DEV_USER);
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**requireAdmin must be an inline `MiddlewareHandler`**, not a factory function (applied as `.use('*', requireAdmin)`):
|
||||||
|
```typescript
|
||||||
|
import type { MiddlewareHandler } from 'hono';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db } from '../db/client.js';
|
||||||
|
import { users } from '../db/schema.js';
|
||||||
|
|
||||||
|
export const requireAdmin: MiddlewareHandler = async (c, next) => {
|
||||||
|
const devUser = c.get('user') as { id: number } | undefined;
|
||||||
|
const userId = devUser?.id;
|
||||||
|
if (!userId) return c.json({ error: 'Forbidden' }, 403);
|
||||||
|
|
||||||
|
// Always look up is_admin from DB — bypass only skips OIDC, not the DB check
|
||||||
|
const [row] = await db
|
||||||
|
.select({ isAdmin: users.isAdmin })
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.id, userId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!row?.isAdmin) return c.json({ error: 'Forbidden' }, 403);
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**ContextVariableMap augmentation** — include side-effect import from devBypass.ts (line 39):
|
||||||
|
```typescript
|
||||||
|
import '../auth/devBypass.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `apps/api/src/index.ts` — mount adminRouter
|
||||||
|
|
||||||
|
**Analog:** self — extend in place.
|
||||||
|
|
||||||
|
**Existing route mounting pattern** (lines 67–72):
|
||||||
|
```typescript
|
||||||
|
app.route('/api/me', meRouter);
|
||||||
|
app.route('/api/events', eventsRouter);
|
||||||
|
app.route('/api/lists', listsRouter);
|
||||||
|
app.route('/api/list-items', listItemsRouter);
|
||||||
|
app.route('/api/push', pushRouter);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add after the existing route block** (same style, behind the existing devAuthBypass → oidcAuthMiddleware band already covering `/api/*`):
|
||||||
|
```typescript
|
||||||
|
import { adminRouter } from './routes/admin.js';
|
||||||
|
// ...
|
||||||
|
app.route('/api/admin', adminRouter);
|
||||||
|
```
|
||||||
|
|
||||||
|
No additional middleware needed at the `app` level — `requireAdmin` is applied inside `adminRouter` itself (Pitfall 9).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `apps/api/src/routes/me.ts` — add isAdmin + needsProviderSetup
|
||||||
|
|
||||||
|
**Analog:** self — extend in place.
|
||||||
|
|
||||||
|
**Current response shape** (lines 30–43 dev-bypass path, lines 66–73 OIDC path):
|
||||||
|
```typescript
|
||||||
|
return c.json({
|
||||||
|
user: {
|
||||||
|
id: devUser.id,
|
||||||
|
displayName: devUser.displayName,
|
||||||
|
color: devUser.color,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pattern:** Both paths (dev-bypass + OIDC) must add `isAdmin` and `needsProviderSetup`. The dev-bypass path currently short-circuits WITHOUT a DB lookup — for `isAdmin` it MUST query the DB for user id 1 (same as requireAdmin). `needsProviderSetup` requires a COUNT/EXISTS on `memberCredentials` for the current user id.
|
||||||
|
|
||||||
|
**DB import additions needed:**
|
||||||
|
```typescript
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db } from '../db/client.js';
|
||||||
|
import { users, memberCredentials } from '../db/schema.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
**needsProviderSetup lookup pattern** — copy Drizzle `.select().from().where().limit(1)` pattern from user.ts lines 79–82:
|
||||||
|
```typescript
|
||||||
|
const [cred] = await db
|
||||||
|
.select({ id: memberCredentials.id })
|
||||||
|
.from(memberCredentials)
|
||||||
|
.where(eq(memberCredentials.userId, userId))
|
||||||
|
.limit(1);
|
||||||
|
const needsProviderSetup = !cred;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `apps/api/src/auth/user.ts` — first-login-wins is_admin bootstrap
|
||||||
|
|
||||||
|
**Analog:** self — extend in place.
|
||||||
|
|
||||||
|
**Insert block** (lines 112–122) — add `isAdmin` to the `.values({...})` call:
|
||||||
|
```typescript
|
||||||
|
// Before INSERT: check if zero admins exist (first-login-wins, D-01)
|
||||||
|
const [{ count }] = await db
|
||||||
|
.select({ count: sql<number>`COUNT(*)` })
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.isAdmin, true));
|
||||||
|
const shouldBeAdmin = Number(count) === 0;
|
||||||
|
|
||||||
|
const [inserted] = await db
|
||||||
|
.insert(users)
|
||||||
|
.values({
|
||||||
|
oidcIss,
|
||||||
|
oidcSub,
|
||||||
|
displayName: displayName ?? null,
|
||||||
|
color,
|
||||||
|
isAdmin: shouldBeAdmin, // ← new
|
||||||
|
})
|
||||||
|
.$returningId();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Import additions needed:**
|
||||||
|
```typescript
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `apps/api/src/broker/outboxWorker.ts` — promote triggerTargetedResync
|
||||||
|
|
||||||
|
**Analog:** self — promote private function to export.
|
||||||
|
|
||||||
|
**Current private function signature** (lines 302–348):
|
||||||
|
```typescript
|
||||||
|
async function triggerTargetedResync(
|
||||||
|
calendarUrl: string,
|
||||||
|
userId: number,
|
||||||
|
clientCache?: Map<number, FastmailClient>,
|
||||||
|
): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Change:** add `export` keyword. Admin routes (and member self-service) will import and call it after credential upsert.
|
||||||
|
|
||||||
|
**Also export `loadClientForUser`** (lines 271–288) — needed for the initial full per-member sync (no known `calendarUrl` after first credential save):
|
||||||
|
```typescript
|
||||||
|
export async function loadClientForUser(userId: number): Promise<FastmailClient>
|
||||||
|
```
|
||||||
|
|
||||||
|
For the post-credential-save full sync (no specific `calendarUrl`), the admin route calls `loadClientForUser`, then `client.fetchCalendars()`, iterates each `davCal`, and calls `syncCalendar` for each — mirroring what the poller does per member.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `apps/pwa/src/App.tsx` — add /admin route
|
||||||
|
|
||||||
|
**Analog:** self — extend in place.
|
||||||
|
|
||||||
|
**Existing Routes block** (lines 121–127):
|
||||||
|
```typescript
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Navigate to="/calendar" replace />} />
|
||||||
|
<Route path="/calendar" element={<CalendarShell />} />
|
||||||
|
<Route path="/lists" element={<ListsIndex />} />
|
||||||
|
<Route path="/lists/:listId" element={<ListDetail />} />
|
||||||
|
</Routes>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add `/admin` route** with inline redirect guard:
|
||||||
|
```typescript
|
||||||
|
import { AdminPage } from './routes/AdminPage.js';
|
||||||
|
// ...
|
||||||
|
<Route
|
||||||
|
path="/admin"
|
||||||
|
element={
|
||||||
|
meQuery.data?.user.isAdmin
|
||||||
|
? <AdminPage />
|
||||||
|
: <Navigate to="/calendar" replace />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**meQuery consumption pattern** (lines 63–68):
|
||||||
|
```typescript
|
||||||
|
const meQuery = useQuery({
|
||||||
|
queryKey: ['me'],
|
||||||
|
queryFn: fetchMe,
|
||||||
|
retry: false,
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
The `isAdmin` guard on the route uses `meQuery.data?.user.isAdmin` — while `meQuery` is loading, `isAdmin` is `undefined` (falsy), so the route redirects. Add a loading gate if flash-of-redirect is a concern (planner's call).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `apps/pwa/src/api/client.ts` — add isAdmin + needsProviderSetup to MeUser
|
||||||
|
|
||||||
|
**Analog:** self — extend in place.
|
||||||
|
|
||||||
|
**Current MeUser interface** (lines 62–66):
|
||||||
|
```typescript
|
||||||
|
export interface MeUser {
|
||||||
|
id: number;
|
||||||
|
displayName: string | null;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add fields:**
|
||||||
|
```typescript
|
||||||
|
export interface MeUser {
|
||||||
|
id: number;
|
||||||
|
displayName: string | null;
|
||||||
|
color: string;
|
||||||
|
isAdmin: boolean; // from users.is_admin
|
||||||
|
needsProviderSetup: boolean; // true when no member_credentials row exists
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**API fetch functions pattern for admin routes** — copy from `createEvent` (lines 221–233):
|
||||||
|
```typescript
|
||||||
|
export async function createEvent(payload: CreateEventPayload): Promise<CreateEventResponse> {
|
||||||
|
const res = await fetch('/api/events/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
redirect: 'manual',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
handleAuthResponse(res, 'POST /api/events/create');
|
||||||
|
return res.json() as Promise<CreateEventResponse>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Apply same pattern for `fetchAdminMembers`, `saveCredential`, `fetchAdminCalendars`, `setSharedCalendar`, `saveMyCredential`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `apps/pwa/src/routes/AdminPage.tsx` (NEW) — /admin page shell
|
||||||
|
|
||||||
|
**Analog:** `apps/pwa/src/routes/ListsIndex.tsx` (page-level component with TanStack Query + sections)
|
||||||
|
|
||||||
|
**Page structure pattern** — copy AppNav/content layout from `App.tsx` content area style (lines 99–102):
|
||||||
|
```typescript
|
||||||
|
const contentStyle: React.CSSProperties = {
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
minHeight: 0,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**TanStack Query fetch pattern** — copy from App.tsx meQuery (lines 63–68); admin page will add its own queries for members and calendars:
|
||||||
|
```typescript
|
||||||
|
const membersQuery = useQuery({
|
||||||
|
queryKey: ['admin', 'members'],
|
||||||
|
queryFn: fetchAdminMembers,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Section label style** — per UI-SPEC, copy the pattern from `SettingsSheet.tsx` section headers:
|
||||||
|
```typescript
|
||||||
|
// 13px / weight 600 / var(--color-text-muted) / uppercase / letterSpacing 0.06em
|
||||||
|
{
|
||||||
|
fontSize: 'var(--text-label-size)',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--color-text-muted)',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.06em',
|
||||||
|
marginBottom: 'var(--space-2)',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `apps/pwa/src/components/CredentialSheet.tsx` (NEW) — credential bottom sheet
|
||||||
|
|
||||||
|
**Analog:** `apps/pwa/src/components/SettingsSheet.tsx` (closest exact match: bottom sheet pattern, role="dialog", Escape key, focus management)
|
||||||
|
|
||||||
|
**Bottom sheet structural pattern** (SettingsSheet.tsx lines 52–76):
|
||||||
|
```typescript
|
||||||
|
export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) {
|
||||||
|
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') onClose();
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', onKeyDown);
|
||||||
|
return () => document.removeEventListener('keydown', onKeyDown);
|
||||||
|
}, [isOpen, onClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && closeButtonRef.current) {
|
||||||
|
closeButtonRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sheet container style** (apply zIndex 301, backdrop 300, borderRadius 12px 12px 0 0 — matching SettingsSheet):
|
||||||
|
```typescript
|
||||||
|
// Backdrop
|
||||||
|
{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', zIndex: 300 }
|
||||||
|
// Sheet
|
||||||
|
{ position: 'fixed', bottom: 0, left: 0, right: 0, background: 'var(--color-surface)',
|
||||||
|
borderRadius: '12px 12px 0 0', padding: 'var(--space-6)', zIndex: 301 }
|
||||||
|
```
|
||||||
|
|
||||||
|
**ARIA pattern:**
|
||||||
|
```tsx
|
||||||
|
<div role="dialog" aria-modal="true" aria-label="Rotate Credential">
|
||||||
|
```
|
||||||
|
|
||||||
|
**Password input pattern** (UI-SPEC — never pre-filled, `type="password"`, `autocomplete="new-password"`):
|
||||||
|
```tsx
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
style={{ /* ... full-width, border, borderRadius, fontSize */ }}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**TanStack Query mutation pattern** — copy from PWA list mutation (useMutation with onSuccess invalidation):
|
||||||
|
```typescript
|
||||||
|
const credentialMutation = useMutation({
|
||||||
|
mutationFn: saveCredential,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['admin', 'members'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['me'] }); // needsProviderSetup refresh
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `apps/pwa/src/components/SetupBanner.tsx` (NEW) — needsProviderSetup banner
|
||||||
|
|
||||||
|
**Analog:** `apps/pwa/src/components/PermissionDeniedBanner.tsx` (conditional banner rendered from App.tsx level)
|
||||||
|
|
||||||
|
**Pattern:** renders only when `meQuery.data?.user.needsProviderSetup === true`. No dismiss button per UI-SPEC — disappears when `needsProviderSetup` becomes false after save.
|
||||||
|
|
||||||
|
**Banner style** (UI-SPEC Surface 4):
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
background: 'var(--color-surface-dim)',
|
||||||
|
border: '1px solid var(--color-border)',
|
||||||
|
borderRadius: 'var(--space-2)',
|
||||||
|
padding: 'var(--space-4)',
|
||||||
|
margin: 'var(--space-4)',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`role="status"` for live announcement:**
|
||||||
|
```tsx
|
||||||
|
<div role="status" aria-live="polite">
|
||||||
|
{/* KeyRound icon + heading + body + CTA */}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Shared Patterns
|
||||||
|
|
||||||
|
### resolveUserId — auth helper per router
|
||||||
|
|
||||||
|
**Source:** `apps/api/src/routes/push.ts` lines 37–49 (canonical copy in use across push, events, lists routers)
|
||||||
|
|
||||||
|
**Apply to:** `apps/api/src/routes/admin.ts` (member self-service endpoint on `/api/me/credential` added to meRouter)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function resolveUserId(c: Context): Promise<number | null> {
|
||||||
|
const devUser = c.get('user') as { id: number } | undefined;
|
||||||
|
if (devUser) return devUser.id;
|
||||||
|
|
||||||
|
const auth = await getAuth(c);
|
||||||
|
if (!auth) return null;
|
||||||
|
|
||||||
|
const iss = (auth.iss as string | undefined) ?? '';
|
||||||
|
const sub = auth.sub ?? '';
|
||||||
|
const displayName = deriveDisplayName(auth);
|
||||||
|
const user = await upsertUser(iss, sub, displayName);
|
||||||
|
return user?.id ?? null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hono sub-router mounting
|
||||||
|
|
||||||
|
**Source:** `apps/api/src/index.ts` lines 67–72
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
app.route('/api/admin', adminRouter);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Apply to:** index.ts — adminRouter added to the existing route block (after the auth guards already cover `/api/*`).
|
||||||
|
|
||||||
|
### Zod + zValidator (no hook = safe for non-credential fields)
|
||||||
|
|
||||||
|
**Source:** `apps/api/src/routes/events.ts` lines 27–28 + 137
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { zValidator } from '@hono/zod-validator';
|
||||||
|
// Usage:
|
||||||
|
eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => { ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
**Apply to:** non-credential admin routes (GET /members, GET /calendars, PUT /calendars/:id/shared).
|
||||||
|
|
||||||
|
**For credential routes only — add the no-echo hook** (RESEARCH.md Pattern 2). Never return `result.error` directly for any route that accepts `appPassword`.
|
||||||
|
|
||||||
|
### ContextVariableMap side-effect import
|
||||||
|
|
||||||
|
**Source:** every route file (push.ts line 25, events.ts line 39, me.ts line 26)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import '../auth/devBypass.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Apply to:** `apps/api/src/routes/admin.ts` and `apps/api/src/lib/requireAdmin.ts`.
|
||||||
|
|
||||||
|
### NavLink + Lucide icon (nav entry)
|
||||||
|
|
||||||
|
**Source:** `apps/pwa/src/components/AppNav.tsx` lines 14–15 + `BottomTabBar.tsx` lines 76–98
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { NavLink } from 'react-router';
|
||||||
|
import { CalendarDays, List } from 'lucide-react';
|
||||||
|
// NavLink usage:
|
||||||
|
<NavLink to="/calendar" aria-label="Calendar" style={({ isActive }) => ({
|
||||||
|
...tabBase, ...(isActive ? tabActiveOverride : {}),
|
||||||
|
})}>
|
||||||
|
<CalendarDays size={22} aria-hidden="true" />
|
||||||
|
<span>Calendar</span>
|
||||||
|
</NavLink>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Apply to:** `AppNav.tsx` (DesktopNav section) and `BottomTabBar.tsx` — add Admin entry with `ShieldCheck` icon (size 18/22), conditional on `isAdmin === true`.
|
||||||
|
|
||||||
|
### CSS token inline style pattern
|
||||||
|
|
||||||
|
**Source:** `apps/pwa/src/components/BottomTabBar.tsx` lines 27–45
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const tabBase: React.CSSProperties = {
|
||||||
|
fontSize: 'var(--text-label-size, 13px)',
|
||||||
|
color: 'var(--color-text-muted)',
|
||||||
|
fontFamily: 'var(--font-family-base)',
|
||||||
|
minHeight: '44px',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Apply to:** all new PWA components (AdminPage, CredentialSheet, SetupBanner). No hard-coded px except the 44px touch-target minimum. All color/typography/spacing references through `var(--token)`.
|
||||||
|
|
||||||
|
### `handleAuthResponse` + `redirect: 'manual'` in fetch
|
||||||
|
|
||||||
|
**Source:** `apps/pwa/src/api/client.ts` lines 51–58 + 80–83
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function handleAuthResponse(res: Response, label: string): void {
|
||||||
|
if (res.type === 'opaqueredirect' || res.status === 401) throw new SessionExpiredError();
|
||||||
|
if (!res.ok) throw new Error(`${label} failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
// Usage:
|
||||||
|
const res = await fetch('/api/admin/members', { credentials: 'include', redirect: 'manual' });
|
||||||
|
handleAuthResponse(res, 'GET /api/admin/members');
|
||||||
|
```
|
||||||
|
|
||||||
|
**Apply to:** all new `client.ts` fetch functions for admin and me/credential endpoints.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## No Analog Found
|
||||||
|
|
||||||
|
| File | Role | Data Flow | Reason |
|
||||||
|
|---|---|---|---|
|
||||||
|
| (none) | — | — | All files have close analogs in the existing codebase |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Analog Search Scope
|
||||||
|
|
||||||
|
- `apps/api/src/routes/` — all route files
|
||||||
|
- `apps/api/src/auth/` — devBypass.ts, user.ts, middleware.ts
|
||||||
|
- `apps/api/src/broker/` — crypto.ts, client.ts, outboxWorker.ts
|
||||||
|
- `apps/api/src/db/` — schema.ts, migrations/
|
||||||
|
- `apps/pwa/src/` — App.tsx, api/client.ts, components/, routes/
|
||||||
|
|
||||||
|
**Files scanned:** 15 source files read directly.
|
||||||
|
|
||||||
|
**Pattern extraction date:** 2026-06-13
|
||||||
@@ -44,17 +44,17 @@ Notes:
|
|||||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||||
| 10-01-01 | 01 | 1 | ADMIN-01/02/03 | T-10-01 | schema additive only; no destructive DDL | typecheck | `pnpm --filter @familysync/api exec tsc --noEmit` | ✅ existing | ⬜ pending |
|
| 10-01-01 | 01 | 1 | ADMIN-01/02/03 | T-10-01 | schema additive only; no destructive DDL | typecheck | `pnpm --filter @familysync/api exec tsc --noEmit` | ✅ existing | ⬜ pending |
|
||||||
| 10-01-02 | 01 | 1 | ADMIN-01/02/03 | T-10-01 | generate+migrate (never push); live DB columns verified by query | integration (DB) | mysql2 `SHOW COLUMNS`/`SHOW TABLES` assert (see plan verify) | ✅ existing | ⬜ pending |
|
| 10-01-02 | 01 | 1 | ADMIN-01/02/03 | T-10-01 | db:generate + db:migrate actually RUN (never push); migration SQL additive-only; live DB columns verified by mysql2 query (tsc NOT proof) | integration (DB) | mysql2 `SHOW COLUMNS`/`SHOW TABLES` assert (see plan verify) | ✅ existing | ⬜ pending |
|
||||||
| 10-01-03 | 01 | 1 | ADMIN-03 | T-10-03 | dev-bypass admin row scoped to guarded dev/e2e DB only | source | `grep -c is_admin apps/pwa/e2e/global-setup.ts` | ✅ existing | ⬜ pending |
|
| 10-01-03 | 01 | 1 | ADMIN-03 | T-10-03 | dev-bypass admin row scoped to guarded dev/e2e DB only | source | `grep -c is_admin apps/pwa/e2e/global-setup.ts` | ✅ existing | ⬜ pending |
|
||||||
| 10-02-01 | 02 | 2 | ADMIN-03 | T-10-04/05/06/07 | requireAdmin 403s non-admin; role from DB not client flag | unit | `pnpm --filter @familysync/api test -- requireAdmin` | ❌ W0 → `apps/api/tests/lib/requireAdmin.test.ts` | ⬜ pending |
|
| 10-02-01 | 02 | 2 | ADMIN-03 | T-10-04/05/06/07 | requireAdmin 403s non-admin; role from DB not client flag | unit | `pnpm --filter @familysync/api test -- requireAdmin` | ❌ W0 → `apps/api/tests/lib/requireAdmin.test.ts` | ⬜ pending |
|
||||||
| 10-02-02 | 02 | 2 | ADMIN-03 | T-10-04 | first-login-wins is_admin; member-count-agnostic | integration (DB) | `pnpm --filter @familysync/api test -- user` | ✅ `apps/api/tests/auth/user.test.ts` (extend) | ⬜ pending |
|
| 10-02-02 | 02 | 2 | ADMIN-03 | T-10-04 | first-login-wins is_admin; member-count-agnostic | integration (DB) | `pnpm --filter @familysync/api test -- user` | ✅ `apps/api/tests/auth/user.test.ts` (extend) | ⬜ pending |
|
||||||
| 10-02-03 | 02 | 2 | ADMIN-03 | T-10-06/07 | /api/me exposes isAdmin + needsProviderSetup (UX-only flag) | integration | `pnpm --filter @familysync/api test -- me` | ✅ `apps/api/tests/routes/me.test.ts` (extend) | ⬜ pending |
|
| 10-02-03 | 02 | 2 | ADMIN-03 | T-10-06/07 | /api/me exposes isAdmin + needsProviderSetup (UX-only flag) | integration | `pnpm --filter @familysync/api test -- me` | ✅ `apps/api/tests/routes/me.test.ts` (extend) | ⬜ pending |
|
||||||
| 10-03-01 | 03 | 3 | ADMIN-01 | T-10-SC | broker helpers exported; bodies unchanged | unit | `pnpm --filter @familysync/api test -- outbox` | ✅ `apps/api/tests/broker/` | ⬜ pending |
|
| 10-03-01 | 03 | 3 | ADMIN-01 | T-10-SC | broker helpers exported; bodies unchanged | unit | `pnpm --filter @familysync/api test -- outbox` | ✅ `apps/api/tests/broker/` | ⬜ pending |
|
||||||
| 10-03-02 | 03 | 3 | ADMIN-01/02/03 | T-10-08/09/10/11/13 | guard-first 403; no password echo/log; encrypted at rest; exclusive is_shared | integration | `pnpm --filter @familysync/api test -- admin` | ❌ W0 → `apps/api/tests/routes/admin.test.ts` | ⬜ pending |
|
| 10-03-02 | 03 | 3 | ADMIN-01/02/03 | T-10-08/09/10/11/13 | guard-first 403; ALL credential-validation failures → one generic `{error:'Invalid request'}` 400 (no password echo/log); shared credentialSync helper; encrypted at rest; exclusive is_shared | integration | `pnpm --filter @familysync/api test -- admin` | ❌ W0 → `apps/api/tests/routes/admin.test.ts` | ⬜ pending |
|
||||||
| 10-03-03 | 03 | 3 | ADMIN-01 | T-10-09/10/12 | self-service member-scoped; no cross-member write; no echo | integration | `pnpm --filter @familysync/api test -- credential` | ❌ W0 → in `apps/api/tests/routes/admin.test.ts` | ⬜ pending |
|
| 10-03-03 | 03 | 3 | ADMIN-01 | T-10-09/10/12 | self-service member-scoped; no cross-member write; calls SAME shared validateEncryptAndStoreCredential helper; no echo | integration | `pnpm --filter @familysync/api test -- credential` | ❌ W0 → in `apps/api/tests/routes/admin.test.ts` | ⬜ pending |
|
||||||
| 10-04-01 | 04 | 4 | ADMIN-01/02/03 | T-10-14 | MeUser flags UX-only; self-service payload has no userId | typecheck | `pnpm --filter @familysync/pwa exec tsc --noEmit` | ✅ existing | ⬜ pending |
|
| 10-04-01 | 04 | 4 | ADMIN-01/02/03 | T-10-14 | MeUser flags UX-only; self-service payload has no userId | typecheck | `pnpm --filter @familysync/pwa exec tsc --noEmit` | ✅ existing | ⬜ pending |
|
||||||
| 10-04-02 | 04 | 4 | ADMIN-01 | T-10-15/16 | password never pre-filled; autocomplete=new-password | typecheck + source | `pnpm --filter @familysync/pwa exec tsc --noEmit` + grep new-password | ✅ existing | ⬜ pending |
|
| 10-04-02 | 04 | 4 | ADMIN-01 | T-10-15/16 | password never pre-filled; autocomplete=new-password; SetupBanner clears ONLY on success (['me'] invalidation), no dismiss button | typecheck + source | `pnpm --filter @familysync/pwa exec tsc --noEmit` + grep new-password | ✅ existing | ⬜ pending |
|
||||||
| 10-04-03 | 04 | 4 | ADMIN-03 | T-10-14 | /admin redirect for non-admin; nav entry hidden for non-admin | e2e (playwright) | `pnpm --filter @familysync/pwa test:e2e -- admin` | ❌ W0 → `apps/pwa/e2e/admin.spec.ts` | ⬜ pending |
|
| 10-04-03 | 04 | 4 | ADMIN-03 | T-10-14 | /admin redirect for non-admin; nav entry hidden for non-admin; e2e is the gate, playwright-cli supplementary | e2e (playwright) | `pnpm --filter @familysync/pwa test:e2e -- admin` | ❌ W0 → `apps/pwa/e2e/admin.spec.ts` | ⬜ pending |
|
||||||
|
|
||||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||||
|
|
||||||
@@ -65,8 +65,8 @@ Sampling continuity: every task has an `<automated>` verify; no 3 consecutive ta
|
|||||||
## Wave 0 Requirements
|
## Wave 0 Requirements
|
||||||
|
|
||||||
- [ ] `apps/api/tests/lib/requireAdmin.test.ts` — NEW unit tests for the guard (403 non-admin / next() admin / no-user / DB-not-client-flag). Created in Plan 02 Task 1 (RED first).
|
- [ ] `apps/api/tests/lib/requireAdmin.test.ts` — NEW unit tests for the guard (403 non-admin / next() admin / no-user / DB-not-client-flag). Created in Plan 02 Task 1 (RED first).
|
||||||
- [ ] `apps/api/tests/routes/admin.test.ts` — NEW integration tests covering ADMIN-01 (credential validate/encrypt/no-echo), ADMIN-02 (exclusive is_shared), ADMIN-03 (403 for non-admin on every admin route — import `app`, never adminRouter directly), and the member self-service credential cases. Created in Plan 03 Tasks 2 & 3 (RED first).
|
- [ ] `apps/api/tests/routes/admin.test.ts` — NEW integration tests covering ADMIN-01 (credential validate/encrypt/no-echo, all failure modes → one generic 400), ADMIN-02 (exclusive is_shared), ADMIN-03 (403 for non-admin on every admin route — import `app`, never adminRouter directly), and the member self-service credential cases (shared-helper reuse). Created in Plan 03 Tasks 2 & 3 (RED first).
|
||||||
- [ ] `apps/pwa/e2e/admin.spec.ts` — NEW e2e: admin sees nav entry + reaches /admin; non-admin (route-mocked isAdmin:false) sees no entry and is redirected. Created in Plan 04 Task 3 (RED first).
|
- [ ] `apps/pwa/e2e/admin.spec.ts` — NEW e2e: admin sees nav entry + reaches /admin; non-admin (route-mocked isAdmin:false) sees no entry and is redirected. Created in Plan 04 Task 3 (RED first; this spec is the binding gate, playwright-cli is supplementary).
|
||||||
- [ ] Extend `apps/api/tests/auth/user.test.ts` — first-login-wins cases (Plan 02 Task 2).
|
- [ ] Extend `apps/api/tests/auth/user.test.ts` — first-login-wins cases (Plan 02 Task 2).
|
||||||
- [ ] Extend `apps/api/tests/routes/me.test.ts` — isAdmin + needsProviderSetup cases (Plan 02 Task 3).
|
- [ ] Extend `apps/api/tests/routes/me.test.ts` — isAdmin + needsProviderSetup cases (Plan 02 Task 3).
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ Existing infrastructure (Vitest + Playwright + real-DB harness + global-setup se
|
|||||||
|----------|-------------|------------|-------------------|
|
|----------|-------------|------------|-------------------|
|
||||||
| Live CalDAV PROPFIND against the real Fastmail account on a real credential save | ADMIN-01 | CI/e2e mock CalDAV (dev-bypass user 1 has no Fastmail credential — [[dev-data-user1-no-calendars]]); a true end-to-end save against Fastmail needs a real app password | At go-live, an admin enters a real Fastmail app password in the credential sheet; confirm 200 + the member's calendar appears after the initial sync. Optional operator spot-check, not a phase gate. |
|
| Live CalDAV PROPFIND against the real Fastmail account on a real credential save | ADMIN-01 | CI/e2e mock CalDAV (dev-bypass user 1 has no Fastmail credential — [[dev-data-user1-no-calendars]]); a true end-to-end save against Fastmail needs a real app password | At go-live, an admin enters a real Fastmail app password in the credential sheet; confirm 200 + the member's calendar appears after the initial sync. Optional operator spot-check, not a phase gate. |
|
||||||
|
|
||||||
All other phase behaviors (route guard, no-echo, encryption-at-rest, exclusive is_shared, nav gating, /admin redirect) have automated coverage (Vitest + Playwright/playwright-cli — the route guard and nav gating are desktop-Chromium-drivable per CLAUDE.md, so no human checkpoint).
|
All other phase behaviors (route guard, no-echo, encryption-at-rest, exclusive is_shared, nav gating, /admin redirect) have automated coverage (Vitest + Playwright/playwright-cli — the route guard and nav gating are desktop-Chromium-drivable per CLAUDE.md, so no human checkpoint; playwright-cli is a supplementary confirmation while the e2e spec is the binding gate).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -94,3 +94,4 @@ All other phase behaviors (route guard, no-echo, encryption-at-rest, exclusive i
|
|||||||
- [x] `nyquist_compliant: true` set in frontmatter
|
- [x] `nyquist_compliant: true` set in frontmatter
|
||||||
|
|
||||||
**Approval:** approved 2026-06-13
|
**Approval:** approved 2026-06-13
|
||||||
|
</content>
|
||||||
|
|||||||
Reference in New Issue
Block a user