Compare commits

...

7 Commits

Author SHA1 Message Date
Flea Flicker 5ec9e9a8fd fix(ci): correct typo groombok->groombook and fix Reset image cache-from indentation
CI / Test (pull_request) Successful in 12s
CI / Lint & Typecheck (pull_request) Successful in 15s
CI / Build & Push Docker Images (pull_request) Successful in 44s
- Fix API image tag typo: groombok -> groombook (line 103)
- Fix Reset image cache-from/cache-to indentation: moved from under tags: (12 spaces) to under with: (10 spaces)
- This corrects the Reset image build failure in CI runs.
2026-05-30 05:14:51 +00:00
Flea Flicker e9aef5719f GRO-1939: Add CI smoke test for blackholed migrate runtime
CI / Test (pull_request) Successful in 15s
CI / Lint & Typecheck (pull_request) Successful in 17s
CI / Build & Push Docker Images (pull_request) Failing after 53s
Cherry-picked from fix/GRO-1909-migrate-corepack-offline (f007eca)
2026-05-30 04:49:59 +00:00
Flea Flicker c588c94dcb GRO-1955: hotfix seed.ts broken uc reference in random pet batch (#112)
CI / Test (push) Failing after 9s
CI / Lint & Typecheck (push) Successful in 20s
CI / Build & Push Docker Images (push) Has been skipped
2026-05-30 04:42:33 +00:00
Lint Roller e00cdc1321 fix(db): add missing 'short' value to coat_type enum (GRO-1953) (#110)
CI / Lint & Typecheck (push) Failing after 14s
CI / Test (push) Successful in 14s
CI / Build & Push Docker Images (push) Has been skipped
2026-05-30 04:20:02 +00:00
Scrubs McBarkley 1891b9c523 GRO-1949: add behavioral and skin medicalAlertPool types, deterministic seeding for TestCooper/TestRocky (#109)
CI / Test (push) Successful in 12s
CI / Lint & Typecheck (push) Failing after 15s
CI / Build & Push Docker Images (push) Has been skipped
2026-05-30 04:12:06 +00:00
Flea Flicker 0ab16b82e0 GRO-1921: Fix UAT reset CronJob to seed full UAT profile with extended pet fields (#106)
CI / Test (push) Successful in 11s
CI / Test (pull_request) Successful in 10s
CI / Lint & Typecheck (push) Successful in 16s
CI / Lint & Typecheck (pull_request) Successful in 16s
CI / Build & Push Docker Images (push) Successful in 57s
CI / Build & Push Docker Images (pull_request) Successful in 1m2s
2026-05-30 03:42:43 +00:00
Flea Flicker 981a257d2d Merge pull request 'fix(api): repair root src/routes/pets.ts visit-count query (GRO-1945)' (#107) from flea/GRO-1945-pets-visitcount-hotfix into dev
CI / Test (push) Successful in 10s
CI / Lint & Typecheck (push) Successful in 14s
CI / Build & Push Docker Images (push) Successful in 21s
CI / Test (pull_request) Successful in 10s
CI / Lint & Typecheck (pull_request) Successful in 15s
CI / Build & Push Docker Images (pull_request) Successful in 59s
2026-05-30 03:24:03 +00:00
4 changed files with 136 additions and 93 deletions
+11
View File
@@ -118,6 +118,17 @@ jobs:
cache-from: type=registry,ref=git.farh.net/groombook/cache:migrate
cache-to: type=registry,ref=git.farh.net/groombook/cache:migrate,mode=max
- name: Smoke test migrate image (blackhole npmjs.org)
run: |
set -euo pipefail
IMAGE="git.farh.net/groombook/migrate:${{ steps.version.outputs.tag }}"
docker pull "$IMAGE"
docker run --rm \
--add-host registry.npmjs.org:127.0.0.1 \
--entrypoint="" \
"$IMAGE" \
pnpm --version
- name: Build and push Seed image
uses: docker/build-push-action@v6
with:
@@ -0,0 +1,14 @@
-- Migration: 0035_add_short_to_coat_type_enum.sql
-- GRO-1953: Adds missing "short" value to the coat_type enum so that seed data
-- (which uses coatTypePool including "short") can be inserted without error.
--
-- The seed file defines coatTypePool as:
-- ["short", "medium", "long", "double", "wire", "silky", "curly", "hairless"]
-- but migration 0031 created the enum without "short", causing:
-- PostgresError: invalid input value for enum coat_type: "short"
BEGIN;
ALTER TYPE "coat_type" ADD VALUE IF NOT EXISTS 'short';
COMMIT;
@@ -246,6 +246,13 @@
"when": 1751140800000,
"tag": "0034_extend_pet_profile_columns",
"breakpoints": true
},
{
"idx": 35,
"version": "7",
"when": 1751140800000,
"tag": "0035_add_short_to_coat_type_enum",
"breakpoints": true
}
]
}
+104 -93
View File
@@ -270,6 +270,10 @@ const medicalAlertPool: MedicalAlert[] = [
{ id: "", type: "other", description: "Seizure history — avoid flashing lights", severity: "high" },
{ id: "", type: "other", description: "Luxating patella — short walks only", severity: "medium" },
{ id: "", type: "other", description: "Ear infections — dry thoroughly after bath", severity: "low" },
{ id: "", type: "behavioral", description: "Anxiety — calm environment preferred", severity: "low" },
{ id: "", type: "behavioral", description: "Fear-based aggression — approach with caution", severity: "high" },
{ id: "", type: "skin", description: "Contact dermatitis — avoid harsh chemicals", severity: "medium" },
{ id: "", type: "skin", description: "Hot spots — monitor and report any worsening", severity: "high" },
];
const preferredCutPool: string[] = [
@@ -385,78 +389,19 @@ const servicesDef = [
{ id: "b0000001-0000-0000-0000-00000000000a", name: "Sanitary Trim", desc: "Hygienic trim of paw pads, face, and sanitary areas", price: 2500, dur: 20 },
];
// ── Known-users-only seed (prod/demo) ───────────────────────────────────────
// ── UAT staff account seeding (shared between seed paths) ─────────────────────
/**
* Seeds only the minimal known users for prod/demo environments.
* Creates: Demo Manager staff + Demo Client + Demo Dog + basic services.
* Idempotent: skips creation if records already exist.
* Seeds or upserts the deterministic UAT staff accounts with numeric OIDC subs
* from SEED_UAT_*_OIDC_SUB / SEED_UAT_GROOMER_OIDC_SUBS env vars.
*
* In the full seed path this must run AFTER random staff are created so the
* deterministic upserts land on the correct rows (groomers referenced by the
* UAT test-client appointment logic use groomers[0] etc.).
*
* In seedKnownUsers() this replaces the inline UAT-staff block.
*/
async function seedKnownUsers() {
const url = process.env.DATABASE_URL;
if (!url) {
console.error("DATABASE_URL is not set");
process.exit(1);
}
const client = postgres(url, { max: 5 });
const db = drizzle(client, { schema });
console.log("Seeding known users (prod/demo mode)...\n");
const KNOWN_STAFF_ID = "00000000-0000-0000-0000-000000000001";
const DEMO_CLIENT_ID = "00000000-0000-0000-0000-000000000002";
const DEMO_PET_ID = "00000000-0000-0000-0000-000000000003";
// ── Staff: Demo Manager ──
const [existingStaff] = await db
.select()
.from(schema.staff)
.where(eq(schema.staff.email, "demo-manager@groombook.dev"))
.limit(1);
if (existingStaff) {
console.log(`✓ Staff '${existingStaff.name}' already exists — skipping`);
} else {
await db.insert(schema.staff).values({
id: KNOWN_STAFF_ID,
name: "Demo Manager",
email: "demo-manager@groombook.dev",
oidcSub: "demo-manager-001",
role: "manager",
isSuperUser: true,
active: true,
});
console.log("✓ Created staff 'Demo Manager' (oidcSub: demo-manager-001)");
}
// ── Staff: SEED_ADMIN_EMAIL admin ──
const adminEmail = process.env.SEED_ADMIN_EMAIL;
if (adminEmail) {
const adminName = process.env.SEED_ADMIN_NAME ?? "Admin";
const ADMIN_STAFF_ID = "00000000-0000-0000-0000-000000000002";
const [existingAdmin] = await db
.select()
.from(schema.staff)
.where(eq(schema.staff.email, adminEmail))
.limit(1);
if (existingAdmin) {
console.log(`✓ Staff admin '${existingAdmin.name}' already exists — skipping`);
} else {
await db.insert(schema.staff).values({
id: ADMIN_STAFF_ID,
name: adminName,
email: adminEmail,
oidcSub: adminEmail,
role: "manager",
isSuperUser: true,
active: true,
});
console.log(`✓ Created staff admin '${adminName}' (${adminEmail})`);
}
}
async function seedUatStaffAccounts(db: ReturnType<typeof drizzle>) {
// ── Staff: UAT Super User (oidcSub from SEED_UAT_SUPER_OIDC_SUB env var) ──
const uatSuperOidcSub = process.env.SEED_UAT_SUPER_OIDC_SUB;
if (uatSuperOidcSub) {
@@ -680,6 +625,84 @@ async function seedKnownUsers() {
console.log(`✓ Created UAT pet '${pet.name}'`);
}
}
}
// ── Known-users-only seed (prod/demo) ───────────────────────────────────────
/**
* Seeds only the minimal known users for prod/demo environments.
* Creates: Demo Manager staff + Demo Client + Demo Dog + basic services.
* Idempotent: skips creation if records already exist.
*/
async function seedKnownUsers() {
const url = process.env.DATABASE_URL;
if (!url) {
console.error("DATABASE_URL is not set");
process.exit(1);
}
const client = postgres(url, { max: 5 });
const db = drizzle(client, { schema });
console.log("Seeding known users (prod/demo mode)...\n");
const KNOWN_STAFF_ID = "00000000-0000-0000-0000-000000000001";
const DEMO_CLIENT_ID = "00000000-0000-0000-0000-000000000002";
const DEMO_PET_ID = "00000000-0000-0000-0000-000000000003";
// ── Staff: Demo Manager ──
const [existingStaff] = await db
.select()
.from(schema.staff)
.where(eq(schema.staff.email, "demo-manager@groombook.dev"))
.limit(1);
if (existingStaff) {
console.log(`✓ Staff '${existingStaff.name}' already exists — skipping`);
} else {
await db.insert(schema.staff).values({
id: KNOWN_STAFF_ID,
name: "Demo Manager",
email: "demo-manager@groombook.dev",
oidcSub: "demo-manager-001",
role: "manager",
isSuperUser: true,
active: true,
});
console.log("✓ Created staff 'Demo Manager' (oidcSub: demo-manager-001)");
}
// ── Staff: SEED_ADMIN_EMAIL admin ──
const adminEmail = process.env.SEED_ADMIN_EMAIL;
if (adminEmail) {
const adminName = process.env.SEED_ADMIN_NAME ?? "Admin";
const ADMIN_STAFF_ID = "00000000-0000-0000-0000-000000000002";
const [existingAdmin] = await db
.select()
.from(schema.staff)
.where(eq(schema.staff.email, adminEmail))
.limit(1);
if (existingAdmin) {
console.log(`✓ Staff admin '${existingAdmin.name}' already exists — skipping`);
} else {
await db.insert(schema.staff).values({
id: ADMIN_STAFF_ID,
name: adminName,
email: adminEmail,
oidcSub: adminEmail,
role: "manager",
isSuperUser: true,
active: true,
});
console.log(`✓ Created staff admin '${adminName}' (${adminEmail})`);
}
}
// ── UAT staff accounts + Better Auth credentials (shared impl) ──────────────
// Extracted into seedUatStaffAccounts() so it runs in both seedKnownUsers()
// and the full seed() UAT branch.
await seedUatStaffAccounts(db);
// ── Services: idempotent upsert using name as unique key ─────────────────────
// UNIQUE constraint on services.name (migration 0020) must exist first.
@@ -847,30 +870,10 @@ async function seed() {
console.log(`✓ Upserted admin staff '${adminName}' (${adminEmail})`);
}
// ── UAT Groomer Personas (SEED_UAT_GROOMER_EMAILS + SEED_UAT_GROOMER_NAMES) ──
const groomerEmails = process.env.SEED_UAT_GROOMER_EMAILS?.split(",").map((e) => e.trim()).filter(Boolean) ?? [];
const groomerNames = process.env.SEED_UAT_GROOMER_NAMES?.split(",").map((n) => n.trim()).filter(Boolean) ?? [];
const groomerCount = Math.min(groomerEmails.length, groomerNames.length);
for (let i = 0; i < groomerCount; i++) {
const email = groomerEmails[i]!;
const name = groomerNames[i]!;
const staffId = `00000000-0000-0000-0000-${String(5 + i).padStart(12, "0")}`;
await db.insert(schema.staff)
.values({
id: staffId,
name,
email,
oidcSub: email,
role: "groomer",
isSuperUser: false,
active: true,
})
.onConflictDoUpdate({
target: schema.staff.email,
set: { id: staffId, name, role: "groomer", isSuperUser: false, active: true },
});
console.log(`✓ Upserted groomer '${name}' (${email})`);
}
// ── UAT staff accounts + Better Auth credentials (shared impl) ──────────────
// Seeds deterministic UAT staff with numeric OIDC subs and Better Auth credentials.
// Must run AFTER random staff are created so upserts land correctly.
await seedUatStaffAccounts(db);
// ── Services ──
// Upsert services using name as unique key. With deterministic IDs in
@@ -1059,6 +1062,14 @@ async function seed() {
temperamentScore: randInt(1, 5),
temperamentFlags: pickN(temperamentFlagPool, randInt(1, 3)),
medicalAlerts: (() => {
// Deterministic alerts for UAT AC pets
if (uc.petName === "TestCooper") {
return pickN(medicalAlertPool.filter((a) => a.type === "behavioral"), 1).map((a) => ({ ...a, id: uuid() }));
}
if (uc.petName === "TestRocky") {
return pickN(medicalAlertPool.filter((a) => a.type === "skin"), 1).map((a) => ({ ...a, id: uuid() }));
}
// Other UAT pets: random
if (rand() < 0.3) {
const count = rand() < 0.7 ? 1 : 2;
return pickN(medicalAlertPool, count).map((a) => ({ ...a, id: uuid() }));