Skip to content

Architecture improvements: booking lifecycle, single capacity source, and cleanup#15

Merged
bradburch merged 14 commits into
mainfrom
arch-improvements
Jul 7, 2026
Merged

Architecture improvements: booking lifecycle, single capacity source, and cleanup#15
bradburch merged 14 commits into
mainfrom
arch-improvements

Conversation

@bradburch

Copy link
Copy Markdown
Owner

Eight-task architecture pass from a whole-codebase review. Every task was implemented and reviewed by an independent agent pass; a final whole-branch review verified cross-task integration (verdict: ready to merge, 243/243 tests, typecheck + lint clean).

What changed

1. Admin booking lifecycle — nothing ever updated BookingRequests.Status before: sitters couldn't see or manage requests, and pending rows held capacity forever. Adds GET /:slug/admin/bookings + POST /:slug/admin/bookings/:id/status (confirm/cancel, atomic SQL guard, cancel is terminal) and a BookingsSection in the dashboard. Cancelling frees capacity automatically since capacity queries count pending/confirmed only.

2. Month availability reads D1, not Google Calendar — the month grid previously derived boarding/house-sit busy days from calendar events and failed open on errors, so it could contradict the authoritative booking check. Both now share the same D1 → capacity-engine pipeline; the calendar is strictly a one-way sync target.

3. Stub provider registry deleted — Notion/Gmail "integrations" only flipped a status flag. Collapsed to the real Google Calendar integration; connected-stub kept as a documented legacy value.

4. Data-layer hygiene — covering index for the windowed-slot capacity queries (migration 0007 + schema.sql), CHECK (PetCount >= 1) for fresh installs, opportunistic pruning of expired login codes, and migrate:local/migrate:remote scripts. Operational note: existing DBs must be baselined per migrations/README.md before ever running migrate:remote — 0002 is destructive if re-run.

5. Single source of truth for service/question/option types — killed the three parallel copies; UI types now derive from src/shared, and the admin save() payload is typed so a missed field mapping is a compile error (the class of bug that once silently dropped startTime/endTime/capacity).

6. Embed frontend structureuseAsync hook replaces four hand-rolled active-flag effects (stale-while-error semantics, no lint suppressions); the 630-line embed App.tsx split into Identify/BookTab/MineTab/QuestionField; availability check button got the missing in-flight state.

7. Admin form state pushed down — TimeOff/Clients own their draft state (with the old run() clear-error-at-action-start semantics preserved via clearError); NullableNumberField moved to sections/fields.tsx.

8. Small items — WidgetPreview's 70-line auto-size polling machinery replaced with a fixed-height scrolling iframe; embed postMessage targetOrigin tightened from '*' to the referrer origin with a safe fallback.

Notes for review

🤖 Generated with Claude Code

bradburch and others added 14 commits July 7, 2026 07:57
Sitters can now see all bookings and move them out of pending forever
limbo. Adds repo.listBookingsForTenant/updateBookingStatus (guarded
UPDATE — cancelled is terminal), GET/POST admin routes, a new
BookingsSection wired first in the dashboard, and tests covering the
list/confirm/cancel/validation/tenant-isolation/capacity-release cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
monthAvailability now builds its boarding/house-sit capacity map the same way
checkAvailability does (listCapacityRows -> rowsToCapacityEvents -> buildCapacity),
so the customer-facing month grid can no longer contradict the authoritative
booking check or fail open on a Google Calendar error. Calendar stays a
one-way sync target only.

The "mine" flag now comes from a new listUserBookingDatesInRange query keyed
on EndUserId (across all service types) instead of matching a calendar
event's customerEmail, since monthAvailability no longer reads events at all.
Deleted the now-unused calendar-read helpers (listCalendarEvents,
categorizeCalendarEvent, CalendarEvent, BLOCK_EVENT_SUMMARY).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Notion (crm) and Gmail (email) were speculative stub capabilities whose
"connect" only flipped a connected-stub status flag. Google Calendar is the
only real integration, so delete the registry generality: providers.ts
collapses to a single calendarView() projection, the stub connect endpoint
and setProviderStatus are gone, and the admin settings GET/AppsSection now
work with a plain `calendar` object instead of a `providers` array. The
ProviderConnections table/schema and the calendar OAuth flow are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add migration 0007_slot_index.sql with covering index for windowed-slot capacity
- Update sql/schema.sql to include the new index and CHECK constraint on PetCount
- Refactor createLoginCode to use db.batch() for atomic delete-then-insert with opportunistic pruning of expired codes
- Add migrate:local and migrate:remote scripts to package.json
- Add test verifying login code pruning deletes expired codes while preserving valid ones

All tests pass; typecheck and lint pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n types

ServiceOption now lives in src/shared/booking/service-rules.ts alongside the
existing ServiceQuestion/ServiceConstraints. app/shared-ui/api.ts and
app/admin/shared.ts derive their field shapes from the shared types
(Pick/Omit/intersection) instead of re-declaring every field, and App.tsx's
save() builds its PUT payload as an annotated SettingsPayload/ServicePayload/
ServiceOptionForm so a field added to the shared shapes — or dropped by a
hand mapping in save() — is a compile error instead of a silently missing
field on the wire (the class of bug that once dropped startTime/endTime/
capacity). Wire format and server behavior are unchanged; no server files
touched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ookTab

Adds app/shared-ui/useAsync.ts, a small hook that replaces the repeated
`let active = true` effect-cancellation pattern with a deps-driven
fn/reload contract exposing { data, error, loading, reload }. It never
calls setState synchronously in its own effect body (only from inside
.then/.catch), so it satisfies react-hooks/set-state-in-effect without
suppressing it, and its internal effect deps are just [fn, reloadCount]
so exhaustive-deps is trivially satisfied.

Converts four call sites to it: embed/Calendar.tsx's month-availability
fetch, embed App's MineTab bookings load, embed App's outer "me" load,
and admin App's customers load. The admin settings-load effect is left
as-is — `settings` is mutated directly by several child sections beyond
the initial fetch, and `refresh()` needs to stay an awaitable action
reused by several other handlers, so mirroring it through useAsync's
data would need a second effect just to sync state, defeating the point.
BookingsSection's own load pattern is untouched too, per scope.

Splits the ~630-line embed/App.tsx into Identify.tsx, BookTab.tsx,
MineTab.tsx, and QuestionField.tsx (verbatim moves, plus a small
embed/shared.ts for the slug/errorMsg helpers all four need), leaving
App.tsx as the shell/composition.

Also gives BookTab's availability check() a loading state: the button
disables and reads "Checking…" while the request is in flight, matching
the other buttons in the app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review finding: the admin customers loader caught its own error and
returned [], so a failed reload (e.g. a transient error right after
add/remove customer) blanked the customer list under the error banner —
the old hand-rolled effect propagated the error and never touched the
last-known list.

Fix: useAsync now keeps the previous data when a load fails
(stale-while-error) and only replaces it on success; loadCustomers
routes the error through handle() for the banner/sign-out but rethrows
instead of swallowing. Calendar keeps its original blank-grid-on-error
rendering via an explicit showData gate, so its behavior is unchanged;
MineTab and the embed "me" loader never reject, so they're unaffected.
Also documents that reload() is fire-and-forget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move TimeOff and Clients draft-form state from Dashboard into their respective sections, following the self-contained pattern of PetAdder and CalendarIdField. Extract NullableNumberField to a shared fields.tsx module used by Business and Services sections.

Changes:
- Create app/admin/sections/fields.tsx with NullableNumberField
- Update BusinessSection and ServicesSection to import from fields.tsx
- Move TimeOffSection to own blockStart/blockEnd state and addBlock/removeBlock handlers
- Move ClientsSection to own custEmail/custName state and customer CRUD handlers
- Move PetAdder API calls into the component (was previously in Dashboard)
- Update removePet handler in ClientsSection to stay consistent
- Slim Dashboard props: TimeOff now takes slug/token/onChanged/handleError; Clients takes slug/token/onCustomersChanged/handleError
- Props are identical in behavior (error handling, disabled/busy states, input clearing)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review fix: before the section refactor, every Clients mutation went through
Dashboard's run(), which called setError('') before the action — so a stale
error from an earlier failure never outlived a later action. Restore that by
passing a clearError prop into ClientsSection/PetAdder, called at the start
of addCustomer, removeCustomer, removePet, and PetAdder's add.

Also fold the three ClientsSection handlers into one mutate() helper that
mirrors run()'s clear/busy/catch shape, replacing the inline async IIFE on
the pet-remove button with a named removePet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sage security

Two changes:
1. Replace WidgetPreview's 25-tick polling loop + ResizeObserver auto-sizing with a fixed-height iframe (640px / 70vh) that scrolls internally. Removes ~60 lines of timing heuristics and observers.

2. Compute parent origin once in app/embed/shared.ts using document.referrer with '*' fallback, then use it as targetOrigin in both pawbook:resize and pawbook:booked postMessage calls (App.tsx, BookTab.tsx). Improves security by sending to computed origin instead of wildcard.

Message payloads and embed.js consumer remain unchanged. Tests, typecheck, lint all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g it

Review finding: renaming to _active only dodged the unused-var lint rule.
The prop had zero references inside WidgetPreview, so remove it from the
params, the prop type, and the call site. EmbedSection's own active prop
is untouched — the everActive lazy-mount gating still needs it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ners

Three Important findings from the final whole-branch review:

- Add migrations/README.md documenting that fresh installs use sql/schema.sql
  + sql/seed.sql, and that any already-provisioned DB must be baselined
  (create d1_migrations, insert rows for 0001-0007) before ever running
  migrate:local/migrate:remote, since 0002_tenant_config_limits.sql rebuilds
  Tenants and would wipe MaxHouseSitsPerDay/MaxStayNights/Timezone on re-run.
  Add a matching warning comment to the top of that migration file.

- Calendar.tsx: hold onAuthExpired in a ref (updated via a no-deps effect)
  instead of putting it in fetchMonth's useCallback deps, restoring the old
  safeguard against a future parent passing a fresh inline closure every
  render (which would otherwise refetch -> setState -> render -> refetch
  forever).

- TimeOffSection and BookingsSection now clear the Dashboard error banner at
  the start of their mutating actions (addBlock/removeBlock, confirm/decline/
  cancel), matching the clearError pattern already used by ClientsSection, so
  a stale error from an earlier failure doesn't outlive a later success.

Verified: npm test (243/243), npm run typecheck, npm run lint all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nding real email

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bradburch bradburch merged commit 93d573a into main Jul 7, 2026
2 checks passed
@bradburch bradburch deleted the arch-improvements branch July 7, 2026 22:17
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