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>
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>
- §5.7: add TC-WEB-5.7.5 through TC-WEB-5.7.7 for pet size/coat in admin UI
- §5.8.1 (new): add Buffer Rules Management test cases
- TC-WEB-5.8.2 through TC-WEB-5.8.7: create/edit/delete buffer rules, size/coat filtering, empty state
- §5.9: add TC-WEB-5.9.4 and TC-WEB-5.9.5 for service default buffer minutes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- PetForm interface: add sizeCategory and coatType fields
- EMPTY_PET: initialise new fields as empty strings
- openEditPet: pre-populate from pet.petSizeCategory and pet.coatType
- submitPet body: include petSizeCategory and coatType in POST/PATCH
- Pet form UI: add Size Category and Coat Type dropdowns after Breed field
- Size: Small / Medium / Large / X-Large (maps to enum values)
- Coat: Smooth / Double / Curly / Wire / Long / Hairless (maps to CoatType union)
- Both optional — blank "Not set" option matches API optional semantics
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Switch from ghcr.io (GitHub Container Registry) to git.farh.net
Gitea Container Registry. The Gitea Actions runner does not have
access to GitHub's GITHUB_TOKEN for ghcr.io authentication.
Based on fix/ci-registry-auth branch pattern (a582bd0).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The 'error' useState was declared but never read — only setError was called.
Now renders the error message as a red text node when the fetch fails.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sections 5.20, 5.21, and 5.22 added to UAT_PLAYBOOK.md:
- TC-WEB-5.20.x: Buffer rules CRUD, validation, empty state (14 cases)
- TC-WEB-5.21.x: Service default buffer field in table and form (7 cases)
- TC-WEB-5.22.x: Pet size category and coat type in portal PetForm (7 cases)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit ports the GRO-1173 admin UI changes from the app monorepo
into the extracted groombook/web repo, using the correct source paths
(src/ instead of apps/web/src/):
- New BufferRulesSection component (full CRUD UI for /api/buffer-rules)
- Default Buffer (minutes) field added to service create/edit form
- Size Category and Coat Type dropdowns added to PetForm (portal)
- @groombook/types Service interface extended with defaultBufferMinutes
- BufferRulesSection embedded in Settings page
The PetForm already had coatType — this commit adds petSizeCategory
and renders both fields with proper dropdown selectors.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Move CI workflow from .github/workflows/ to .gitea/workflows/
- Add uat branch to CI triggers (push and pull_request)
- Fix Dockerfile HEALTHCHECK to use wget instead of curl
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fix TypeScript error on line 114: HTMLElement | undefined is not
assignable to Element. Added ! assertion since length guard already
excludes the empty-array case.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Remove unused 'X' import from lucide-react in PetProfiles.tsx
- Delete 10 corrupted demo-pet PNG files that contain Alibaba AccessDenied XML
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(ci): use Gitea registry for Docker push (#9)
GRO-1348
- Change Docker login from ghcr.io/GITHUB_TOKEN to git.farh.net/REGISTRY_TOKEN
- Update image tags from ghcr.io/groombook/web to git.farh.net/groombook/web
- Replace GitHub Actions cache with registry cache
- Make coatType and petSizeCategory optional on Pet (?:) — they may not be set
- Remove "single" and "short" from COAT_TYPES (not in CoatType union)
- Use { name: "Add" } instead of /add/i to target the + button specifically
- Add optional chaining to puppyCutSpans[0]?.closest() (noUncheckedIndexedAccess)
- Add optional chaining to petsData[0]?.id ?? "" in PetProfiles
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>