Add POST /api/admin/seed — a manager-only API endpoint that creates
minimal known users (Demo Manager staff + Demo Client + Demo Dog + basic
services) via the API instead of direct DB writes.
Add SEED_KNOWN_USERS_ONLY=true env var to seed.ts for lean prod/demo
seeding. Known users get deterministic UUIDs so seeding is idempotent.
Note: infra changes (disabling AUTH_DISABLED in prod/demo, fixing the
failing seed job) require updates to groombook/infra repo.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Now that portal clients are restricted to status:"cancelled" only,
update the PATCH /portal/waitlist/:id tests to send a valid value
so auth and ownership checks are exercised correctly.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Restrict portal PATCH waitlist status to z.literal("cancelled") only
- Appointment notes: field projection + null check from PR #109
- Resolve index.ts conflict: keep both portal and calendar public routes
- Resolve portal.ts conflict: keep min(1) validation for customerNotes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Move client-facing POST/PATCH/DELETE waitlist routes to portalRouter
so impersonation sessions can reach them (were blocked by requireRole guard)
- Fix portalRouter double-mount: remove from auth-protected api block,
register publicly at app.route("/api/portal", ...) instead
- Replace N+1 queries in GET /waitlist with a single JOIN across
clients, pets, and services tables
- Remove dead expiredIds variable in markExpiredEntries; use .some()
instead of computing an array only for its length
- Fix stray indentation in appointments.ts DELETE handler (line 487)
- Update waitlist tests to exercise routes at new /portal/waitlist paths;
add leftJoin and lt to chainable mock
Co-Authored-By: Paperclip <noreply@paperclip.ing>
TypeScript's split()[0] is typed as string | undefined in strict mode.
Using slice(0, 10) is cleaner and avoids the type issue.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
When reading waitlist entries, active entries with preferredDate < today
are marked as expired both in the database and in the response.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add 401 when DELETE /waitlist/:id has no session (auth bypass fix)
- Add auth to PATCH /waitlist/:id (was zero auth)
- Add RBAC guard for /waitlist/* routes
- Fix migration to use proper ENUM type instead of TEXT
- Add unit tests for auth scenarios
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* 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>