Compare commits

..

22 Commits

Author SHA1 Message Date
Flea Flicker 5bb8fbcb7d Merge pull request 'Promote uat → main (atomic): GRO-2105/2094/2099/2089/2180/2213 portal bundle' (#48) from uat into main
CI / Test (push) Successful in 19s
CI / Lint & Typecheck (push) Successful in 27s
CI / Build & Push Docker Image (push) Successful in 40s
2026-06-08 19:29:49 +00:00
Flea Flicker bc21d6de09 Promote dev → uat: GRO-2213 portal booking preferredTime HH:MM:SS fix (#52)
CI / Test (push) Successful in 21s
CI / Test (pull_request) Successful in 22s
CI / Lint & Typecheck (push) Successful in 26s
CI / Lint & Typecheck (pull_request) Successful in 28s
CI / Build & Push Docker Image (push) Successful in 25s
CI / Build & Push Docker Image (pull_request) Successful in 20s
2026-06-08 17:36:16 +00:00
Flea Flicker 32ef3bca4d Merge pull request 'Promote dev → uat: GRO-2180 portal Appointments ISO startTime fix' (#50) from dev into uat
CI / Lint & Typecheck (pull_request) Failing after 10m47s
CI / Test (push) Failing after 10m51s
CI / Lint & Typecheck (push) Failing after 10m52s
CI / Build & Push Docker Image (push) Has been skipped
CI / Test (pull_request) Failing after 15m38s
CI / Build & Push Docker Image (pull_request) Has been skipped
Merge PR #50: fix(GRO-2180) portal Appointments ISO startTime (dev → uat)

QA-approved (gb_lint); PR CI green after transient runner re-run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 10:28:50 +00:00
Flea Flicker 47c29ecbc2 Promote to UAT: GRO-2105 BookingFlow/RescheduleFlow availability fix (#47)
CI / Test (push) Successful in 17s
CI / Lint & Typecheck (push) Successful in 23s
CI / Build & Push Docker Image (push) Successful in 19s
CI / Test (pull_request) Failing after 10m34s
CI / Lint & Typecheck (pull_request) Failing after 10m34s
CI / Build & Push Docker Image (pull_request) Has been skipped
2026-06-02 19:17:03 +00:00
The Dogfather de7386e47a Promote to UAT: GRO-2094 React bootstrap error instrumentation (#45)
CI / Test (push) Successful in 23s
CI / Lint & Typecheck (push) Successful in 30s
CI / Build & Push Docker Image (push) Successful in 13s
Co-authored-by: The Dogfather <20+gb_dogfather@noreply.git.farh.net>
Co-committed-by: The Dogfather <20+gb_dogfather@noreply.git.farh.net>
2026-06-02 18:42:25 +00:00
Scrubs McBarkley fdff0977ad Merge pull request 'Promote uat → main: GRO-2012 RescheduleFlow portalSessionId fallback' (#40) from uat into main
CI / Test (push) Successful in 23s
CI / Lint & Typecheck (push) Successful in 29s
CI / Build & Push Docker Image (push) Successful in 16s
Promote uat → main: GRO-2012 RescheduleFlow portalSessionId fallback

Gate checks:
- UAT: GRO-2023 done (CTO verified, ec29f719)
- Security: GRO-2032 Barkley PASS
- UAT_PLAYBOOK.md: TC-WEB-5.26 present

Fix: CustomerPortal.tsx:329 sessionId={session?.id ?? portalSessionId}
Fix commit: f29f1828c8

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-01 19:10:08 +00:00
The Dogfather ec29f71974 Merge pull request 'Promote to UAT: GRO-2012 RescheduleFlow portalSessionId fallback' (#39) from dev into uat
CI / Test (push) Successful in 21s
CI / Lint & Typecheck (push) Successful in 30s
CI / Build & Push Docker Image (push) Successful in 10s
CI / Test (pull_request) Successful in 21s
CI / Lint & Typecheck (pull_request) Successful in 28s
CI / Build & Push Docker Image (pull_request) Successful in 13s
2026-06-01 17:46:35 +00:00
The Dogfather bd2a0d9516 Merge pull request 'Promote dev -> uat: GRO-2011 login-blank fix (+ GRO-1867)' (#37) from dev into uat
CI / Test (push) Successful in 19s
CI / Lint & Typecheck (push) Successful in 23s
CI / Build & Push Docker Image (push) Successful in 10s
2026-06-01 16:38:14 +00:00
The Dogfather 0e5e9d1f16 Merge pull request 'chore: promote dev → uat (GRO-1829 SW fix)' (#32) from dev into uat
CI / Test (push) Successful in 13s
CI / Lint & Typecheck (push) Successful in 23s
CI / Build & Push Docker Image (push) Successful in 15s
Merge: promote dev → uat (GRO-1829 SW fix)
2026-05-27 02:27:32 +00:00
The Dogfather 3b4d0f15f6 Merge pull request 'chore: promote dev → uat (GRO-1795 StatusBadge)' (#28) from dev into uat
CI / Lint & Typecheck (push) Successful in 17s
CI / Test (push) Successful in 13s
CI / Build & Push Docker Image (push) Successful in 34s
Merge PR #28: promote dev → uat (GRO-1795 StatusBadge)
2026-05-26 13:23:52 +00:00
The Dogfather 87939e5413 Merge pull request 'chore: promote dev → uat (GRO-1794 booking analytics)' (#27) from dev into uat
CI / Test (push) Successful in 19s
CI / Lint & Typecheck (push) Successful in 22s
CI / Build & Push Docker Image (push) Successful in 12s
Merge dev → uat: GRO-1794 booking funnel analytics events
2026-05-26 13:16:39 +00:00
The Dogfather 4e3a038bf3 Merge pull request 'Promote dev → uat (GRO-1793: dynamic time slots)' (#25) from dev into uat
CI / Test (push) Successful in 14s
CI / Lint & Typecheck (push) Successful in 16s
CI / Build & Push Docker Image (push) Failing after 6s
Promote dev → uat: GRO-1793 dynamic portal time slots (#25)
2026-05-26 13:02:16 +00:00
Scrubs McBarkley 2aad7cb6a0 Merge pull request 'promote: uat → main (GRO-1757 SSO auto-provision fix)' (#21) from uat into main
CI / Test (push) Successful in 13s
CI / Lint & Typecheck (push) Successful in 21s
CI / Build & Push Docker Image (push) Successful in 13s
2026-05-26 02:16:28 +00:00
Lint Roller 8349ea00de Merge pull request 'promote: dev → uat (GRO-1757 SSO auto-provision fix)' (#19) from dev into uat
CI / Test (push) Successful in 13s
CI / Lint & Typecheck (push) Successful in 33s
CI / Build & Push Docker Image (push) Successful in 14s
CI / Test (pull_request) Successful in 19s
CI / Lint & Typecheck (pull_request) Successful in 24s
CI / Build & Push Docker Image (pull_request) Successful in 15s
promote: dev → uat (GRO-1757 SSO auto-provision fix)
2026-05-25 23:48:10 +00:00
Chris Farhood 0c41640f59 Add .mcp.json
CI / Test (push) Successful in 20s
CI / Lint & Typecheck (push) Successful in 27s
CI / Build & Push Docker Image (push) Successful in 4m1s
2026-05-24 18:15:24 +00:00
The Dogfather 0306c7fbd9 Merge pull request 'chore(GRO-1592): promote dev→uat SSO session cookie fix' (#16) from promote-uat-gro1592 into uat
CI / Test (push) Successful in 12s
CI / Lint & Typecheck (push) Successful in 18s
CI / Build & Push Docker Image (push) Failing after 39s
2026-05-23 14:13:43 +00:00
Chris Farhood 93da2f1dd8 chore: promote dev→uat for GRO-1592 SSO session cookie fix
CI / Lint & Typecheck (pull_request) Successful in 17s
CI / Test (pull_request) Successful in 18s
CI / Build & Push Docker Image (pull_request) Failing after 41s
- Fixed frontend auth client baseURL fallback to use window.location.origin
- Added UAT test coverage (TC-AUTH-5.3.4)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-23 14:13:12 +00:00
The Dogfather 62cbfe4e43 Merge pull request 'promote: dev → uat (GRO-1173 buffer rules + GRO-1470 pet save persistence)' (#14) from dev into uat
CI / Test (push) Successful in 14s
CI / Lint & Typecheck (push) Successful in 19s
CI / Build & Push Docker Image (push) Successful in 9s
promote: dev → uat (GRO-1173 buffer rules + GRO-1470 pet save persistence) (#14)

Merged-By: The Dogfather (CTO)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-21 19:46:41 +00:00
The Dogfather db6a2a1bbf Merge pull request 'promote: dev → uat (Renovate config, GRO-1081)' (#11) from dev into uat
promote: dev → uat (Renovate config, GRO-1081)

Merge PR #11: dev → uat promotion
Includes: chore: add Renovate config (GRO-1081)
2026-05-20 12:42:04 +00:00
The Dogfather 032a3796ba Merge pull request 'chore: promote dev to uat (CI Docker registry fix)' (#10) from dev into uat
chore: promote dev to uat (CI Docker registry fix) (#10)

Promotes GRO-1348 CI registry fix to UAT.
2026-05-20 11:17:21 +00:00
the-dogfather-cto[bot] cac8fc947e chore(GRO-1289): promote dev to uat — add UAT_PLAYBOOK.md
chore(GRO-1289): promote dev to uat — add UAT_PLAYBOOK.md
2026-05-14 21:13:56 +00:00
the-dogfather-cto[bot] 592be1301c chore: promote dev to uat (#3)
chore: promote dev to uat
2026-05-11 13:19:33 +00:00
5 changed files with 107 additions and 6 deletions
+4
View File
@@ -34,6 +34,10 @@ export interface Pet {
breed: string | null;
weightKg: number | null;
dateOfBirth: string | null;
/** Portal-shaped serialization of weightKg (GET/PATCH /api/portal/pets). */
weight?: string | number | null;
/** Portal-shaped serialization of dateOfBirth (GET/PATCH /api/portal/pets). */
birthDate?: string | null;
healthAlerts: string | null;
groomingNotes: string | null;
cutStyle: string | null;
+8
View File
@@ -154,4 +154,12 @@ describe("PetForm", () => {
expect(screen.getByText("Anxious")).toBeTruthy();
expect(screen.getByText("Good with kids")).toBeTruthy();
});
// ── Weight pre-fill from portal `weight` key (GRO-2207) ───────────────────────
it("pre-fills weight from the portal `weight` key when weightKg is absent", () => {
const portalPet: Pet = { ...BASE_PET, weightKg: null, weight: "12.50" };
render(<PetForm pet={portalPet} onSave={onSave} onCancel={onCancel} />);
expect(screen.getByDisplayValue(12.5)).toBeTruthy();
});
});
+80
View File
@@ -0,0 +1,80 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { BasicInfoTab, formatSizeCategory } from "../portal/sections/PetProfiles.js";
import type { Pet } from "@groombook/types";
// The portal endpoint (GET /api/portal/pets) serializes DB columns under
// portal-shaped keys: weightKg→weight, dateOfBirth→birthDate. The read view
// must surface those keys (GRO-2207), not the raw staff-side weightKg/dateOfBirth.
const PORTAL_PET: Pet = {
id: "pet-1",
clientId: "client-1",
name: "Pup Alpha",
species: "dog",
breed: "Poodle",
// Staff-shaped keys intentionally null — only the portal keys are populated,
// proving the read view reads `weight`/`birthDate`.
weightKg: null,
dateOfBirth: null,
weight: "12.50",
birthDate: "2022-03-10T00:00:00.000Z",
petSizeCategory: "extra_large",
healthAlerts: null,
groomingNotes: null,
cutStyle: null,
shampooPreference: null,
specialCareNotes: null,
customFields: {},
coatType: null,
preferredCuts: [],
medicalAlerts: [],
createdAt: "2024-01-01T00:00:00.000Z",
updatedAt: "2024-01-01T00:00:00.000Z",
};
describe("BasicInfoTab read view (GRO-2207)", () => {
it("renders Weight from the portal `weight` key", () => {
render(<BasicInfoTab pet={PORTAL_PET} readOnly />);
expect(screen.getByText("12.50 kg")).toBeInTheDocument();
});
it("renders Date of Birth from the portal `birthDate` key", () => {
render(<BasicInfoTab pet={PORTAL_PET} readOnly />);
expect(screen.getByText("March 10, 2022")).toBeInTheDocument();
});
it("renders Size Category formatted from petSizeCategory", () => {
render(<BasicInfoTab pet={PORTAL_PET} readOnly />);
expect(screen.getByText("Size Category")).toBeInTheDocument();
expect(screen.getByText("Extra Large")).toBeInTheDocument();
});
it("falls back to staff-shaped keys when portal keys are absent", () => {
const staffShaped: Pet = { ...PORTAL_PET, weight: null, birthDate: null, weightKg: 25, dateOfBirth: "2020-01-05T00:00:00.000Z" };
render(<BasicInfoTab pet={staffShaped} readOnly />);
expect(screen.getByText("25 kg")).toBeInTheDocument();
expect(screen.getByText("January 5, 2020")).toBeInTheDocument();
});
it("shows Unknown for missing weight/DoB and size", () => {
const empty: Pet = { ...PORTAL_PET, weight: null, birthDate: null, weightKg: null, dateOfBirth: null, petSizeCategory: null };
render(<BasicInfoTab pet={empty} readOnly />);
// Weight, Date of Birth and Size Category rows all read "Unknown".
expect(screen.getAllByText("Unknown").length).toBeGreaterThanOrEqual(3);
});
});
describe("formatSizeCategory", () => {
it("title-cases each underscore-separated segment", () => {
expect(formatSizeCategory("extra_large")).toBe("Extra Large");
expect(formatSizeCategory("small")).toBe("Small");
expect(formatSizeCategory("medium")).toBe("Medium");
expect(formatSizeCategory("large")).toBe("Large");
});
it("returns Unknown for null/undefined/empty", () => {
expect(formatSizeCategory(null)).toBe("Unknown");
expect(formatSizeCategory(undefined)).toBe("Unknown");
expect(formatSizeCategory("")).toBe("Unknown");
});
});
+1 -1
View File
@@ -22,7 +22,7 @@ function newAlert(): Omit<MedicalAlert, "id"> {
export function PetForm({ pet, onSave, onCancel, saving, saveError }: Props) {
const [name, setName] = useState(pet?.name ?? "");
const [breed, setBreed] = useState(pet?.breed ?? "");
const [weight, setWeight] = useState(pet?.weightKg ?? 0);
const [weight, setWeight] = useState(Number(pet?.weight ?? pet?.weightKg ?? 0));
const [notes, setNotes] = useState(pet?.healthAlerts ?? "");
const [coatType, setCoatType] = useState<CoatType | "">((pet?.coatType as CoatType) ?? "");
const [petSizeCategory, setPetSizeCategory] = useState<SizeOption | "">(pet?.petSizeCategory as SizeOption ?? "");
+14 -5
View File
@@ -176,9 +176,9 @@ export function PetProfiles({ sessionId, readOnly }: Props) {
</div>
<div className="flex-1">
<h2 className="text-xl font-semibold text-stone-800">{selectedPet.name}</h2>
<p className="text-stone-500 text-sm">{selectedPet.breed ?? "Unknown breed"} · {selectedPet.weightKg ? `${selectedPet.weightKg} kg` : "Unknown weight"}</p>
<p className="text-stone-500 text-sm">{selectedPet.breed ?? "Unknown breed"} · {(() => { const w = selectedPet.weight ?? selectedPet.weightKg; return w != null && w !== "" ? `${w} kg` : "Unknown weight"; })()}</p>
<p className="text-stone-400 text-xs mt-0.5">
Born {selectedPet.dateOfBirth ? new Date(selectedPet.dateOfBirth).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }) : "Unknown"}
Born {(() => { const d = selectedPet.birthDate ?? selectedPet.dateOfBirth; return d ? new Date(d).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }) : "Unknown"; })()}
</p>
</div>
{!readOnly && (
@@ -222,6 +222,14 @@ export function PetProfiles({ sessionId, readOnly }: Props) {
);
}
export function formatSizeCategory(size?: string | null): string {
if (!size) return "Unknown";
return size
.split("_")
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ");
}
function InfoRow({ label, value }: { label: string; value: React.ReactNode }) {
return (
<div className="flex flex-col sm:flex-row sm:items-center py-2.5 border-b border-stone-100 last:border-0">
@@ -244,7 +252,7 @@ function SeverityBadge({ severity }: { severity: "low" | "medium" | "high" }) {
);
}
function BasicInfoTab({ pet, readOnly }: { pet: Pet; readOnly: boolean }) {
export function BasicInfoTab({ pet, readOnly }: { pet: Pet; readOnly: boolean }) {
const score = pet.temperamentScore;
const flags = pet.temperamentFlags ?? [];
@@ -252,8 +260,9 @@ function BasicInfoTab({ pet, readOnly }: { pet: Pet; readOnly: boolean }) {
<div>
<InfoRow label="Name" value={pet.name} />
<InfoRow label="Breed" value={pet.breed || "Unknown"} />
<InfoRow label="Weight" value={pet.weightKg ? `${pet.weightKg} kg` : "Unknown"} />
<InfoRow label="Date of Birth" value={pet.dateOfBirth ? new Date(pet.dateOfBirth).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }) : "Unknown"} />
<InfoRow label="Weight" value={(() => { const w = pet.weight ?? pet.weightKg; return w != null && w !== "" ? `${w} kg` : "Unknown"; })()} />
<InfoRow label="Date of Birth" value={(() => { const d = pet.birthDate ?? pet.dateOfBirth; return d ? new Date(d).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }) : "Unknown"; })()} />
<InfoRow label="Size Category" value={formatSizeCategory(pet.petSizeCategory)} />
{/* Temperament (staff-set, read-only) */}
{(score != null || flags.length > 0) && (