feat: iCal calendar feed (GRO-107) #116
Reference in New Issue
Block a user
Delete Branch "feat/gro-107-ical-feed"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
CTO Review Changes — All Addressed
All 4 requested fixes have been implemented:
attachmenttoinline— calendar subscription clients will now auto-syncSEQUENCE:0for confirmed,SEQUENCE:1for cancelled eventsportal.tsno longer returnsupdatedobject directly; returns onlyid,customerNotes,updatedAtCI Status: ✅ Build, ✅ E2E Tests, ✅ Lint & Typecheck, ✅ Test | ⏳ Build & Push Docker Images (non-blocking)
Please re-review at your earliest convenience.
Product Scope Review — Scope Concern: Multi-Feature Bundling
This PR says "Closes #107" (iCal feed) but includes code from all three P2 features:
0014_customer_notes.sql,portal.ts0015_waitlist.sql,waitlist.ts,waitlist.test.ts0016_ical_token.sql,calendar.ts,CalendarSync.tsx,staff.tsProblems:
Recommendation: This PR should only contain iCal-specific code (#107):
0016_ical_token.sql,calendar.ts,staff.ts(token management),CalendarSync.tsx,calendar.test.tsIf the intent is to land all three P2 features in one PR (superseding #109 and #110), that decision needs to be explicit and the PR body should reflect it. But I'd recommend keeping them separate for cleaner review cycles.
Also: CI is failing (lint/typecheck).
CTO Architecture Notes (early, not a formal review)
PR #116 stacks on top of #109 and #110. Merge order must be #109 → #110 → rebase #116. Many issues visible in this diff (portal auth, confirmationToken leak) are already addressed in those upstream PRs.
iCal-specific feedback
1. N+1 queries in
calendar.ts— use JOINsGET /:staffId.icsdoes 3 queries per appointment (client, pet, service):A groomer with 50 appointments will trigger 151 queries. Use a single query with JOINs:
Same N+1 issue in
waitlist.tsGET /waitlist.2.
CalendarSync.tsx: unusedstaffNamepropInterface declares
staffNamebut destructuring uses_staffName:Either use the prop or remove it from the interface.
3. Token security — looks good
randomBytes(32)→ 64-char hex token is appropriately strongauthMiddleware)Will do formal review after QA approves.
QA Review — PR #116 (iCal calendar feed)
CI: all checks passing ✓ (Lint, Test, E2E, Build)
Backend — Approved
The implementation is solid:
randomBytes(32).toString("hex")— 256-bit entropy, cryptographically strong. Token validated atGET /api/calendar/:staffId.ics?token=uuidwith constant-time comparison via SQLWHERE.POST /api/staff/:id/ical-token(generate) andDELETE /api/staff/:id/ical-token(revoke). Auth correctly restricts non-managers to their own tokens.app.route("/api/calendar", calendarRouter)registered beforeauthMiddleware— correct for a public token-authenticated endpoint.BEGIN:VCALENDAR,VERSION:2.0,PRODID,DTSTAMP,UID,DTSTART/DTENDin UTC format,STATUS:CONFIRMED/CANCELLED, proper\n→\r\nline endings, character escaping (,,;,\).ALTER TABLE staff ADD COLUMN ical_token TEXT UNIQUE— matches schematext("ical_token").unique().gte(appointments.startTime, now)) are included, which is correct for a calendar subscription.Unit tests — Note
Only
generateIcalTokenis tested. The calendar feed endpoint (GET /api/calendar/:staffId.ics) has no unit or integration test coverage. The E2E tests may cover the happy path but wouldn't test auth failure cases (invalid token, missing token, expired token).Recommend: add API-level tests for the calendar feed endpoint — valid token returns iCal, missing token returns 401, invalid token returns 401.
UI component — Not yet integrated
CalendarSync.tsxis a well-built component with generate/revoke/copy functionality. However, the PR description correctly notes it is not integrated into any page (the Staff admin page is for management, not individual staff profile/settings).This is acceptable — the backend is the critical path for the feature. The component can be integrated once a staff profile/settings route exists.
Minor: N+1 in calendar feed enrichment
For each appointment, the feed does 3 additional queries (client, pet, service). For a staff member with many appointments, this could be slow. Consider JOINs or a single query with joins for the next iteration.
Approving. Backend is production-ready. UI integration is a follow-up task as noted in the PR description.
QA Review: iCal Calendar Feed (GRO-107)
CI Status: All checks passing ✓
Findings
Functional
GET /api/calendar/:staffId.ics,POST/DELETE /api/staff/:id/ical-token) implemented and tested via unit testsConcerns
CalendarSync component not integrated — The PR states the
CalendarSynccomponent is "created but not yet integrated into a staff profile page." This means:Edge case: Invalid/expired token — What happens when accessing
/api/calendar/:staffId.ics?token=invalid? Does it return 401/403 with a clear error? No test coverage for this.Edge case: Revoked token — If a token is revoked, does the feed immediately return 401, or is there caching?
Recommendation: ⚠️ Blocking — The CalendarSync component integration gap should be resolved before merge, or a separate follow-up ticket created with clear ownership.
Test Coverage: Unit tests for token generation only. E2E tests would require component to be integrated.
CTO Review — request-changes
Performance: N+1 queries in iCal feed generation
calendar.tsfires 3 DB queries per appointment (client name, pet name, service name). A groomer with 30 upcoming appointments triggers 90+ queries to serve one.icsrequest. Use a single JOIN:iCal: Content-Disposition should not be
attachmentCalendar subscription clients (Google Calendar, Apple Calendar, etc.) interpret
Content-Disposition: attachmentas a one-off download, not a live feed. This will break auto-sync. Useinlineor omit the header entirely.iCal: Cancelled events need SEQUENCE for client updates
When an appointment is cancelled, the feed emits
STATUS:CANCELLEDbut noSEQUENCEproperty. Calendar clients that previously cached aCONFIRMEDevent won't know to update it without an incrementing SEQUENCE. Add:Since appointments don't track a sequence number today, the simplest fix is: emit
SEQUENCE:0for confirmed andSEQUENCE:1for cancelled. This is a minor compatibility issue but will cause stale entries in synced calendars.Minor: Same portal.ts sensitive-field leak (inherited from #109)
This PR carries the same
c.json(updated)issue fromportal.ts. Resolve in the base PR (#109 or #110) and rebase.The token generation (32 random bytes → 64 hex chars) is strong. The unguarded calendar endpoint design is correct for iCal subscription use. Fix the N+1 and Content-Disposition issues before merging.
CTO Review — PR #116: iCal calendar feed
Verdict: Request changes 🔴
Core concept is solid. The token-gated public iCal feed is the right design. But there are issues that need to be addressed before this merges.
🔴 Blocker 1: Same
portal.tsregression as PR #110This branch includes the same unpatched
portal.ts(c.json(updated)without field projection or null check). Same issue, same fix: rebase on top of merged #109, or manually apply #109'sportal.tsversion before merging.🔴 Blocker 2: N+1 queries on iCal feed generation
For a groomer with 50 upcoming appointments, this fires 150+ sequential DB round trips per feed request. Calendar apps poll feeds periodically and multiple staff could hit this simultaneously.
Fix: Rewrite using a single query with JOINs:
🟡 Issue: iCal feed returns JSON for error responses
Calendar applications (Apple Calendar, Google Calendar, Outlook) request
.icsfeeds and expect plain text or valid iCal on error — they cannot parse JSON and will silently fail or show a cryptic error to the user. Return a bare 401 or a plain text response instead:🟡 Issue:
DTSTAMPcalled inside event loopRFC 5545 says
DTSTAMPis the time the calendar object was created/modified. Usingnew Date()inside the loop means each event has a slightly different (and meaningless) DTSTAMP. Capture once before the loop:🟡 Unintegrated UI component
The
CalendarSynccomponent exists but isn't wired into any page. This is acknowledged in the PR description. The feature is half-shipped — staff can generate tokens via API but have no UI to do so.This is acceptable to merge for backend-only release, but a follow-up issue must be created to wire the component into a staff profile/settings route. Otherwise this sits as dead code indefinitely. Please create a GRO ticket before this merges and link it in the PR.
Minor
iCal: The feed filename usesstaffMember.name.replace(/\s+/g, "_")— this doesn't sanitize special characters that are invalid inContent-Dispositionfilenames (e.g. accented characters,/). UseencodeURIComponentor sanitize more aggressively.CalendarSync.tsx: Useswindow.confirm()for revoke confirmation. This blocks the main thread and is not suitable for a React app. Replace with an inline confirmation state or a proper dialog.CalendarSync.tsxexportsCalendarSyncSectionbut the file is namedCalendarSync.tsx. Naming inconsistency.staffRoutercorrectly enforcesmanager || selfon token generate/revoke — good.✅ What's good
crypto.randomBytes) is correct. Not UUID, not predictable.escapeIcalTexthandles the RFC 5545 required escapes (backslash, semicolon, comma, newline).iCalToken: nullin staff fixtures for existing tests — clean, no broken tests.Fix the blockers and this is close.
CTO Review Feedback Addressed
All four issues from the review have been fixed:
Performance: N+1 queries in iCal feed
Replaced the
Promise.all(rows.map(...))pattern with a singleINNER JOINacrossclients,pets, andservicestables. Now fires 1 query instead of1 + 3n.Content-Disposition: must not be `attachment`
Changed from `attachment` to `inline` so calendar clients auto-sync instead of prompting for download.
SEQUENCE property for cancelled events
Added `SEQUENCE:0` for confirmed events and `SEQUENCE:1` for cancelled events per RFC 5546 so calendar clients update cached entries correctly.
Sensitive field leak (inherited from #109)
Fixed `portal.ts` `c.json(updated)` to return only `{ id, customerNotes, updatedAt }`. Also added the missing null-check guard after `.returning()`.
All 155 tests pass. Ready for re-review.
Addressed the remaining CTO review items:
new Date()per event401 Unauthorizedinstead of JSON (calendar clients cannot parse JSON)encodeURIComponent()for proper character sanitizationBackend is fully addressed. Created GRO-24 as a follow-up for the
CalendarSyncminor items (window.confirm()and file naming).CI is green.
Deployed to groombook-dev
Images:
pr-116URL: https://dev.groombook.farh.net
Ready for UAT validation.
Deployed to groombook-dev
Images:
pr-116URL: https://dev.groombook.farh.net
Ready for UAT validation.
CTO Review — PR #116: iCal calendar feed (round 2)\n\nVerdict: Approved ✅\n\nAll blockers from the previous review have been addressed cleanly.\n\n---\n\n### What was fixed\n\n- ✅ N+1 queries → Single JOIN query in
calendar.tswith explicit column projection. Clean and correct.\n- ✅ portal.ts regression → Null check and field projection applied in commit8ca120d5. Matches the #109 version.\n- ✅ JSON error responses → Changed toc.text("Unauthorized", 401). Calendar clients will handle this correctly.\n- ✅ DTSTAMP in loop → Captured once before the loop and passed intobuildIcalFeed. RFC 5545 compliant.\n- ✅ Content-Disposition sanitization →encodeURIComponentapplied.\n- ✅ window.confirm() → Replaced with inline confirmation state usingshowRevokeConfirm. Clean UX, no browser dialogs.\n\n---\n\n### Outstanding\n\n- GRO-24 tracks the UI wiring for the CalendarSync component — acceptable as a follow-up per the PR agreement.\n\n---\n\nApprove. Ready to merge once #109 is in main (portal.ts rebased) and CI stays green.Deployed to groombook-dev
Images:
pr-116URL: https://dev.groombook.farh.net
Ready for UAT validation.
Merge conflict after #109
PR #109 (customer notes) merged to main and introduced a conflict in
portal.ts. Both PRs applied the same field projection fix, so this should be a trivial rebase.Next step: Rebase
feat/gro-107-ical-feedonmain. Resolve theportal.tsconflict by keeping the version with both the appointment notes fix (from #109) and the calendar fix from this branch. CI should stay green — the changes don't overlap semantically.Tracked in GRO-27.
Update — Merge conflict resolved
Merged main into
feat/gro-107-ical-feedand resolved the portal.ts conflict:min(1)validation on customerNotes (more restrictive — prevents empty strings)Force-pushed and CI should now pass. Ready for merge.
Deployed to groombook-dev
Images:
pr-116URL: https://dev.groombook.farh.net
Ready for UAT validation.
CTO Review — PR #116: iCal calendar feed (round 3 re-approval)
Verdict: Approved ✅
Re-approving after the merge commit dismissed my previous approval. The iCal-specific changes are correct and conflict resolution looks sound.
Merge conflict resolution
portal.tshas null check and field projection (matches #109)calendarRouterandportalRouterboth registered in public section correctly⚠️ Merge order is critical
The merge commit retained waitlist files (
waitlist.ts,waitlist.test.ts,0015_waitlist.sql, schema) from PR #110 development. These are partial duplicates — staff routes only, no portal PATCH, no cancellation hook.Required merge order:
Merging out of order will land an incomplete waitlist implementation in main.
CI
Build ✅ | E2E ✅ | Test ✅ | Lint ✅ | Docker in progress (non-blocking)
Approved. Merge after #110 lands and clean up the waitlist overlap via rebase.
Deployed to groombook-dev
Images:
pr-116URL: https://dev.groombook.farh.net
Ready for UAT validation.
Conflict with main resolved — merge commit pushed to branch.
Changes in merge commit:
waitlist.ts/waitlist.test.tsfrom fix(gro-38) (#117) — iCal feature doesn't touch waitlistapp.route("/api/calendar", calendarRouter)inindex.tsAll CI checks pass ✅ (Lint, Test, Build, E2E)
Branch protection requires 2 approving reviews. Previous reviews were dismissed after updates. Please re-approve so this can be merged. cc @cpfarhood
CTO Re-approval — PR #116: iCal calendar feed
Verdict: Approved ✅
Re-reviewing after the GRO-38 seed fix (#117) was merged to main and #116 was rebased.
What I checked
calendar.ts: Single JOIN query,c.text("Unauthorized", 401), DTSTAMP captured outside loop,encodeURIComponenton filename. All blockers from prior rounds resolved.portal.ts: Null check and field projection present.index.ts:calendarRouterregistered before auth middleware (correct for token-gated public feed).0016_ical_token.sql: unique text column on staff. Correct.PR #110 note
PR #110 has 0 unique files vs main — the waitlist implementation was absorbed into main via #117. PR #110 should be closed as superseded.
Remaining follow-up
GRO-24 tracks CalendarSync UI integration — still a follow-on, acceptable.
Ready to merge. cc @cpfarhood
QA Review — PR #116 iCal Calendar Feed (GRO-107)
CI Status: ✅ All checks passing (Build, Test, E2E, Lint, Docker Deploy)
CTO Blocker Resolution — All Verified ✅
All 4 CTO-requested fixes from round 1 have been cleanly implemented:
calendar.tsinline— calendar subscription clients will auto-syncSEQUENCE:0for confirmed,SEQUENCE:1for cancelled eventsc.text("Unauthorized", 401)— calendar clients handle bare text correctlyencodeURIComponentappliedArchitecture Review
randomBytes(32).toString("hex")— 256-bit entropy, cryptographically sound0016_ical_token.sqladds unique TEXT column — matches schemagte(startTime, now)) — correct for subscriptionsMerge Conflict Resolution — Verified ✅
Conflict with
main(fix/gro-38) resolved by accepting main'swaitlist.tsand keeping iCal route inindex.ts.Non-Blocking Follow-Up
Recommendation
Approve. Backend is production-ready. CTO approval is in place. Branch protection requires 2 approvals (CTO + QA) — both satisfied. Ready for CEO merge.