Skip to content

Architecture improvements + non-technical-user UX round#16

Closed
bradburch wants to merge 15 commits into
mainfrom
arch-improvements
Closed

Architecture improvements + non-technical-user UX round#16
bradburch wants to merge 15 commits into
mainfrom
arch-improvements

Conversation

@bradburch

Copy link
Copy Markdown
Owner

Summary

This branch carries a series of architecture cleanups followed by a UX hardening round driven by simulated non-technical-user testing.

Architecture / refactoring

  • Admin booking lifecycle: list, confirm, cancel endpoints and dashboard section
  • Month availability reads capacity from D1 instead of Google Calendar; stub provider registry collapsed to Google Calendar only
  • src/shared made the single source of truth for service/question/option types; data-layer hygiene pass
  • Embed app split up with a shared useAsync (retains last-known data on failed loads); admin form state pushed down into sections
  • Widget preview + postMessage security improvements; stale-error and Calendar refetch-loop fixes
  • Project skill documenting how to run Pawbook locally without sending real email

UX round for non-technical sitters (final commit)

Two persona subagents (business setup, daily operations) drove the real app and reported friction; everything blocking was fixed and re-verified against the fixed build:

  • Time off: inclusive "First/Last day off" fields converted to the DB's exclusive convention at the UI boundary (blocking Aug 10–17 no longer leaves Aug 17 bookable)
  • Service options: duration-derived option keys auto-dedupe, so two same-length options with different names save; only true duplicate names error
  • Booking triage: dashboard lands on Bookings with "Needs your reply (N)"; confirm/decline/cancel give explicit feedback including whether the client was emailed; cancel asks for confirmation; declined is stored and displayed distinctly from cancelled
  • Status emails: confirm/decline/cancel email the customer when Resend is configured; response carries notified so the dashboard stays honest
  • New fields (additive migration 0008): business contact email/phone (shown in the widget), client phone, per-pet care notes
  • Plain language: embed page copy buttons + numbered Squarespace/Wix steps; nav renames ("Your website", "Pet types"); seed data attached to the demo customer so no "Unknown customer" rows

Test plan

  • npm test — 245/245 passing (includes new tests for the declined lifecycle and same-duration option keys)
  • Typecheck + eslint + prettier clean
  • Local migration applied (0008_contact_and_notes.sql, additive-only) and reseeded; API smoke-tested; three browser-driving subagent passes verified the flows end to end

🤖 Generated with Claude Code

bradburch and others added 15 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>
Fixes from two rounds of simulated non-technical-user testing (setup and
daily-operations personas), re-verified against the fixed build:

- Time off: the UI now treats the typed range as inclusive ("First/Last
  day off, both days are included") and converts to the DB's exclusive
  convention at the boundary — blocking Aug 10-17 no longer silently
  leaves Aug 17 bookable.
- Service options: duration-derived option keys auto-dedupe server-side,
  so two same-length options ("30 minutes" and "Puppy Check-in") save
  fine; only genuinely duplicate names error, and the message says so.
- Booking triage: dashboard lands on Bookings with a "Needs your reply
  (N)" group sorted by date; confirm/decline/cancel show a fixed bottom
  status bar including whether the client was emailed; cancelling a
  confirmed booking asks first; declined requests are stored (Declined
  flag) and displayed distinctly from cancelled bookings.
- Booking status emails: confirm/decline/cancel notify the customer via
  Resend when email is configured; the response reports notified so the
  dashboard is honest when it isn't.
- New sitter-facing data (migration 0008, all additive): business
  contact email/phone (shown in the widget), client phone numbers, and
  per-pet care notes.
- Plain language: embed page gets copy buttons and numbered
  Squarespace/Wix steps; "Embed" nav renamed "Your website", "Pets" to
  "Pet types"; Services page explains how to add a new offering; stale
  error banners clear on section switch; login screen drops the
  DEMO_NOTES.md reference.
- Seed: demo bookings belong to the demo customer (no more "Unknown
  customer"), plus pending requests so a fresh install demos triage.

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

Copy link
Copy Markdown
Owner Author

Superseded by #17 — same content, rebased cleanly onto main after the #15 squash made this branch's history conflict.

@bradburch bradburch closed this Jul 8, 2026
@bradburch bradburch deleted the arch-improvements branch July 8, 2026 02:24
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