* feat: add customer-facing appointment notes (GRO-106)
- Migration 0014: add customer_notes column to appointments
- Schema update: add customerNotes field to appointments table
- Factory update: include customerNotes in buildAppointment
- Portal route: PATCH /api/portal/appointments/:id/notes
- Ownership validation via impersonation session
- Future-only validation (no edits after start)
- 500 character limit
- Register portal router in index.ts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* Fix confirmationToken leak and add unit tests for portal notes endpoint
- Return only id, customerNotes, updatedAt instead of full appointment row
- Add comprehensive unit tests covering auth, ownership, time-gating, and validation
- Fix: confirmationToken no longer returned to portal session
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat: add customer notes UI to portal and staff views (GRO-178)
- Add customerNotes field to Appointment type
- Add read-only customer notes display in staff appointment detail modal
- Add customer notes textarea with save, char counter (500 max), and disabled state
- Wire up PATCH /api/portal/appointments/:id/notes in portal UI
- Update mockData with customerNotes field
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix: address QA review feedback - null check and portal route auth
- Add null check after db.update().returning() in portal notes endpoint
- Move portal router registration before auth middleware so clients can access it
- Remove unused ENDED_SESSION variable from test file
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(portal): address QA review - isUpcoming time parsing and session header
- Fixed parseTimeTo24Hour to handle 12-hour AM/PM format correctly
- Added X-Impersonation-Session-Id header to CustomerNotesSection fetch
- Added comprehensive tests for CustomerNotesSection and time parsing
- Fixed TypeScript strict null checks for parseTimeTo24Hour
Fixes QA review issues:
- isUpcoming() now correctly parses 12-hour time format
- CustomerNotesSection sends session ID header for auth
- Added unit tests for new UI component
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix: thread sessionId as prop instead of sessionStorage
CustomerNotesSection was reading sessionStorage for the impersonation
session ID, but CustomerPortal stores it in React state. Pass sessionId
as a prop through AppointmentsSection and AppointmentCard instead.
Also update tests to pass sessionId prop and add test for null sessionId
case.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Scrubs McBarkley <scrubs@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: groombook-cto[bot] <269737991+groombook-cto[bot]@users.noreply.github.com>
Extend CI to build PR-tagged Docker images and auto-deploy them to
groombook-dev when all checks pass. This unblocks Flea Flicker UAT
validation for open PRs.
Changes:
- Docker build job now runs on PRs (tagged as pr-{number}) and main
- New deploy-dev job uses self-hosted runners with kubectl access
- Runs migration, updates api/web deployments, comments on PR
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Convert mobile-groomers.html from Markdown to proper HTML
- Fix "Client/p pet profiles" typo → "Client/pet profiles"
- Add missing trailing newlines to all files
- Remove duplicate "Back to Home" link in getting-started/index.html
- Fix link paths: use relative URLs instead of /groombook/docs/ absolute paths
- Remove placeholder testimonials with fake names
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add landing page, blog posts (launch announcement, why we built),
getting-started guide, mobile groomers page, and Jekyll config
to docs/ folder for GitHub Pages publishing.
Closesgroombook/groombook#168
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Fixes ESLint @typescript-eslint/no-unused-vars error in CI.
baseUrl was declared but never used; confirmUrl/cancelUrl correctly
use apiUrl instead.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add customer confirmation/cancellation flow for appointments:
- DB migration (0013): add confirmation_status, confirmed_at, cancelled_at,
confirmation_token to appointments table with index on token column
- schema.ts + factories.ts + types: expose new columns and ConfirmationStatus type
- GET /api/book/confirm/:token — tokenized confirm via email link (redirects)
- GET /api/book/cancel/:token — tokenized cancel via email link, single-use token
- POST /api/appointments/:id/confirm — portal/staff confirm endpoint
- POST /api/appointments/:id/cancel — portal/staff cancel endpoint
- Reminder emails now include Confirm/Cancel CTA buttons with tokenized links
- Reminder service generates confirmation token if missing before sending
- Staff calendar shows confirmation status indicator on appointment cards
and in the detail modal (confirmed ✓ / customer cancelled ✗)
- /booking/confirmed, /booking/cancelled, /booking/error redirect pages
- 23 new unit tests covering all new endpoints and edge cases
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Fix route mismatch: mock /api/impersonation/sessions/session-1 (plural)
- Navigate to /?sessionId=session-1 so CustomerPortal fetches session
- Replace .bg-amber-500 with data-testid="impersonation-banner"
- Remove waitForTimeout(1100), use proper waitFor
- Fix locale-dependent time regex in "banner shows started time" test
- Fix loading state race by waiting for response before fulfilling
- Add data-testid to ImpersonationBanner component
- Add trailing newlines to both spec files
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Remove unused makeSelectChain function from search.test.ts (lint blocker)
- Fix handleClientClick/handlePetClick to navigate to /admin/clients?highlight={id}
so the target client is identified in the URL rather than silently ignored
- Add console.warn for fetch errors in GlobalSearch instead of swallowing silently
Auth middleware verified: searchRouter is registered on the api Hono instance
which applies authMiddleware + resolveStaffMiddleware globally — no coverage gap.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Backend:
- GET /api/search?q={query} — returns up to 10 matching active clients and 10
matching pets in a single request; clients matched on name/email/phone,
pets matched on name/breed with owner name included
- Special chars (%, _, \) escaped before ILIKE to prevent injection/accidents
- Disabled clients excluded; pets from disabled client owners excluded via JOIN filter
- Route registered under protected API (auth + RBAC middleware applies automatically)
- Export `ilike` from @groombook/db alongside existing drizzle-orm helpers
Frontend:
- GlobalSearch component in sticky admin header: debounced input (300ms),
grouped dropdown (Clients / Pets sections), loading/empty states
- Client results show name + phone; pet results show name, breed, owner name
- Touch-friendly: 44px input height, 48px min row height, full-width dropdown
- Outside-click closes dropdown; selecting a result navigates to /admin/clients
Tests (apps/api/src/__tests__/search.test.ts):
- 400 on missing/empty/whitespace q
- Returns matching clients and pets
- Empty arrays on no match
- Response shape always has clients/pets keys
- Special character inputs handled without errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix gradient regression in ReportCards.tsx: use distinct color stops
(--color-accent-lighter → --color-accent-light) to restore subtle gradient
- Fix BrandingContext meta tag accumulation: cache ref with useRef instead
of querying DOM on every render to prevent duplicate elements on remount
- Add BrandingContext.test.tsx: verify CSS vars applied, theme-color meta
created/updated, and no duplicate meta tags on rerender
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DB migration 0012: add photo_key and photo_uploaded_at columns to pets table
- S3 client utility (apps/api/src/lib/s3.ts): presigned PUT/GET, delete via Rook-Ceph RGW
- API photo routes on petsRouter:
- POST /:petId/photo/upload-url — returns presigned PUT URL + object key
- POST /:petId/photo/confirm — records key in DB after successful upload
- DELETE /:petId/photo — deletes from storage and clears DB
- GET /:petId/photo — returns presigned GET URL
- RBAC: all staff roles (manager, receptionist, groomer) may upload/delete photos;
restructured index.ts guards so groomer-accessible photo paths don't overlap
with the manager/receptionist-only general pets write guard
- Frontend PetPhotoDisplay: responsive image with shimmer skeleton and paw placeholder
- Frontend PetPhotoUpload: client-side resize to max 1200px, XHR with progress,
presigned PUT flow — binary data never passes through the API server
- Wired both components into Clients.tsx staff portal pet cards
- Unit tests: 14 test cases covering all four routes (happy path + error cases)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Replace all hardcoded brand color hex values with CSS custom properties
so BrandingContext drives both the customer portal and staff site.
- index.css: add derived accent/primary vars using color-mix()
(--color-accent-hover, --color-accent-dark, --color-accent-light,
--color-accent-lighter, --color-primary-dark); fix focus ring styles
to use var(--color-primary) instead of hardcoded hex
- BrandingContext.tsx: also update <meta name="theme-color"> in sync
with primaryColor so PWA theme-color tracks branding at runtime
- portal/sections: replace bg-[#8b7355], text-[#6b5a42], bg-[#f0ebe4],
bg-[#faf5ef], hover:bg-[#7a6549] etc. with Tailwind v4 CSS var
utilities (bg-(--color-accent), text-(--color-accent-dark), etc.)
- pages: replace inline style "#4f8a6f"/"#3d7a5f" with
var(--color-primary) / var(--color-primary-dark) across Appointments,
Book, Clients, GroupBooking, Invoices, Reports, Services, Staff, and
DevSessionIndicator
Closes#91
Co-Authored-By: Paperclip <noreply@paperclip.ing>
vitest was erroneously added to the packages/db importer in
pnpm-lock.yaml during factory test setup, but packages/db/package.json
does not declare vitest as a dependency. This caused CI to fail with
ERR_PNPM_OUTDATED_LOCKFILE.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- apps/e2e/tests/login.spec.ts: 8 tests for DevLoginSelector page
- renders staff and clients sections
- shows loading state
- displays staff with role/email, clients with pet count
- clicking staff navigates to /admin with dev-user stored
- clicking client navigates to / with dev-user stored
- skip login removes dev-user and navigates to /admin
- handles empty users response
- apps/e2e/tests/impersonation.spec.ts: 8 tests for ImpersonationBanner
- banner displays when session is active
- shows reason and started time
- End Session and Audit buttons visible
- clicking End Session calls API and hides banner
- Extend button appears when time < 5 mins and not extended
- URL is cleaned when session ends
- apps/e2e/tests/fixtures.ts: added /api/dev/users mock for login tests
Tests cover resetFactoryCounters(), counter determinism, override
merging, and compile-time enforcement of required fields on
buildAppointment. All 16 new tests pass (92 total).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add three indexes to prevent full table scans as session volume grows:
- impersonation_sessions(staff_id, status) for active-session lookup
- impersonation_sessions(client_id) for existing-session check
- impersonation_audit_logs(session_id) for audit log lookup by session
Migration 0011 applied and verified on dev database.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Phase 1 — Seed Hardening:
- Replace all Math.random() calls in seed.ts with a Mulberry32 seeded PRNG
(seed 42) so the same data set is reproduced on every run
- Replace crypto.randomUUID() with a PRNG-based UUID v4 generator
- Add manager (Jordan Lee) and receptionist (Sam Rivera) staff members
to seed — previously all staff were groomers
- New packages/db/src/reset.ts drops all tables/enums and re-runs
migrate + seed; exposed as `pnpm db:reset` at root
- Generate migration 0010_impersonation_sessions.sql for the
impersonation_sessions and impersonation_audit_logs tables that were
already in schema.ts but had no corresponding migration
Phase 2 — Test Factories:
- New packages/db/src/factories.ts with buildStaff, buildClient, buildPet,
buildService, buildAppointment and resetFactoryCounters helpers
- Exported via @groombook/db/factories subpath (package.json + vitest alias)
- impersonation.test.ts updated to use buildStaff instead of hand-rolled
fixture objects
Closes#90 (Phases 1 + 2)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Use StaffRow type for all staff fixture objects so groomer/receptionist
variants don't cause type errors. Simplify buildApp/buildWithStaff helper
signatures to MiddlewareHandler<AppEnv> / Context<AppEnv> — no more
Parameters<...> inference gymnastics.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- New `apps/api/src/middleware/rbac.ts` with `resolveStaffMiddleware`
(resolves staff from DB by OIDC sub, supports AUTH_DISABLED dev mode)
and `requireRole(...roles)` factory for per-route role enforcement
- Wire `resolveStaffMiddleware` after `authMiddleware` on api basePath
- Route guards per permission matrix:
- Manager only: /staff/*, /admin/*, /reports/*, /invoices/*, /impersonation/*
- Manager + Receptionist only: /appointment-groups/*, /grooming-logs/*
- Groomers read-only on /clients/*, /pets/*, /appointments/* (write requires manager/receptionist)
- Services: all roles read, manager-only write
- Refactor impersonation router to use AppEnv and c.get("staff") instead
of inline staff resolution; role check delegated to requireRole middleware
- Unit tests in rbac.test.ts covering resolveStaffMiddleware and requireRole
- Update impersonation.test.ts to inject staff directly via context
Closes#88 (Phase 1)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Import ImpersonationSession from @groombook/types (component was updated in #78)
- Remove stale tests: "shows customer name" and "returns null when inactive"
(component no longer renders customer name or checks session.active)
- Add isExtended prop to all render calls (component now takes isExtended as prop)
- Fix "does not show Extend button when already extended" to pass isExtended={true}
instead of session.extended (prop was extracted from session in #78)
- Fix clients.test.ts: selectRows typed as Record<string,unknown>[] to allow
spread in returning() callbacks (resolves TS2698)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Staff impersonation mode shipped in v2026.320 — managers can now view
the customer portal as any client, with a live countdown banner, extend/end
controls, and a full audit log. Update the feature list so new visitors
and potential adopters see the capability.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Closes#81
- Add window.location.href = '/admin/clients' after clearing session
state in handleEnd so staff are sent back to the admin panel
- Add a test that verifies the redirect fires when End Session is clicked
Co-Authored-By: Paperclip <noreply@paperclip.ing>