From 9f2809e89be02206bc34928b75881cef75edc9cb Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Thu, 21 May 2026 10:31:43 +0000 Subject: [PATCH 1/5] fix(GRO-1441): remove duplicate coatType/petSizeCategory from buildPet Lines 108-109 were duplicates of lines 102-103 from the PR #12 merge. Removing the duplicate pair resolves the TS1117 error on dev. Co-Authored-By: Paperclip --- packages/db/src/factories.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/db/src/factories.ts b/packages/db/src/factories.ts index cac71f7..c91f34a 100644 --- a/packages/db/src/factories.ts +++ b/packages/db/src/factories.ts @@ -105,8 +105,6 @@ export function buildPet(overrides: Partial & { 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"), }; From 49f70eb74bcbad4182309da57f1c9923aec17f85 Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Fri, 22 May 2026 22:18:52 +0000 Subject: [PATCH 2/5] fix(GRO-1544): restore /health alongside /api/health endpoint The previous GRO-1544 PR changed /health to /api/health but removed the /health endpoint entirely. This breaks: - Dockerfile HEALTHCHECK (curl -f http://localhost:3000/health) - K8s readinessProbe/livenessProbe (httpGet: path: /health, port: 3000) Both paths are registered before auth middleware so both remain publicly accessible without authentication. Co-Authored-By: Claude Opus 4.7 --- src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.ts b/src/index.ts index 9ae50b8..2abf712 100644 --- a/src/index.ts +++ b/src/index.ts @@ -59,6 +59,9 @@ app.use( ); // Health check — no auth required, registered on app at full path before auth middleware +// /health: used by Dockerfile HEALTHCHECK and K8s readinessProbe/livenessProbe (port 3000 direct) +app.get("/health", (c) => c.json({ status: "ok" })); +// /api/health: used by Gateway HTTPRoute (/api/* → API pod) app.get("/api/health", (c) => c.json({ status: "ok" })); // Public booking routes — no auth required, must be registered before auth middleware From e26d960046f01ac82daa8fa68b6c262f5836e7b8 Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Sat, 23 May 2026 01:30:16 +0000 Subject: [PATCH 3/5] fix(GRO-1576): add provenance: false to all build-push-action steps Docker Buildx v6 defaults to OCI attestation manifests (--attest type=provenance,mode=max). These hit a Gitea registry bug when image layers are pre-existing (blob mount), causing "unknown" errors on manifest list push. API image succeeds because it pushes new layers; migrate/seed/ reset fail because their layers already exist. Disabling provenance attestation on all four build-push-action steps resolves the push failures. Addresses GRO-1575. Co-Authored-By: Paperclip --- .gitea/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index df10baa..a222bb8 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -93,6 +93,7 @@ jobs: file: Dockerfile target: runner push: true + provenance: false tags: | git.farh.net/groombook/api:${{ steps.version.outputs.tag }} ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/api:latest' || '' }} @@ -106,6 +107,7 @@ jobs: file: Dockerfile target: migrate push: true + provenance: false tags: | git.farh.net/groombook/migrate:${{ steps.version.outputs.tag }} ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/migrate:latest' || '' }} @@ -119,6 +121,7 @@ jobs: file: Dockerfile target: seed push: true + provenance: false tags: | git.farh.net/groombook/seed:${{ steps.version.outputs.tag }} ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/seed:latest' || '' }} @@ -132,6 +135,7 @@ jobs: file: Dockerfile target: reset push: true + provenance: false tags: | git.farh.net/groombook/reset:${{ steps.version.outputs.tag }} ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/reset:latest' || '' }} From b486c44a82b99d9ac8592407d19f91331ab4994c Mon Sep 17 00:00:00 2001 From: Scrubs McBarkley <18+gb_scrubs@noreply.git.farh.net> Date: Sun, 24 May 2026 20:11:44 +0000 Subject: [PATCH 4/5] fix(api): add timeouts for OIDC discovery fetch and DB connection (#66) --- packages/db/src/index.ts | 2 +- src/index.ts | 10 ++++++---- src/lib/auth.ts | 4 +++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 8b3b01f..e3bc914 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -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; } diff --git a/src/index.ts b/src/index.ts index 2abf712..3dd8921 100644 --- a/src/index.ts +++ b/src/index.ts @@ -285,14 +285,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); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 9e78740..da2b2d1 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -186,7 +186,9 @@ export async function initAuth(): Promise { const discoveryUrlStr = `${providerConfig.issuerUrl}/.well-known/openid-configuration`; let oidcConfig: Record = {}; 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; From a1466b44c9ffa46077703e10c659dee42d014cef Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Mon, 25 May 2026 15:29:36 +0000 Subject: [PATCH 5/5] feat(gro-1743): add UAT customer and pets to admin seed endpoint Add UAT Customer (uat-customer@groombook.dev) with two pets (Bella and Max) to the idempotent admin seed endpoint for portal UAT testing. - Client: UAT Customer, email: uat-customer@groombook.dev, phone: 555-0100, status: active - Pet 1: Bella, Dog, Poodle, coatType: curly - Pet 2: Max, Dog, Labrador Retriever, coatType: short Issue: GRO-1743 Co-Authored-By: Paperclip --- apps/api/src/routes/admin/seed.ts | 55 +++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/apps/api/src/routes/admin/seed.ts b/apps/api/src/routes/admin/seed.ts index 1220991..a5ac911 100644 --- a/apps/api/src/routes/admin/seed.ts +++ b/apps/api/src/routes/admin/seed.ts @@ -36,6 +36,18 @@ const DEMO_PET = { weightKg: "30.00", }; +const UAT_CLIENT = { + name: "UAT Customer", + email: "uat-customer@groombook.dev", + phone: "555-0100", + status: "active" as const, +}; + +const UAT_PETS = [ + { name: "Bella", species: "Dog", breed: "Poodle", coatType: "curly" as const }, + { name: "Max", species: "Dog", breed: "Labrador Retriever", coatType: "short" as const }, +]; + const DEMO_SERVICES = [ { id: "b0000001-0000-0000-0000-000000000001", name: "Bath & Brush", description: "Full bath, blow-dry, brush out, and ear cleaning", basePriceCents: 4500, durationMinutes: 45 }, { id: "b0000001-0000-0000-0000-000000000002", name: "Full Groom — Small", description: "Complete grooming for dogs under 25 lbs", basePriceCents: 6500, durationMinutes: 60 }, @@ -128,6 +140,49 @@ adminSeedRouter.post("/seed", async (c) => { results.push(`Created pet '${DEMO_PET.name}' for Demo Client (id: ${created!.id})`); } + // ── Client: UAT Customer ────────────────────────────────────────────────── + const [existingUatClient] = await db + .select() + .from(clients) + .where(eq(clients.email, UAT_CLIENT.email)); + + let uatClientId: string; + if (existingUatClient) { + uatClientId = existingUatClient.id; + results.push(`Client '${UAT_CLIENT.name}' already exists (id: ${uatClientId})`); + } else { + const [created] = await db.insert(clients).values(UAT_CLIENT).returning(); + uatClientId = created!.id; + results.push(`Created client '${UAT_CLIENT.name}' (id: ${uatClientId})`); + } + + // ── Pets: UAT Customer Pets ─────────────────────────────────────────────── + const existingUatPets = await db + .select() + .from(pets) + .where(eq(pets.clientId, uatClientId)); + + for (const uatPet of UAT_PETS) { + const existingPet = existingUatPets.find( + (p) => p.name === uatPet.name && p.species === uatPet.species + ); + if (existingPet) { + results.push(`Pet '${uatPet.name}' already exists for UAT Customer (id: ${existingPet.id})`); + } else { + const [created] = await db + .insert(pets) + .values({ + clientId: uatClientId, + name: uatPet.name, + species: uatPet.species, + breed: uatPet.breed, + coatType: uatPet.coatType, + }) + .returning(); + results.push(`Created pet '${uatPet.name}' for UAT Customer (id: ${created!.id})`); + } + } + return c.json({ message: "Seed complete", details: results,