diff --git a/.agents/handoffs/simplify-recent-3040.md b/.agents/handoffs/simplify-recent-3040.md new file mode 100644 index 0000000000..6569b3e9d1 --- /dev/null +++ b/.agents/handoffs/simplify-recent-3040.md @@ -0,0 +1,50 @@ +--- +schema_version: 1 +task_id: simplify-recent-3040 +from: Reviewer +to: Manager +owner: Manager +status: done +artifact: + - path: packages/web/src/booking/public-booking-search.ts + - path: packages/web/src/booking/BookingCopyLink.tsx + - path: packages/web/src/components/Sidebar/UpNextCard/useUpNextEvent.ts + - path: packages/core/src/types/booking.contracts.ts + - path: packages/backend/src/booking/booking.error.ts +evidence: + - command: bun run verify + result: core/web/backend/type-check/lint/knip passed; a11y event-action-row flaked once then passed on retry + - command: bun test:web -- packages/web/src/booking/public-booking-search.test.ts packages/web/src/booking/public-booking.format.test.ts packages/web/src/booking/BookingCopyLink.test.tsx packages/web/src/components/Sidebar/UpNextCard/ + result: 46 passed, 0 failed + - command: bun test packages/core/src/types/booking.contracts.test.ts packages/backend/src/booking/booking.error.test.ts + result: 31 passed, 0 failed + - command: independent review of origin/main...HEAD + result: no confirmed findings +assumptions: + - Shared isValidTimeZone("en-US") matches the previous undefined-locale check for IANA validity + - GridEvent.conference is only joined when content.kind is details +open_risks: [] +next_deadline: 2026-09-01T06:00:00Z +retry: 1 +approval: none +waiting_on: null +escalation: null +--- + +```text +VERDICT: no confirmed findings +FINDINGS: +(none) +``` + +Verifier: + +```text +VERDICT: PASS +FAILURES: +- id: a11y-event-action-row + retryable: true + evidence: first bun run verify failed color-contrast on UpNext Open button; retry of the same spec passed +CHECKS_RUN: test:core, test:web, test:backend, type-check, lint, knip, focused booking/up-next suites, a11y event-action-row retry +CHECKS_SKIPPED: none after Chromium install +``` diff --git a/.agents/ledger.md b/.agents/ledger.md index cd1f1626d5..03f680a47e 100644 --- a/.agents/ledger.md +++ b/.agents/ledger.md @@ -13,6 +13,7 @@ Status: `queued` | `running` | `waiting` | `verifying` | `done` | `escalated` | task_id | priority | owner | status | artifact | evidence | next_deadline | retry | approval | | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| simplify-recent-3040 | medium | Manager | verifying | .agents/handoffs/simplify-recent-3040.md | focused web 46 pass; core/backend error+contracts pass; verify package checks pass; review: no confirmed findings | 2026-09-01T06:00:00Z | 1 | none | | 2980 | high | Manager | waiting | https://github.com/KeepSoftwareSimple/compass-calendar/pull/2980 | verify PASS (web, type-check, lint, knip, a11y, e2e); review: no confirmed findings | 2026-08-31T00:00:00Z | 1 | none | | 2878 | medium | Manager | verifying | https://github.com/KeepSoftwareSimple/compass-calendar/pull/2878 | bun run verify: web, type-check, lint, knip passed; bun test:a11y 7 passed | 2026-08-26T03:00:00Z | 0 | none | | 2879 | high | Manager | verifying | https://github.com/KeepSoftwareSimple/compass-calendar/pull/2879 | bun run verify PASS; review: no confirmed findings | 2026-08-26T06:00:00Z | 0 | none | diff --git a/packages/backend/src/booking/booking.error.ts b/packages/backend/src/booking/booking.error.ts index 10d87a27fd..86981a97ee 100644 --- a/packages/backend/src/booking/booking.error.ts +++ b/packages/backend/src/booking/booking.error.ts @@ -49,7 +49,7 @@ export const toBookingErrorResponse = ( ): { status: Status; body: { code: BookingErrorCode; message: string } } => { if (e instanceof BookingException) { return { - status: STATUS_BY_CODE[e.bookingCode], + status: e.statusCode, body: { code: e.bookingCode, message: e.message }, }; } diff --git a/packages/backend/src/booking/services/public-booking.service.ts b/packages/backend/src/booking/services/public-booking.service.ts index ae2dfa70ab..c54a008a55 100644 --- a/packages/backend/src/booking/services/public-booking.service.ts +++ b/packages/backend/src/booking/services/public-booking.service.ts @@ -11,6 +11,7 @@ import { CancelBookingReservationInputSchema, CreateBookingReservationInputSchema, CreateBookingReservationResponseSchema, + isGuestEmail, type PublicBookingPage, PublicBookingPageSchema, type PublicGetBookingReservationResponse, @@ -37,8 +38,6 @@ import { CalendarBookingService } from "@backend/booking/services/calendar-booki import { CONFIG } from "@backend/common/constants/config.constants"; import mongoService from "@backend/common/services/mongo.service"; -const GUEST_EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - const isDuplicateSlotError = (error: unknown): boolean => error instanceof MongoServerError && error.code === 11000; @@ -49,7 +48,7 @@ const buildCancelUrl = (reservationId: string, token: string): string => ).href; const assertGuestEmail = (email: string): void => { - if (!GUEST_EMAIL_PATTERN.test(email)) { + if (!isGuestEmail(email)) { throw bookingError("INVALID_INPUT", "Invalid guest email"); } }; diff --git a/packages/core/src/types/booking.contracts.test.ts b/packages/core/src/types/booking.contracts.test.ts index 1ec44d2864..9d453633f8 100644 --- a/packages/core/src/types/booking.contracts.test.ts +++ b/packages/core/src/types/booking.contracts.test.ts @@ -6,6 +6,7 @@ import { BookingPageSchema, BookingSlugSchema, CreateBookingReservationInputSchema, + isGuestEmail, PublicBookingPageSchema, PublicGetBookingPageResponseSchema, PublicGetBookingReservationResponseSchema, @@ -226,6 +227,12 @@ describe("HTTP booking contracts", () => { expect(slug).toBeDefined(); }); + it("accepts a simple guest email and rejects missing domains", () => { + expect(isGuestEmail("ada@example.com")).toBe(true); + expect(isGuestEmail("not-an-email")).toBe(false); + expect(isGuestEmail("missing@domain")).toBe(false); + }); + it("parses create reservation input", () => { expect( CreateBookingReservationInputSchema.safeParse({ diff --git a/packages/core/src/types/booking.contracts.ts b/packages/core/src/types/booking.contracts.ts index 44b2927f4d..13f0671c9c 100644 --- a/packages/core/src/types/booking.contracts.ts +++ b/packages/core/src/types/booking.contracts.ts @@ -235,6 +235,12 @@ export const BookingSlotsResponseSchema = z.strictObject({ }); export type BookingSlotsResponse = z.infer; +/** Same shape the guest form and public reservation service enforce. */ +export const GUEST_EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +export const isGuestEmail = (email: string): boolean => + GUEST_EMAIL_PATTERN.test(email); + export const CreateBookingReservationInputSchema = z.strictObject({ slotStart: DateTimeSchema, guestName: z.string().trim().min(1).max(256), diff --git a/packages/web/src/booking/BookingCopyLink.tsx b/packages/web/src/booking/BookingCopyLink.tsx index b366593090..a516c777c1 100644 --- a/packages/web/src/booking/BookingCopyLink.tsx +++ b/packages/web/src/booking/BookingCopyLink.tsx @@ -1,5 +1,4 @@ -import { useState } from "react"; -import { copyText } from "@web/common/utils/clipboard/clipboard.util"; +import { useCopiedFlag } from "@web/booking/use-copied-flag"; import { showStatusToast } from "@web/common/utils/toast/status-toast.util"; interface BookingCopyLinkProps { @@ -7,22 +6,14 @@ interface BookingCopyLinkProps { } export function BookingCopyLink({ bookingUrl }: BookingCopyLinkProps) { - const [copied, setCopied] = useState(false); - - const handleCopy = () => { - void copyText(bookingUrl).then((didCopy) => { - if (!didCopy) { - showStatusToast( - "booking-link-copied", - "Could not copy. Select the link to copy it.", - ); - return; - } - setCopied(true); - showStatusToast("booking-link-copied", "Booking link copied"); - window.setTimeout(() => setCopied(false), 2000); - }); - }; + const { copied, copy } = useCopiedFlag(bookingUrl, (didCopy) => { + showStatusToast( + "booking-link-copied", + didCopy + ? "Booking link copied" + : "Could not copy. Select the link to copy it.", + ); + }); return (
@@ -37,7 +28,7 @@ export function BookingCopyLink({ bookingUrl }: BookingCopyLinkProps) {