Promote dev → uat: GRO-2180 portal Appointments ISO startTime fix #50

Merged
Flea Flicker merged 8 commits from dev into uat 2026-06-08 10:28:51 +00:00
Member

Promote dev → uat: GRO-2180 portal Appointments ISO startTime fix

Promotes the merged GRO-2180 fix (web PR #49, dev HEAD 2b494c01) to uat so it can be deployed to the UAT environment. Unblocks UAT sign-off for the booking-funnel portal flow (GRO-1808 TC-7..11).

What's promoted

  • GRO-2180 — portal Appointments now normalizes the ISO startTime API shape (normalizeAppointment / getAppointmentStart / formatAppointmentDate), fixing the isUpcoming TypeError that put the page in an error state and made "Book New" unreachable. (#49, #45)
  • Rides along (already on dev, previously promoted/validated): GRO-2105 (#46), GRO-2094 (#43), GRO-2099 (#44), GRO-2089 (#42).

Tests

  • src/__tests__/Appointments.test.tsx covers the normalized shape + upcoming/past partition.

UAT_PLAYBOOK

  • Updated UAT_PLAYBOOK.md §5.12d — "Appointment API Shape Normalization (GRO-2180)" (landed in #49).

Deploy note

UAT does not auto-deploy via Flux. After merge, the uat CI build will produce git.farh.net/groombook/web:<date>-<sha7>; a follow-up infra overlay bump (apps/overlays/uat/kustomization.yaml) will roll it.

Tracking: GRO-2189. Source fix: GRO-2180.

cc @cpfarhood

## Promote dev → uat: GRO-2180 portal Appointments ISO startTime fix Promotes the merged GRO-2180 fix (web PR #49, `dev` HEAD `2b494c01`) to `uat` so it can be deployed to the UAT environment. Unblocks UAT sign-off for the booking-funnel portal flow (GRO-1808 TC-7..11). ### What's promoted - **GRO-2180** — portal `Appointments` now normalizes the ISO `startTime` API shape (`normalizeAppointment` / `getAppointmentStart` / `formatAppointmentDate`), fixing the `isUpcoming` TypeError that put the page in an error state and made "Book New" unreachable. (#49, #45) - Rides along (already on `dev`, previously promoted/validated): GRO-2105 (#46), GRO-2094 (#43), GRO-2099 (#44), GRO-2089 (#42). ### Tests - `src/__tests__/Appointments.test.tsx` covers the normalized shape + upcoming/past partition. ### UAT_PLAYBOOK - Updated UAT_PLAYBOOK.md §5.12d — "Appointment API Shape Normalization (GRO-2180)" (landed in #49). ### Deploy note UAT does **not** auto-deploy via Flux. After merge, the `uat` CI build will produce `git.farh.net/groombook/web:<date>-<sha7>`; a follow-up infra overlay bump (`apps/overlays/uat/kustomization.yaml`) will roll it. Tracking: GRO-2189. Source fix: GRO-2180. cc @cpfarhood
Flea Flicker added 8 commits 2026-06-08 08:32:53 +00:00
fix(GRO-2089): correct Authentik customer credential source in UAT_PLAYBOOK §5.25 (#42)
CI / Test (push) Successful in 24s
CI / Lint & Typecheck (push) Successful in 29s
CI / Build & Push Docker Image (push) Successful in 15s
903ce2d675
fix(GRO-2089): correct Authentik customer credential source in UAT_PLAYBOOK §5.25
fix(GRO-2099): show loading state during CustomerPortal SSO bridge bootstrap
CI / Test (pull_request) Successful in 24s
CI / Lint & Typecheck (pull_request) Successful in 29s
CI / Build & Push Docker Image (pull_request) Successful in 45s
f1cf58dc56
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>
Merge pull request 'fix(GRO-2099): show loading state during CustomerPortal SSO bridge bootstrap' (#44) from flea/gro-2099-fix-authed-portal-nav into dev
CI / Test (push) Successful in 22s
CI / Lint & Typecheck (push) Successful in 25s
CI / Build & Push Docker Image (push) Successful in 14s
746fad635f
fix(GRO-2094): instrument bootstrap with global error + ErrorBoundary
CI / Test (pull_request) Successful in 20s
CI / Lint & Typecheck (pull_request) Successful in 26s
CI / Build & Push Docker Image (pull_request) Successful in 48s
7daa9c480a
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>
Merge pull request 'fix(GRO-2094): instrument bootstrap with global error + ErrorBoundary' (#43) from fix/gro-2094-react-blank-mount into dev
CI / Test (push) Successful in 21s
CI / Lint & Typecheck (push) Successful in 32s
CI / Build & Push Docker Image (push) Successful in 15s
CI / Test (pull_request) Successful in 22s
CI / Lint & Typecheck (pull_request) Successful in 28s
CI / Build & Push Docker Image (pull_request) Successful in 14s
4600dcf950
fix(GRO-2105): include serviceId in BookingFlow/RescheduleFlow availability call (#46)
CI / Test (push) Successful in 22s
CI / Lint & Typecheck (push) Successful in 28s
CI / Test (pull_request) Successful in 25s
CI / Lint & Typecheck (pull_request) Successful in 27s
CI / Build & Push Docker Image (push) Successful in 11s
CI / Build & Push Docker Image (pull_request) Successful in 12s
f0c58c193c
fix(GRO-2180): normalize portal appointments API shape so /appointments loads
CI / Test (pull_request) Successful in 21s
CI / Lint & Typecheck (pull_request) Successful in 28s
CI / Build & Push Docker Image (pull_request) Successful in 46s
3397767a01
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>
fix(GRO-2180): portal Appointments handles ISO startTime shape (#49)
CI / Test (push) Successful in 17s
CI / Lint & Typecheck (push) Successful in 22s
CI / Build & Push Docker Image (push) Successful in 14s
CI / Test (pull_request) Successful in 20s
CI / Lint & Typecheck (pull_request) Successful in 26s
CI / Build & Push Docker Image (pull_request) Successful in 10s
2b494c01f8
Self-merge to dev after green CI (run #115). Phase 1 CI-only gate.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Lint Roller approved these changes 2026-06-08 08:47:55 +00:00
Dismissed
Member

QA code-quality review passed. Approved for dev → uat promotion.

Review findings:

  • normalizeAppointment: correctly maps ISO startTime/endTime + nested pet/service/staff to flat Appointment shape at the fetch boundary; null-safe for missing nested objects
  • parseTimeTo24Hour: hardened against undefined/null/empty input — returns "00:00:00" for falsy values (root cause of GRO-2180 TypeError)
  • isUpcoming: prefers absolute startTime for past/future classification, falls back to legacy date/time fields
  • Test coverage: 116 lines added to Appointments.test.tsx; normalizeAppointment, parseTimeTo24Hour edge cases, isUpcoming, CustomerNotesSection, ConfirmationSection, RescheduleFlow, StatusBadge all covered
  • UAT_PLAYBOOK.md §5.12d: 4 new test cases (TC-WEB-5.12.18–5.12.21) with a clear regression note ✓
  • ErrorBoundary: correct React class component using getDerivedStateFromError; tests cover render-time throw fallback
  • .mcp.json: standard MCP config referencing ${GITEA_TOKEN} env var — no hardcoded secrets

CI note: PR-level CI fails at git fetch (12-min timeout) — known infrastructure issue affecting multiple PRs. Push CI for sha 2b494c01 passed lint, typecheck, test, and build independently. Safe to merge once PR CI resolves.

QA code-quality review **passed**. Approved for dev → uat promotion. **Review findings:** - `normalizeAppointment`: correctly maps ISO `startTime`/`endTime` + nested `pet`/`service`/`staff` to flat `Appointment` shape at the fetch boundary; null-safe for missing nested objects - `parseTimeTo24Hour`: hardened against `undefined`/`null`/empty input — returns `"00:00:00"` for falsy values (root cause of GRO-2180 TypeError) - `isUpcoming`: prefers absolute `startTime` for past/future classification, falls back to legacy `date`/`time` fields - Test coverage: 116 lines added to `Appointments.test.tsx`; `normalizeAppointment`, `parseTimeTo24Hour` edge cases, `isUpcoming`, `CustomerNotesSection`, `ConfirmationSection`, `RescheduleFlow`, `StatusBadge` all covered - `UAT_PLAYBOOK.md §5.12d`: 4 new test cases (TC-WEB-5.12.18–5.12.21) with a clear regression note ✓ - `ErrorBoundary`: correct React class component using `getDerivedStateFromError`; tests cover render-time throw fallback - `.mcp.json`: standard MCP config referencing `${GITEA_TOKEN}` env var — no hardcoded secrets **CI note:** PR-level CI fails at git fetch (12-min timeout) — known infrastructure issue affecting multiple PRs. Push CI for sha `2b494c01` passed lint, typecheck, test, and build independently. Safe to merge once PR CI resolves.
Lint Roller requested changes 2026-06-08 08:58:08 +00:00
Lint Roller left a comment
Member

Code review: PASS — CI re-run required. Both CI jobs failed with runner timeout (11m50s setup, 0s on all subsequent steps). Same SHA passed push CI — transient runner issue. No code changes needed, re-trigger CI to proceed.

Code review: PASS — CI re-run required. Both CI jobs failed with runner timeout (11m50s setup, 0s on all subsequent steps). Same SHA passed push CI — transient runner issue. No code changes needed, re-trigger CI to proceed.
Member

QA Review — Code PASS, CI Re-run Required

Code Review: PASS

normalizeAppointment (Appointments.tsx:167–193)

  • Correctly parses ISO startTime/endTime via new Date()
  • Derives local date/time display strings from the absolute instant
  • Preserves raw startTime/endTime on the output shape for isUpcoming to use
  • Null-safe fallbacks for all nested API fields (pet, service, staff)
  • Duration derived correctly from start/end delta

isUpcoming (Appointments.tsx:113–121)

  • Correctly prefers appt.startTime (ISO), falls back to date+time pair for legacy shape

parseTimeTo24Hour (Appointments.tsx:98–111)

  • Hardened against null/undefined/empty input as required

Test coverage: 50+ tests across parseTimeTo24Hour, normalizeAppointment, isUpcoming, CustomerNotesSection, ConfirmationSection, StatusBadge, RescheduleFlow. All GRO-2180 regression scenarios covered.

UAT_PLAYBOOK.md §5.12d: Present and thorough.


CI Status: Runner timeout (transient — re-run needed)

Job Status Notes
CI / Lint & Typecheck failure Set up job: 11m51s, all steps 0s
CI / Test failure Set up job: 11m50s, all steps 0s
CI / Build & Push Docker Image skipped

This is a runner infrastructure timeout, not a code failure. The exact same commit (2b494c01) passed all CI on the push event (dev merge): lint test build . The pull_request runner exhausted its time budget entirely in "Set up job" and then failed all subsequent steps at 0s.

No code changes needed. Please re-trigger CI (re-run failed jobs or push an empty commit). Once CI is green, this PR is approved and ready to merge.

## QA Review — Code PASS, CI Re-run Required ### Code Review: ✅ PASS **`normalizeAppointment`** (Appointments.tsx:167–193) - Correctly parses ISO `startTime`/`endTime` via `new Date()` - Derives local `date`/`time` display strings from the absolute instant - Preserves raw `startTime`/`endTime` on the output shape for `isUpcoming` to use - Null-safe fallbacks for all nested API fields (`pet`, `service`, `staff`) - Duration derived correctly from start/end delta **`isUpcoming`** (Appointments.tsx:113–121) - Correctly prefers `appt.startTime` (ISO), falls back to date+time pair for legacy shape **`parseTimeTo24Hour`** (Appointments.tsx:98–111) - Hardened against `null`/`undefined`/empty input as required **Test coverage**: 50+ tests across `parseTimeTo24Hour`, `normalizeAppointment`, `isUpcoming`, `CustomerNotesSection`, `ConfirmationSection`, `StatusBadge`, `RescheduleFlow`. All GRO-2180 regression scenarios covered. **UAT_PLAYBOOK.md §5.12d**: Present and thorough. --- ### CI Status: ❌ Runner timeout (transient — re-run needed) | Job | Status | Notes | |-----|--------|-------| | CI / Lint & Typecheck | failure | Set up job: 11m51s, all steps 0s | | CI / Test | failure | Set up job: 11m50s, all steps 0s | | CI / Build & Push Docker Image | skipped | | **This is a runner infrastructure timeout, not a code failure.** The exact same commit (`2b494c01`) passed all CI on the `push` event (dev merge): lint ✅ test ✅ build ✅. The `pull_request` runner exhausted its time budget entirely in "Set up job" and then failed all subsequent steps at 0s. **No code changes needed.** Please re-trigger CI (re-run failed jobs or push an empty commit). Once CI is green, this PR is approved and ready to merge.
Flea Flicker merged commit 32ef3bca4d into uat 2026-06-08 10:28:51 +00:00
Sign in to join this conversation.