Compare commits

...

23 Commits

Author SHA1 Message Date
Flea Flicker dc3c23055a fix(api): add timeouts for OIDC discovery fetch and DB connection
CI / Test (pull_request) Successful in 16s
CI / Lint & Typecheck (pull_request) Successful in 19s
CI / Build & Push Docker Images (pull_request) Successful in 52s
- OIDC discovery fetch in initAuth() now has a 5s AbortSignal.timeout
  to fail fast instead of hanging indefinitely when the auth server is unreachable.
  This was identified as a root cause of startup ECONNRESET crashes on UAT
  where ztunnel drops TCP connections before headers arrive.

- DB postgres client now sets connect_timeout: 5 so failed connection attempts
  fail fast rather than hanging the startup sequence.

- Graceful shutdown timeout tightened to 8s (from 10s) to avoid
  getting killed by Kubernetes liveness-probe deadline while draining.

Fixes GRO-1678.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-24 19:46:23 +00:00
The Dogfather 62dfc7776b Merge pull request 'fix(GRO-1544): register health endpoint at /api/health not /health' (#52) from fix/gro-1544-api-health-endpoint into dev
CI / Lint & Typecheck (push) Failing after 1m29s
CI / Test (push) Failing after 1m29s
CI / Build & Push Docker Images (push) Has been skipped
fix(GRO-1544): register health endpoint at /api/health not /health

Merge PR #52 to dev. Unblocks GRO-1485 UAT regression chain.
2026-05-22 21:49:55 +00:00
The Dogfather 68df697cf3 Merge pull request 'fix(GRO-1533): fix migration 0031 for empty databases' (#57) from fix/gro-1533-migration-0031-coat-type into dev
CI / Test (push) Successful in 16s
CI / Lint & Typecheck (push) Successful in 17s
CI / Build & Push Docker Images (push) Successful in 30s
fix(GRO-1533): fix migration 0031 for empty databases (#57)

Adds ADD COLUMN IF NOT EXISTS for coat_type and pet_size_category before ALTER TYPE casts, making migration safe for both fresh and existing databases.

Reviewed-by: gb_lint (QA)
Approved-by: CTO
2026-05-22 15:20:50 +00:00
Chris Farhood 174d1c667b fix(GRO-1533): add missing coat_type/pet_size_category columns in migration 0031
CI / Lint & Typecheck (pull_request) Successful in 10s
CI / Test (pull_request) Successful in 10s
CI / Build & Push Docker Images (pull_request) Successful in 40s
Migration 0031 tries to ALTER the coat_type and pet_size_category columns
on the pets table to use new enum types, but no prior migration adds
these columns. On a fresh DB (after the reset CronJob wiped all tables),
this causes the entire migration chain to fail and roll back.

Added ADD COLUMN IF NOT EXISTS before the ALTER TYPE so the migration
works both on fresh databases and existing ones with the columns.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-22 15:12:07 +00:00
The Dogfather 9fe6e15012 Merge pull request 'fix(GRO-1533): add missing 0032_staff_read_at.sql migration file' (#55) from fix/gro-1533-missing-migration-0032 into dev
CI / Lint & Typecheck (push) Successful in 9s
CI / Test (push) Successful in 10s
CI / Build & Push Docker Images (push) Successful in 21s
fix(GRO-1533): add missing 0032_staff_read_at.sql migration file

Merged by CTO after QA approval (Review #3513).
Unblocks UAT migration pipeline.
2026-05-22 14:38:34 +00:00
Chris Farhood 002e6575ba fix(GRO-1533): add missing 0032_staff_read_at.sql migration file
CI / Test (pull_request) Successful in 2m30s
CI / Lint & Typecheck (pull_request) Successful in 2m32s
CI / Build & Push Docker Images (pull_request) Successful in 4m6s
The migration journal references 0032_staff_read_at but the SQL file
was never committed. drizzle-kit migrate fails with "No file
./migrations/0032_staff_read_at.sql found" which blocks all subsequent
migrations including the 0033 default_buffer_minutes fix.

Added as a no-op since the staff table schema has no readAt column.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-22 14:28:48 +00:00
The Dogfather f9c679b392 Merge pull request 'fix(GRO-1533): add missing default_buffer_minutes migration' (#53) from fix/gro-1533-missing-migration-journal into dev
CI / Lint & Typecheck (push) Failing after 3s
CI / Test (push) Successful in 9s
CI / Build & Push Docker Images (push) Has been skipped
Merge fix/gro-1533-missing-migration-journal into dev: add missing default_buffer_minutes migration (GRO-1533)
2026-05-22 14:08:55 +00:00
Flea Flicker ce0739b3ba fix(GRO-1533): fix snapshot id in 0033_snapshot.json
CI / Test (pull_request) Successful in 10s
CI / Lint & Typecheck (pull_request) Successful in 10s
CI / Build & Push Docker Images (pull_request) Successful in 49s
Fixes id from "0026_stripe_payment" to "0033_add_services_default_buffer_minutes".

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-22 14:07:39 +00:00
Flea Flicker 3609087980 fix(GRO-1533): add missing default_buffer_minutes migration
CI / Lint & Typecheck (pull_request) Successful in 9s
CI / Test (pull_request) Successful in 9s
CI / Build & Push Docker Images (pull_request) Successful in 46s
Adds 0033_add_services_default_buffer_minutes.sql with idempotent
ALTER TABLE to ensure services.default_buffer_minutes exists.

Also fixes _journal.json by adding missing 0031_buffer_rules entry (idx 31)
and 0032_staff_read_at entry (idx 32) that were absent from the journal.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-22 13:55:38 +00:00
Flea Flicker 7b2b533c16 docs(api): update UAT_PLAYBOOK.md §4.0 — new health endpoint path
CI / Test (pull_request) Successful in 9s
CI / Lint & Typecheck (pull_request) Successful in 10s
CI / Build & Push Docker Images (pull_request) Failing after 46s
Added TC-API-0.1 for GET /api/health (unauthenticated).
Corrected path from /health to /api/health (GRO-1544).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 13:49:15 +00:00
Flea Flicker 55894c6ff2 fix(GRO-1544): register health endpoint at /api/health not /health
The health check was registered on `app` at `/health`, but the HTTPRoute
routes `/api/*` to the API pod. Since auth middleware protects the /api
basePath, GET /api/health fell through to authMiddleware → 401.

Now registered on `api` before auth middleware at /api/health.

Updated UAT_PLAYBOOK.md §GRO-1485 — new health endpoint path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 13:49:15 +00:00
The Dogfather f55c74983f Merge pull request 'revert: undo PR #47 Dockerfile apps/api switch (broke CI Docker build)' (#50) from revert/gro-1533-dockerfile-fix into dev
CI / Lint & Typecheck (push) Successful in 9s
CI / Test (push) Successful in 9s
CI / Build & Push Docker Images (push) Successful in 50s
revert: undo PR #47 Dockerfile apps/api switch (broke CI Docker build)

Restores root src/ build path. PR #47 broke CI because pnpm-workspace.yaml
and lockfile dont include apps/*. Admin 500 root cause is in code/schema changes.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-22 13:31:12 +00:00
Chris Farhood 8bdab69288 revert: undo PR #47 Dockerfile apps/api switch (broke CI Docker build)
CI / Test (pull_request) Successful in 8s
CI / Lint & Typecheck (pull_request) Successful in 9s
CI / Build & Push Docker Images (pull_request) Successful in 47s
Reverts the Dockerfile to the root src/ build. PR #47 switched to apps/api/
but the pnpm workspace config and lockfile don't include apps/*, causing
Docker build failures (CI runs #968, #970).

The actual admin 500 root cause needs further investigation — it's in the
code/schema changes, not the Dockerfile build path.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-22 13:30:51 +00:00
The Dogfather 8f7104c3a0 Merge pull request 'fix(GRO-1533): revert Dockerfile to build from apps/api/src/' (#47) from fix/gro-1533-revert-dockefile-build-change into dev
CI / Test (push) Successful in 9s
CI / Lint & Typecheck (push) Successful in 10s
CI / Build & Push Docker Images (push) Failing after 18s
fix(GRO-1533): revert Dockerfile to build from apps/api/src/

Reverts the Dockerfile change that switched from pnpm --filter @groombook/api build
to pnpm build, which caused the image to build from root src/ instead of
apps/api/src/. This produced HTTP 500 on all authenticated admin routes.

Conflict with PR #45 (seed filter fix) resolved: migrate/seed/reset stages
use @groombook/api db:migrate|seed|reset since the builder only has apps/api/.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-22 13:23:49 +00:00
Flea Flicker dab9bfab71 fix(GRO-1533): revert Dockerfile to build from apps/api/src/
CI / Test (pull_request) Successful in 10s
CI / Lint & Typecheck (pull_request) Successful in 10s
CI / Build & Push Docker Images (pull_request) Failing after 18s
The Dockerfile build change (pnpm build → pnpm --filter @groombook/api)
was made against dev HEAD, but the root src/ directory was never fully
audited for parity with apps/api/src/. Admin routes returning 500 for
authenticated users post-OIDC login is consistent with the image
running code with incomplete middleware chain or mismatched schema
types when the root build path was used.

Revert to the apps/api/ build path which is known to work correctly.
UAT is running images from dev branch commit 9462915 which includes
this change alongside schema cleanup commits.

Root cause: Dockerfile was changed to build from root src/ instead of
apps/api/src/ without confirming the two source trees are functionally
identical. The proper fix path (schema audit + reconciliation) is
tracked in GRO-1536.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-22 13:23:33 +00:00
The Dogfather 70bc946a0d Merge pull request 'fix(seed): use --filter @groombook/db for seed/migrate/reset scripts' (#45) from flea-flicker/gro-1531-seed-db-filter into dev
CI / Lint & Typecheck (push) Successful in 10s
CI / Test (push) Successful in 10s
CI / Build & Push Docker Images (push) Successful in 21s
fix(seed): use --filter @groombook/db for seed/migrate/reset scripts

Merge PR #45 to dev. QA approved (review #3506), CTO reviewed.
2026-05-22 13:17:09 +00:00
Flea Flicker 40422a14f0 fix(seed): use --filter @groombook/db to invoke seed/migrate/reset scripts
CI / Lint & Typecheck (pull_request) Successful in 11s
CI / Test (pull_request) Successful in 11s
CI / Build & Push Docker Images (pull_request) Successful in 51s
The seed/migrate/reset Dockerfile targets specified `pnpm db:migrate`
etc., but those scripts are defined in packages/db/package.json, not
at the workspace root. pnpm workspace filtering is required to route
the command to the correct package.

- migrate: pnpm --filter @groombook/db migrate
- seed:    pnpm --filter @groombook/db seed
- reset:   pnpm --filter @groombook/db reset

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-22 13:08:49 +00:00
Chris Farhood 9462915a66 fix: align root src/ enum values with packages/db schema
CI / Lint & Typecheck (push) Successful in 9s
CI / Test (push) Successful in 10s
CI / Build & Push Docker Images (push) Failing after 36s
Root src/routes/ used stale enum values (xlarge→extra_large,
smooth→silky, missing short/medium/silky from coatType) and
sizeCategory→petSizeCategory field name mismatch with the
pets table column.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-22 03:17:13 +00:00
Chris Farhood 46f134a294 fix: use root pnpm build instead of --filter for api in Dockerfile
CI / Test (push) Successful in 11s
CI / Lint & Typecheck (push) Successful in 12s
CI / Build & Push Docker Images (push) Failing after 2m11s
pnpm --filter @groombook/api doesn't match the workspace root package
because pnpm-workspace.yaml only includes packages/*. The root
package.json already has a build script that compiles src/ → dist/,
which is what the Dockerfile copies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-22 03:11:03 +00:00
Chris Farhood 4086b6f5c0 fix: remove duplicate bufferRules table and duplicate properties blocking Docker build
CI / Test (push) Successful in 10s
CI / Lint & Typecheck (push) Successful in 10s
CI / Build & Push Docker Images (push) Failing after 24s
CI Run 942 Docker build fails on TS2451 (duplicate bufferRules at lines
190 & 653 of schema.ts), TS1117 (duplicate defaultBufferMinutes in
services table, duplicate coatType/petSizeCategory in factories.ts),
and TS2322 (null vs number for defaultBufferMinutes in factories.ts).

Keep the newer, more complete bufferRules declaration (with comments and
index) and the .notNull().default(0) variant of defaultBufferMinutes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-22 03:09:00 +00:00
Chris Farhood d7d98791c7 fix(ci): use monorepo-filtered commands in Gitea CI (GRO-1522)
CI / Lint & Typecheck (push) Successful in 10s
CI / Test (push) Successful in 9s
CI / Build & Push Docker Images (push) Failing after 2m1s
The root pnpm scripts typecheck/lint/test the stale src/ directory.
Use pnpm --filter @groombook/api to target the correct apps/api/ path,
matching the GitHub Actions CI configuration.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-22 03:04:12 +00:00
Chris Farhood 3bec5d095a fix(db): remove duplicate petSizeCategoryEnum/coatTypeEnum declarations (GRO-1522)
CI / Lint & Typecheck (push) Failing after 13s
CI / Test (push) Failing after 20s
CI / Build & Push Docker Images (push) Has been skipped
The schema file had two sets of these enum declarations with different values.
The first (stale) set broke all tests importing @groombook/db via the vitest alias,
causing CI to fail and blocking the docker build job.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-22 03:02:20 +00:00
The Dogfather 3fddf80fac Merge pull request 'fix(ci): build all service images + upgrade Node 22 + pin packageManager (GRO-1522)' (#44) from fix/gro-1522-ci-images-node22 into dev
CI / Lint & Typecheck (push) Failing after 12s
CI / Test (push) Failing after 21s
CI / Build & Push Docker Images (push) Has been skipped
fix(ci): build all service images + upgrade Node 22 + pin packageManager (GRO-1522)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-22 02:58:30 +00:00
16 changed files with 168 additions and 64 deletions
+3 -3
View File
@@ -32,10 +32,10 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Typecheck
run: pnpm typecheck
run: pnpm --filter @groombook/api typecheck
- name: Lint
run: pnpm lint
run: pnpm --filter @groombook/api lint
test:
name: Test
@@ -56,7 +56,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Run tests
run: pnpm test
run: pnpm --filter @groombook/api test
docker:
name: Build & Push Docker Images
+4 -4
View File
@@ -17,7 +17,7 @@ COPY src/ src/
COPY tsconfig.json ./
RUN pnpm --filter @groombook/types build && \
pnpm --filter @groombook/db build && \
pnpm --filter @groombook/api build
pnpm build
# Runtime
FROM node:22-alpine AS runner
@@ -42,12 +42,12 @@ CMD ["node", "dist/index.js"]
# Migrate stage — runs drizzle-kit migrate against the database
FROM builder AS migrate
CMD ["pnpm", "db:migrate"]
CMD ["pnpm", "--filter", "@groombook/db", "migrate"]
# Seed stage — populates the database with test data
FROM builder AS seed
CMD ["pnpm", "db:seed"]
CMD ["pnpm", "--filter", "@groombook/db", "seed"]
# Reset stage — drops all tables, re-runs migrations, and re-seeds
FROM builder AS reset
CMD ["pnpm", "db:reset"]
CMD ["pnpm", "--filter", "@groombook/db", "reset"]
+8
View File
@@ -21,6 +21,14 @@ GroomBook API is a Hono-based REST service (TypeScript/Node.js) powering the pet
## Test Cases
### 4.0 Health Check
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-API-0.1 | Unauthenticated health check | GET /api/health | 200 OK, `{"status":"ok"}` |
> **Note (GRO-1544):** Health endpoint registered on `api` basePath before auth middleware at `/api/health`. The old path `/health` was incorrect (routed to web pod via HTTPRoute `/*` rule).
### 4.1 Authentication
| # | Scenario | Steps | Expected |
+3 -1
View File
@@ -6,8 +6,10 @@
CREATE TYPE "pet_size_category" AS ENUM ('small', 'medium', 'large', 'xlarge');
CREATE TYPE "coat_type" AS ENUM ('smooth', 'double', 'wire', 'curly', 'long', 'hairless');
-- ─── Alter pets columns to use new enums ─────────────────────────────────────
-- ─── Add columns to pets if missing, then cast to enums ──────────────────────
ALTER TABLE "pets" ADD COLUMN IF NOT EXISTS "coat_type" text;
ALTER TABLE "pets" ADD COLUMN IF NOT EXISTS "pet_size_category" text;
ALTER TABLE "pets" ALTER COLUMN "coat_type" TYPE "coat_type" USING "coat_type"::text::"coat_type";
ALTER TABLE "pets" ALTER COLUMN "pet_size_category" TYPE "pet_size_category" USING "pet_size_category"::text::"pet_size_category";
@@ -0,0 +1 @@
-- no-op: journal entry exists but no schema change was needed
@@ -0,0 +1,6 @@
-- Migration: 0033_add_services_default_buffer_minutes.sql
-- Adds missing default_buffer_minutes column to services table.
-- 0031_buffer_rules was applied to the DB but its journal entry was missing,
-- so this ensures idempotent column addition for fresh DB restores.
ALTER TABLE "services" ADD COLUMN IF NOT EXISTS "default_buffer_minutes" integer DEFAULT 0 NOT NULL;
@@ -0,0 +1,103 @@
{
"id": "0033_add_services_default_buffer_minutes",
"version": "7",
"dialect": "postgresql",
"tables": {
"authProviderConfig": {
"name": "auth_provider_config",
"columns": {
"id": { "name": "id", "type": "uuid", "primaryKey": true, "default": "gen_random_uuid()", "isNullable": false },
"providerId": { "name": "provider_id", "type": "text", "isNullable": false },
"displayName": { "name": "display_name", "type": "text", "isNullable": false },
"issuerUrl": { "name": "issuer_url", "type": "text", "isNullable": false },
"internalBaseUrl": { "name": "internal_base_url", "type": "text", "isNullable": true },
"clientId": { "name": "client_id", "type": "text", "isNullable": false },
"clientSecret": { "name": "client_secret", "type": "text", "isNullable": false },
"scopes": { "name": "scopes", "type": "text", "isNullable": false, "default": "'openid profile email'" },
"enabled": { "name": "enabled", "type": "boolean", "isNullable": false, "default": "true" },
"createdAt": { "name": "created_at", "type": "timestamp", "isNullable": false, "default": "now()" },
"updatedAt": { "name": "updated_at", "type": "timestamp", "isNullable": false, "default": "now()" }
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {}
},
"businessSettings": {
"name": "business_settings",
"columns": {
"id": { "name": "id", "type": "uuid", "primaryKey": true, "default": "gen_random_uuid()", "isNullable": false },
"businessName": { "name": "business_name", "type": "text", "isNullable": false, "default": "'GroomBook'" },
"logoBase64": { "name": "logo_base64", "type": "text", "isNullable": true },
"logoMimeType": { "name": "logo_mime_type", "type": "text", "isNullable": true },
"logoKey": { "name": "logo_key", "type": "text", "isNullable": true },
"primaryColor": { "name": "primary_color", "type": "text", "isNullable": false, "default": "'#4f8a6f'" },
"accentColor": { "name": "accent_color", "type": "text", "isNullable": false, "default": "'#8b7355'" },
"createdAt": { "name": "created_at", "type": "timestamp", "isNullable": false, "default": "now()" },
"updatedAt": { "name": "updated_at", "type": "timestamp", "isNullable": false, "default": "now()" }
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {}
},
"clients": {
"name": "clients",
"columns": {
"id": { "name": "id", "type": "uuid", "primaryKey": true, "default": "gen_random_uuid()", "isNullable": false },
"name": { "name": "name", "type": "text", "isNullable": false },
"email": { "name": "email", "type": "text", "isNullable": true },
"phone": { "name": "phone", "type": "text", "isNullable": true },
"address": { "name": "address", "type": "text", "isNullable": true },
"notes": { "name": "notes", "type": "text", "isNullable": true },
"emailOptOut": { "name": "email_opt_out", "type": "boolean", "isNullable": false, "default": "false" },
"smsOptIn": { "name": "sms_opt_in", "type": "boolean", "isNullable": false, "default": "false" },
"smsConsentDate": { "name": "sms_consent_date", "type": "timestamp", "isNullable": true },
"smsOptOutDate": { "name": "sms_opt_out_date", "type": "timestamp", "isNullable": true },
"smsConsentText": { "name": "sms_consent_text", "type": "text", "isNullable": true },
"stripeCustomerId": { "name": "stripe_customer_id", "type": "text", "isNullable": true },
"status": { "name": "status", "type": "client_status", "isNullable": false, "default": "'active'" },
"disabledAt": { "name": "disabled_at", "type": "timestamp", "isNullable": true },
"createdAt": { "name": "created_at", "type": "timestamp", "isNullable": false, "default": "now()" },
"updatedAt": { "name": "updated_at", "type": "timestamp", "isNullable": false, "default": "now()" }
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": { "idx_clients_stripe_customer_id": { "columns": ["stripe_customer_id"] } }
},
"invoices": {
"name": "invoices",
"columns": {
"id": { "name": "id", "type": "uuid", "primaryKey": true, "default": "gen_random_uuid()", "isNullable": false },
"appointmentId": { "name": "appointment_id", "type": "uuid", "isNullable": true },
"clientId": { "name": "client_id", "type": "uuid", "isNullable": false },
"subtotalCents": { "name": "subtotal_cents", "type": "integer", "isNullable": false },
"taxCents": { "name": "tax_cents", "type": "integer", "isNullable": false, "default": "0" },
"tipCents": { "name": "tip_cents", "type": "integer", "isNullable": false, "default": "0" },
"totalCents": { "name": "total_cents", "type": "integer", "isNullable": false },
"status": { "name": "status", "type": "invoice_status", "isNullable": false, "default": "'draft'" },
"paymentMethod": { "name": "payment_method", "type": "payment_method", "isNullable": true },
"paidAt": { "name": "paid_at", "type": "timestamp", "isNullable": true },
"stripePaymentIntentId": { "name": "stripe_payment_intent_id", "type": "text", "isNullable": true },
"stripeRefundId": { "name": "stripe_refund_id", "type": "text", "isNullable": true },
"paymentFailureReason": { "name": "payment_failure_reason", "type": "text", "isNullable": true },
"notes": { "name": "notes", "type": "text", "isNullable": true },
"createdAt": { "name": "created_at", "type": "timestamp", "isNullable": false, "default": "now()" },
"updatedAt": { "name": "updated_at", "type": "timestamp", "isNullable": false, "default": "now()" }
},
"indexes": { "idx_invoices_client_id": { "columns": ["client_id"] }, "idx_invoices_status": { "columns": ["status"] }, "idx_invoices_created_at": { "columns": ["created_at"] } },
"foreignKeys": { "invoices_appointment_id_fkey": { "columns": ["appointmentId"], "reference": { "table": "appointments", "columns": ["id"] } }, "invoices_client_id_fkey": { "columns": ["clientId"], "reference": { "table": "clients", "columns": ["id"] } } },
"compositePrimaryKeys": {},
"uniqueConstraints": { "idx_invoices_stripe_payment_intent_id": { "columns": ["stripe_payment_intent_id"] } }
}
},
"enums": {
"appointment_status": { "name": "appointment_status", "values": ["scheduled", "confirmed", "in_progress", "completed", "cancelled", "no_show"] },
"client_status": { "name": "client_status", "values": ["active", "disabled"] },
"impersonation_session_status": { "name": "impersonation_session_status", "values": ["active", "ended", "expired"] },
"invoice_status": { "name": "invoice_status", "values": ["draft", "pending", "paid", "void"] },
"payment_method": { "name": "payment_method", "values": ["cash", "card", "check", "other"] },
"staff_role": { "name": "staff_role", "values": ["groomer", "receptionist", "manager"] },
"waitlist_status": { "name": "waitlist_status", "values": ["active", "notified", "expired", "cancelled"] }
},
"nativeEnums": {}
}
+21
View File
@@ -218,6 +218,27 @@
"when": 1775828067192,
"tag": "0030_messaging",
"breakpoints": true
},
{
"idx": 31,
"version": "7",
"when": 1775860800000,
"tag": "0031_buffer_rules",
"breakpoints": true
},
{
"idx": 32,
"version": "7",
"when": 1775894400000,
"tag": "0032_staff_read_at",
"breakpoints": true
},
{
"idx": 33,
"version": "7",
"when": 1779500000000,
"tag": "0033_add_services_default_buffer_minutes",
"breakpoints": true
}
]
}
+1 -3
View File
@@ -105,8 +105,6 @@ export function buildPet(overrides: Partial<PetRow> & { clientId: string }): Pet
photoKey: null,
photoUploadedAt: null,
image: null,
coatType: null,
petSizeCategory: null,
createdAt: new Date("2025-01-01T00:00:00Z"),
updatedAt: new Date("2025-01-01T00:00:00Z"),
};
@@ -121,7 +119,7 @@ export function buildService(overrides: Partial<ServiceRow> = {}): ServiceRow {
description: "A grooming service",
basePriceCents: 6500,
durationMinutes: 60,
defaultBufferMinutes: null,
defaultBufferMinutes: 0,
active: true,
createdAt: new Date("2025-01-01T00:00:00Z"),
updatedAt: new Date("2025-01-01T00:00:00Z"),
+1 -1
View File
@@ -12,7 +12,7 @@ export function getDb() {
if (_db) return _db;
const url = process.env.DATABASE_URL;
if (!url) throw new Error("DATABASE_URL is not set");
const client = postgres(url, { max: 10 });
const client = postgres(url, { max: 10, connect_timeout: 5 });
_db = drizzle(client, { schema });
return _db;
}
-39
View File
@@ -48,22 +48,6 @@ export const clientStatusEnum = pgEnum("client_status", [
"disabled",
]);
export const petSizeCategoryEnum = pgEnum("pet_size_category", [
"small",
"medium",
"large",
"xlarge",
]);
export const coatTypeEnum = pgEnum("coat_type", [
"smooth",
"double",
"wire",
"curly",
"long",
"hairless",
]);
// ─── Better-Auth Tables ──────────────────────────────────────────────────────
export const user = pgTable("user", {
@@ -196,35 +180,12 @@ export const services = pgTable("services", {
description: text("description"),
basePriceCents: integer("base_price_cents").notNull(),
durationMinutes: integer("duration_minutes").notNull(),
defaultBufferMinutes: integer("default_buffer_minutes"),
active: boolean("active").notNull().default(true),
defaultBufferMinutes: integer("default_buffer_minutes").notNull().default(0),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});
export const bufferRules = pgTable(
"buffer_rules",
{
id: uuid("id").primaryKey().defaultRandom(),
serviceId: uuid("service_id")
.notNull()
.references(() => services.id, { onDelete: "cascade" }),
sizeCategory: petSizeCategoryEnum("size_category"),
coatType: coatTypeEnum("coat_type"),
bufferMinutes: integer("buffer_minutes").notNull(),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
},
(t) => [
unique("uq_buffer_rules_service_size_coat").on(
t.serviceId,
t.sizeCategory,
t.coatType
),
]
);
export const staff = pgTable("staff", {
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
+8 -6
View File
@@ -58,8 +58,8 @@ app.use(
})
);
// Health check (no auth required)
app.get("/health", (c) => c.json({ status: "ok" }));
// Health check no auth required, registered on app at full path before auth middleware
app.get("/api/health", (c) => c.json({ status: "ok" }));
// Public booking routes — no auth required, must be registered before auth middleware
app.route("/api/book", bookRouter);
@@ -282,14 +282,16 @@ startReminderScheduler();
function shutdown() {
console.log("Shutting down gracefully...");
// SIGTERM/SIGINT → server.close() → callback → process.exit(0)
// If graceful close takes >8s, force-exit to avoid being killed undrained
setTimeout(() => {
console.error("Graceful close timeout — forcing exit");
process.exit(1);
}, 8_000);
server.close(() => {
console.log("HTTP server closed");
process.exit(0);
});
setTimeout(() => {
console.error("Forced shutdown after timeout");
process.exit(1);
}, 10_000);
}
process.on("SIGTERM", shutdown);
+3 -1
View File
@@ -186,7 +186,9 @@ export async function initAuth(): Promise<void> {
const discoveryUrlStr = `${providerConfig.issuerUrl}/.well-known/openid-configuration`;
let oidcConfig: Record<string, string> = {};
try {
const discoveryRes = await fetch(discoveryUrlStr);
const discoveryRes = await fetch(discoveryUrlStr, {
signal: AbortSignal.timeout(5000),
});
if (discoveryRes.ok) {
const discovery = await discoveryRes.json() as {
authorization_endpoint?: string;
+2 -2
View File
@@ -193,8 +193,8 @@ bookRouter.post(
name: body.petName,
species: body.petSpecies,
breed: body.petBreed ?? null,
coatType: (body.petCoatType ?? null) as "smooth" | "double" | "wire" | "curly" | "long" | "hairless" | null,
petSizeCategory: (body.petSizeCategory ?? null) as "small" | "medium" | "large" | "xlarge" | null,
coatType: (body.petCoatType ?? null) as "short" | "medium" | "long" | "double" | "wire" | "silky" | "curly" | "hairless" | null,
petSizeCategory: (body.petSizeCategory ?? null) as "small" | "medium" | "large" | "extra_large" | null,
})
.returning();
const pet = petInserted[0];
+2 -2
View File
@@ -19,8 +19,8 @@ bufferRulesRouter.use("*", requireRole("manager"));
const createBufferRuleSchema = z.object({
serviceId: z.string().uuid(),
sizeCategory: z.enum(["small", "medium", "large", "xlarge"]).optional(),
coatType: z.enum(["smooth", "double", "wire", "curly", "long", "hairless"]).optional(),
sizeCategory: z.enum(["small", "medium", "large", "extra_large"]).optional(),
coatType: z.enum(["short", "medium", "long", "double", "wire", "silky", "curly", "hairless"]).optional(),
bufferMinutes: z.number().int().positive(),
});
+2 -2
View File
@@ -24,8 +24,8 @@ const createPetSchema = z.object({
shampooPreference: z.string().max(500).optional(),
specialCareNotes: z.string().max(2000).optional(),
customFields: z.record(z.string(), z.string()).optional(),
sizeCategory: z.enum(["small", "medium", "large", "xlarge"]).optional(),
coatType: z.enum(["smooth", "double", "wire", "curly", "long", "hairless"]).optional(),
petSizeCategory: z.enum(["small", "medium", "large", "extra_large"]).optional(),
coatType: z.enum(["short", "medium", "long", "double", "wire", "silky", "curly", "hairless"]).optional(),
});
const updatePetSchema = createPetSchema.partial().omit({ clientId: true });