Skip to content

Add sitter booking dashboard: confirm/decline requests + calendar reconciliation#21

Merged
bradburch merged 17 commits into
custom-servicesfrom
sitter-booking-dashboard
Jul 11, 2026
Merged

Add sitter booking dashboard: confirm/decline requests + calendar reconciliation#21
bradburch merged 17 commits into
custom-servicesfrom
sitter-booking-dashboard

Conversation

@bradburch

Copy link
Copy Markdown
Owner

Summary

  • New admin dashboard section where a sitter can see incoming booking requests, confirm/decline pending ones, and cancel confirmed ones — ported from an equivalent already-shipped implementation on main (which diverged from custom-services before that work landed), adapted for this branch's per-tenant TenantServices model.
  • Every dashboard load reconciles against Google Calendar: if a synced booking's event was deleted directly in Calendar, the booking is auto-cancelled. Throttled by a KV TTL cache (120s) so back-to-back loads don't hammer the Calendar API.
  • Best-effort customer email notification on status change.

Notable review findings (all resolved)

  • A cross-task bug only visible from the whole-branch view: reconciliation's candidate-booking query had no date bound while the Calendar query was limited to a 181-day window, so any booking outside that window (a just-completed booking, or one booked far out) would have been silently auto-cancelled. Fixed by bounding the candidate query to the same window, plus two new regression tests locking down the spec's non-goals (no Calendar mutation on cancel, no time-change reconciliation).
  • Two smaller issues (missing .pb-chip-bad CSS class, a locally-rendered savebar that could visually collide with the app-level one) were caught and fixed during task-level review.

Test plan

  • npm run typecheck
  • npm run lint
  • npm test (256/256 passing)
  • npm run build
  • Independent whole-branch code review (spec alignment, tenant isolation, error-handling boundaries) — approved

🤖 Generated with Claude Code

bradburch and others added 17 commits July 10, 2026 11:35
…ar reconciliation)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…aces

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements Task 1 schema change (Declined column on BookingRequests) and three
repo-layer functions (listBookingsForTenant, updateBookingStatus,
getBookingWithCustomer) that the sitter dashboard will build on. Includes full
test coverage for booking lifecycle state transitions and tenant isolation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
BookingRow.Declined (added in ae009b3) is required, which flows through to
CapacityRow (BookingRow & { CapacityKind }). Two pre-existing CapacityRow
literals in availability.test.ts's rowsToCapacityEvents tests were missing
it; npm test (vitest) doesn't full-typecheck so this wasn't caught until
npm run typecheck was run explicitly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GET /:slug/admin/bookings lists a tenant's bookings joined with customer
contact info; POST /:slug/admin/bookings/:id/status transitions a booking's
status and best-effort emails the customer, reporting whether they were
notified.
The try/catch only wrapped the reconcileBookingsWithCalendar call; the KV get/put
around it could still throw uncaught out of the GET /admin/bookings route on a
KV outage. Wrap the whole function body so a KV failure is as best-effort as a
Calendar failure.
The previous fix moved the KV put inside the try block, so a
reconcileBookingsWithCalendar failure (the exact case the TTL cache exists to
throttle) skipped the marker write entirely, letting every subsequent
dashboard load during an outage re-attempt the full Calendar round-trip.
Use try/catch/finally so the marker always gets written (guarded with its own
.catch), and guard the initial KV get the same way so a KV read failure is
treated as a cache miss instead of throwing.
Implements Task 5: adds the AdminBooking type (for displaying booking info in the admin dashboard) and the bookings namespace with list() and setStatus() methods to the adminApi object. These will be consumed by the admin booking dashboard component in Task 6.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add .pb-chip-bad to admin.css (existed on main, wasn't ported over
  when the component was copied); cancelled bookings now render with
  the bad/red styling instead of falling back to the default chip.
- Replace BookingsSection's local fixed .pb-savebar with an in-flow
  .pb-hint message. App.tsx already renders a global fixed savebar
  across all sections; a second one at the same position would
  overlap it when both are active.
listSyncedBookingIds returned ALL synced, non-cancelled bookings for the
tenant with no date bound, while reconcileBookingsWithCalendar only queried
Google Calendar for a fixed [today-1, today+180) window. Any booking outside
that window (already completed, or booked far enough out) could never appear
in the Calendar response and was wrongly flagged "missing" and auto-cancelled.

Give listSyncedBookingIds the same window bounds reconcileBookingsWithCalendar
computes for its own Calendar query, using the same overlap-check idiom as
listCapacityRows, so only bookings we could actually have seen in the Calendar
response are considered candidates for cancellation.

Also locks down two spec non-goals with regression tests: cancelling a booking
never calls out to Google Calendar (admin-bookings.test.ts), and reconciliation
only checks bookingId presence, never diffs event start/end times
(calendar-reconcile.test.ts). Test fixture dates switched from hardcoded 2030
values to dates computed relative to the real clock, since the new window
bound is itself relative to real "today" and a hardcoded future date would
eventually age out of it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
listCalendarEvents fetched up to 2500 events with no pagination. When a
tenant's query window has more instances than that (plausible once
recurring series are expanded via singleEvents=true), events past page 1
went missing from the response. reconcileBookingsWithCalendar infers
"event deleted" from a booking id's absence from that list, so truncation
could wrongly auto-cancel real, confirmed bookings.

listCalendarEvents now throws if the Google API response includes a
nextPageToken, instead of silently returning a partial list. Both
existing callers (reconcileIfStale's try/catch, availability.ts's
try/catch) already degrade safely on a thrown error, so this fails loud
at the source rather than adding pagination.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
BOOKING_COLS_QUALIFIED existed only to disambiguate the Id column in two
JOIN queries. BookingRequests.* does the same thing without maintaining a
derived, table-prefixed copy of BOOKING_COLS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@bradburch bradburch merged commit d9440be into custom-services Jul 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant