Dashboard.tsx is at portal/sections/ (2 levels deep from src/),
so the import path needs ../../pages/ not ../pages/.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
TS2307: Cannot find module "../pages/DevLoginSelector.js"
The source file is .tsx, not .ts/js. Fixes typecheck failure in CI.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
When a client clicks "Abigail Brooks" in the dev login selector,
POST /api/portal/dev-session returns 201 but the session may not have
id set immediately (timing issue or API response). This caused both
CustomerPortal and Dashboard to redirect to /login because session?.id
was null.
Changes:
- CustomerPortal: don't redirect to /login for client dev users even
if session is null — the dev-session flow has verified the user
- Dashboard: check for dev user before redirecting when sessionId is null
This ensures client dev users see the portal rather than being
immediately redirected back to /login.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The sessionAttempted state was removed from the redirect condition
(commit df32509) but its declaration and setter calls were left
behind, causing a TypeScript/ESLint unused-variable error.
Removed:
- sessionAttempted useState declaration
- All 4 setSessionAttempted(true) calls
- Stale comment referencing sessionAttempted in redirect block
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The serviceIds array is referenced by later appointment creation code.
Restore it inside the services loop.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Replace random uuid() for service IDs with pre-assigned deterministic
UUIDs (b0000001-0000-0000-0000-...) so that ON CONFLICT DO UPDATE
correctly targets the id column and prevents duplicate inserts.
Also add a one-time deduplication query before inserting that removes
any existing duplicate service rows (keeps lowest id per name), which
cleans up the current deployed database that already has duplicates.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
When navigating to /?sessionId=xxx, Dashboard would immediately
redirect to /login because sessionId was null before the fetch
completed. The impersonation banner never rendered.
Add isImpersonating state: true while impersonation fetch is in-flight,
prevents Dashboard from redirecting until session loads.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- CustomerPortal.tsx: fix stray } in base64 data URL src attribute
- Dashboard.tsx: restore Navigate to /login for !sessionId (defense-in-depth)
The stray } was introduced in commit fa92a65 which also reverted
the Dashboard redirect. This commit restores both fixes.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Dashboard had a defense-in-depth Navigate to /login when sessionId is
null. This fires on initial render before the session is set, causing
E2E tests to fail (they wait for the impersonation banner which never
renders because Dashboard redirected away).
Revert to main-branch behavior: show "Please sign in" message instead
of redirecting. The CustomerPortal-level redirect is sufficient.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes E2E race condition where setSession and setInitComplete are batched
by React concurrent rendering, causing redirect to fire before session
is set. The sessionAttempted flag tracks "did we try" so redirect only
fires when there was NO attempt, not when the state update is pending.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The portal/me endpoint is only called in the client dev user flow
(devUser.type === 'client'), NOT in the impersonation flow which uses
the sessionId param. Removing this mock eliminates potential interference.
The POST /api/portal/dev-session mock is dead code in impersonation tests
since the fixture seeds devUser.type=staff, which skips that code path.
Removed it to eliminate potential interference.
Also changed portal/me mock pattern from 'GET **/api/portal/me' to
'**/api/portal/me**' to ensure it matches correctly regardless of
how Playwright interprets the URL pattern syntax.
The API returns a flat ImpersonationSession object. CustomerPortal.tsx
reads s.id directly from the response. My previous fix incorrectly
wrapped the mock in { session: {...} }, causing s.id to be undefined
and setSession() to never fire.
This reverts the mock structure to be flat, matching the actual API
response format from portal.ts line 516.
The mock returned { id, client } but CustomerPortal.tsx expects
{ session: { id, client } }. This caused setSession to never be called,
leading to redirect to /login and test timeouts.
Also seed dev user in localStorage for impersonation tests to
ensure getDevUser() returns a known state.
QA identified that impersonation.spec.ts mocks impersonation
session endpoints but not portal session endpoints. When
CustomerPortal.tsx validates the session it calls GET /api/portal/me
which fails without a mock, causing the redirect to fire and tests
to fail.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- CustomerPortal.tsx: add initComplete state to track async session
initialization. After init completes with no valid session, redirect:
staff dev users → /admin, all others → /login
- Dashboard.tsx: change !sessionId fallback from dead-end UI to
<Navigate to="/login" replace /> (defense-in-depth)
- All 85 web unit tests pass
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The hardcoded DEV_STAFF_ID (all zeros) did not exist in the staff
table, causing a foreign-key violation and 500 error. Now falls back
to the demo-manager (KNOWN_STAFF_ID from seed) or any active staff
record instead.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Adds pwa-192x192.png, pwa-512x512.png, and favicon.svg to the web
public directory. These are referenced by the VitePWA plugin manifest
and were causing 404 errors on every page load.
cc @cpfarhood
Co-Authored-By: Paperclip <noreply@paperclip.ing>
When a client user selects their account from the dev login selector,
the portal previously had no way to establish an authenticated session —
it only checked for a ?sessionId= URL param (used by the real staff
impersonation flow). This caused the portal to always show "Hi, Guest".
Changes:
- POST /api/portal/dev-session: new endpoint (auth-disabled only) that
creates an impersonation session for a given clientId, using a fixed
dev staff ID to avoid conflicts with the one-active-session-per-staff
rule in the real impersonation flow. Sessions are long-lived (24h).
- CustomerPortal: on mount, after checking for ?sessionId=, also check
for a dev client user in localStorage and call /api/portal/dev-session
to obtain a session. This mirrors the real impersonation flow so all
existing portal API calls (which require X-Impersonation-Session-Id)
work without modification.
cc @cpfarhood
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add active=true filter to all 3 superUserCount queries in staff.ts
(revoke, deactivate, delete) so inactive super users aren't counted,
preventing false positives when checking the last-super-user guardrail.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Fixes seed script crash — both onConflictDoUpdate calls on clients table now use schema.clients.id (PK) as conflict target instead of non-unique schema.clients.email. Email added to set clause for both call sites.
Resolves GRO-298. Unblocks GRO-290, GRO-295, GRO-297.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Before: ~5% probabilistic pending invoices meant UAT couldn't reliably
find billing test data. Shedward was blocked from testing Pay Now flows.
After: deterministic 5 UAT clients (uat-alpha through uat-echo) each get
a completed appointment + pending invoice on every seed run. Client
names and emails documented in Shedward AGENTS.md for direct access.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Backend:
- PATCH /api/staff/:id now accepts optional isSuperUser field
- Only super users can change isSuperUser (403 otherwise)
- Revoke (isSuperUser=false) blocked if target is last super user (400)
- Deactivate (active=false) blocked if target is last super user (400)
- DELETE /:id blocked if target is last super user (400)
- New GET /api/staff/me returns current authenticated staff record
Frontend (Staff.tsx):
- Super User column in staff table with badge indicator
- Grant/Revoke SU button visible only to super users
- Last-super-user guardrail disables revoke button with tooltip
- API errors shown inline below table header
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Each CI build now produces an immutable tag (pr-N-sha7 or
YYYY.MM.DD-sha7) so that docker/build-push-action cache-from
type=gha cannot cross-contaminate between commits.
Previously the shared pr-N tag caused GHA layer cache to reuse
stale JS bundles from earlier builds of the same PR.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GitHub App token pushes do not trigger pull_request workflow events,
blocking CI on bot-authored PRs. Add workflow_dispatch to allow manual
CI runs via: gh workflow run ci.yml --ref <branch>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit also includes GRO-287 fixes:
- PasswordChange: add stateful form with password-match validation
- ReportCards: replace window.location.reload() with refetch via useRef
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Remove readOnly guard from Pay Now button and PaymentModal in BillingPayments.
The readOnly guard was too broad — it hid the Pay Now button during staff
impersonation sessions, making it impossible for staff to collect payments.
Other readOnly guards (Remove payment method, Autopay toggle) remain intact.
Co-authored-by: groombook-engineer[bot] <269742240+groombook-engineer[bot]@users.noreply.github.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Fixes GRO-261 — billing portal session header mismatch and response format bug.
- x-session-id → X-Impersonation-Session-Id in BillingPayments.tsx and Dashboard.tsx
- Handle bare array response from /api/portal/invoices
Co-Authored-By: Paperclip <noreply@paperclip.ing>
cc @cpfarhood
Unauthenticated users saw a blank screen because the needsSetup null-guard
fired before the LoginPage render check. needsSetup stays null for
unauthenticated users since the setup-check effect early-returns when
!session. Now the login check runs first so users see the login page.
Co-authored-by: Flea Flicker <flea-flicker@groombook.io>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Scrubs McBarkley (CEO) <ceo-bot@groombook.farh.net>
Closes GRO-261 — Pay Now button on Billing page now opens a payment modal with invoice selection and simulated payment flow.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(portal): wire Rebook Now button to navigate to booking wizard (GRO-265)
The "Rebook Now" button on the Report Card detail view had no click
handler. Now navigates to /admin/book with pet info pre-filled via URL
params (petName, serviceName). Button text changed from "Book Now" to
"Rebook Now" per the bug report.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(book): pre-fill form from URL params to ensure React state is set
Add useSearchParams to read URL parameters (e.g., ?clientName=Jane)
and sync them to the BookingBody state on mount via useEffect.
This ensures validation checks React state, not empty initial state.
Fixes GRO-255
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(book): add inline validation for date input format (GRO-266)
Date picker now shows a clear error when the value doesn't match
YYYY-MM-DD, instead of silently failing with a browser console warning.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(portal): wire Rebook Now button + clean .js artifacts (GRO-265)
Cherry-picked from contaminated PR #160:
- ReportCards.tsx: Rebook Now button navigates to /admin/book with pet info
- Book.tsx: pre-fill form from URL params (GRO-255)
- Book.tsx: inline date validation (GRO-266)
Also removes compiled .js artifacts (Book.js, ReportCards.js)
that were incorrectly committed.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: groombook-ci[bot] <ci@groombook.bot>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Merges PR #158 — fixes critical production login bug.
- Adds apps/web/.env.production with VITE_API_URL= (empty)
- Prevents localhost:3000 from being baked into the prod bundle
- Auth client now uses relative URLs through the gateway
GRO-258 | QA: Lint Roller ✓ | CTO: The Dogfather ✓
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Manual workflow_dispatch trigger to promote a tested image tag
to production by creating an infra PR. No auto-merge — UAT sign-off
required before prod deploy.
Co-authored-by: groombook-ci[bot] <ci@groombook.bot>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: groombook-ceo[bot] <269735724+groombook-ceo[bot]@users.noreply.github.com>
Prevents localhost:3000 from being baked into the production bundle.
Vite automatically loads .env.production for prod builds, which
with VITE_API_URL= explicitly sets the var to empty string so
auth-client.ts uses relative URLs (?? "" fallback).
Co-Authored-By: Paperclip <noreply@paperclip.ing>