Merge PR #50: fix(GRO-2180) portal Appointments ISO startTime (dev → uat)
QA-approved (gb_lint); PR CI green after transient runner re-run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The /api/portal/appointments endpoint returns ISO startTime/endTime plus
nested pet/service/staff objects, but the portal client Appointment type
expected flat date/time/petName fields. isUpcoming() read appt.date/appt.time
(both undefined), so parseTimeTo24Hour(undefined) threw a TypeError; the
useEffect try/catch set the error state and the success-path-only Book New
button became unreachable.
- Add normalizeAppointment() at the fetch boundary mapping the API shape to the
flat Appointment shape (derives display date/time from startTime, duration
from the start/end delta), tolerant of the legacy flat shape.
- Prefer absolute startTime in isUpcoming(); fall back to date/time.
- Harden parseTimeTo24Hour against blank/undefined input (no NaN).
- Add Appointment.startTime/endTime to the type.
- Tests: normalizeAppointment + isUpcoming(startTime) + parseTimeTo24Hour safety.
- Update UAT_PLAYBOOK.md §5.12.2 and new §5.12d regression cases.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The bundle at /login was executing but the React tree never painted —
no console errors, no fallback UI, just an empty <div id='root'>.
Add three layers of defense so a future failure of this shape is
captured instead of being silently swallowed:
1. window 'error' and 'unhandledrejection' listeners in main.tsx,
printing structured context to console.error so Playwright
sees the failure in the console log even if React unmounts
the tree.
2. A top-level <ErrorBoundary> in main.tsx that renders the
actual exception (name, message, stack) inside the DOM
instead of leaving <div id='root'> empty. The boundary also
logs to console.error via componentDidCatch.
3. New tests for the ErrorBoundary (renders children, surfaces
thrown errors visibly) and two new UAT_PLAYBOOK test cases
(TC-WEB-5.1.6 / 5.1.7) that explicitly assert the
'never-blank-root' invariant on UAT.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Root cause: `Dashboard.tsx:194` runs its own `!sessionId && !isImpersonating &&
!getDevUser()` auth guard, redirecting to `/login` if `sessionId` is null. For
SSO customers, the CustomerPortal's useEffect has to call `/api/auth/get-session`
and then `/api/portal/session-from-auth` to populate `portalSessionId`. During
that bootstrap window (typically 100-300ms), `sessionId` is null and the guard
fires — redirecting the user to `/login` and breaking the post-sign-in flow.
App.tsx additionally returned `null` at `/login` for authenticated users
(`showCustomerPortal` is false at `/login`), leaving a blank React root even
if the redirect target was /login itself.
Fix:
- `CustomerPortal.tsx`: show a 'Loading…' state (`role=status`) while
`!initComplete`. The portal chrome and its child sections only mount once
the bootstrap has resolved, so child auth guards don't fire prematurely.
- `App.tsx`: at `/login` with a valid session, redirect to `/` so the
customer lands on the portal instead of seeing a blank page.
- `App.tsx`: only return `LoginPage` when at `/login` — other portal
routes defer the auth check to `CustomerPortal` (the customer SSO bridge
resolves `portalSessionId` on mount).
- `UAT_PLAYBOOK.md`: add §5.27 with 8 cases covering the bug, the loading
state, the /login auto-redirect, the unauth fallback, and the groomer /
impersonation non-regressions.
- `src/__tests__/portal.test.tsx`: add a regression test that asserts the
loading state is shown during the bridge and the portal nav is NOT in the
DOM mid-bootstrap.
Reproduction (Shedward, run b4ae0155; reproduced locally on UAT image
`2026.06.01-ec29f71`):
1. From `about:blank`, complete customer SSO as `uat-customer`.
2. `browser_navigate` to `/portal`.
3. Pre-fix: redirected to `/login` with blank React root.
4. Post-fix: URL stays at `/portal`, dashboard renders with customer name.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
fix(GRO-2012): pass portalSessionId to RescheduleFlow for SSO bridge customers (closes#38)
- src/portal/CustomerPortal.tsx:329 - use portalSessionId fallback for RescheduleFlow
- src/__tests__/portal.test.tsx - new regression test
- UAT_PLAYBOOK.md §5.26 - new test cases
cc @cpfarhood
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Root cause: SW remained in waiting phase after redeploy, serving stale
precached assets. Without skipWaiting/clientsClaim the old SW persisted
and controlled the page even after a new SW was installed.
Fixes blank-page regression where React never mounted on login.
App.tsx lines 389-393 redirected ALL authenticated users to /admin,
breaking customer portal access after SSO login.
Now checks `session.user.role === "staff"` before redirecting.
Customers (role !== "staff") can access the portal at /.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
QA regression: PR #26 removed fireEvent and waitFor from the
@testing-library/react import, breaking 21 test cases and typecheck.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Lines 28 and 40 access mock.calls[0] which is possibly undefined under
strict TypeScript. Adding ! to satisfy TS2532.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add a StatusBadge component that renders human-readable labels
(Confirmed, Pending, Waitlisted, etc.) with semantic color classes
for appointment cards in the portal. Replaces raw status strings.
- Added STATUS_LABELS map for human-readable status labels
- Updated STATUS_COLORS to use accessible amber/blue tones
- Exported StatusBadge for testing
- Added unit tests for all 7 badge states plus fallback
- Updated UAT_PLAYBOOK.md §5.12c with status badge test cases
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- analytics.test.ts: add vi to vitest import (was used at lines 24, 37, 66)
- BookingError.test.tsx: use regex matchers so phone/email assertions
match partial text in combined <p> element
Co-Authored-By: Paperclip <noreply@paperclip.ing>
GRO-1793: Dynamic portal time slots (replace hardcoded) (#23)
Replaces hardcoded time slot arrays in portal BookingFlow and RescheduleFlow with API-fetched dynamic availability.
QA review pointed out:
- Lint error: 'act' imported but never used in test file
- 6 test failures: date input lacked accessible label
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Gro-1794 required UAT test cases for the booking funnel analytics events.
Covers all 6 events × both flows (public/portal), plus PII audit and
no-op-safety checks.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- New analytics utility (src/lib/analytics.ts) with ANALYTICS_EVENTS constants
and fireAnalyticsEvent() – thin wrapper over window.dispatchEvent, no-op safe
Built for Plausible/GTM integration later.
- Public booking wizard (Book.tsx): fires step-transition events at each step
(service → time → contact → submit) plus booking_confirmed on the dedicated
confirmation page.
- Portal BookingFlow (Appointments.tsx): fires equivalent events for the
portal booking flow. booking_confirmed fires via useEffect when the inline
success state is shown.
- BookingErrorPage: fires booking_error on mount (no PII in payload).
Events include step name and flow type (public/portal) but contain no PII:
no names, emails, phone numbers, or pet names in any payload.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Both BookingFlow and RescheduleFlow in Appointments.tsx now fetch
from /api/book/availability when a date is selected, matching the
public booking wizard behavior. Loading and error states shown.
- Removed hardcoded availableTimes arrays from both flows
- Added useEffect that fetches availability on date change
- Shows "Checking availability…" while loading
- Shows error message on fetch failure
- Shows "No available slots" when API returns empty
Added tests for RescheduleFlow dynamic slot fetching covering:
loading, fetched slots, error, empty, API params, and re-fetch on
date change.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add "Start a new booking" button to BookingError linking to /admin/book
- Add "Book again" button to BookingCancelled linking to /admin/book
- Add business contact info section to BookingError (from BUSINESS_CONTACT_INFO constant)
- Replace hardcoded colors with CSS variables (--color-error, --color-cancelled, etc.)
- Add page-level string constants to eliminate hardcoded strings
- Add unit tests for both pages (9 tests passing)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Documents the acceptance criteria for GRO-1592: after completing
Authentik SSO login without VITE_API_URL set, the
__Secure-better-auth.session_token cookie must be present in the
browser and sent with subsequent /api/* calls.
Updated: UAT_PLAYBOOK.md §5.3
Co-Authored-By: Paperclip <noreply@paperclip.ing>
When VITE_API_URL is not set (e.g. in Docker/container deployments
where the env var was never injected), fallback to
window.location.origin so the auth client uses relative URLs and
cookies are sent to the correct origin.
Previously the fallback was empty string "", which caused the auth
client to default to http://localhost:3000 — the nginx sub_filter
workaround only handles strings baked into the JS bundle at build
time, not runtime-constructed URLs.
Fixes: SSO session cookie not set in browser after Authentik callback
Co-Authored-By: Paperclip <noreply@paperclip.ing>
promote: dev → uat (GRO-1173 buffer rules + GRO-1470 pet save persistence) (#14)
Merged-By: The Dogfather (CTO)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add §5.23 covering:
- API persistence (page reload verification)
- Save error state (form stays open on failure)
- Saving indicator (spinner while in-flight)
Updated UAT_PLAYBOOK.md §5.23
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- handlePetSave is now async; calls PATCH before updating local state
- API response used as source of truth for local state update
- Error state shown on API failure; edit form NOT cleared on failure
- Loading/saving indicator in PetForm while API call in flight
Refs: GRO-1470
Co-Authored-By: Paperclip <noreply@paperclip.ing>