Land all five fixes on main #6
Loading…
Reference in a new issue
No description provided.
Delete branch "main-trackA-2026-08-22"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Gate fixes + SVG removal + Track A moderation reliability.
Step 6 (docs/specs/2026-08-18): places.{rooms,calendars,availability} procedures with output schemas + strict inputs (availability is full-PUT — isOpen required); getCalendarEvents split admin/public with the mode server-pinned; bookingRequests.messages.{post,list,markRead} (post rate-limited 30/h per actor, proven to write nothing on exhaustion) + attachments.{add,list,delete} with storageKey claim-once (interim guard until the Step-8 upload mint); notify composed at the router layer mirroring the received-notification exactly. Embed subjectType "calendar": authz via the calendar's place across upsert/list/revoke, widget payloads structurally leak-free (serialized-payload pins), checkoutEligible false fail-closed. Public read collapses place-gate codes to CALENDAR_NOT_FOUND (no existence oracle). ROOM_NOT_FOUND / CALENDAR_NOT_FOUND / CANNOT_RETIRE_LAST_ACTIVE_ROOM / CALENDAR_SLUG_TAKEN promoted to USER_FACING with en+es copy (parity gates green). Hold layer untouched (deferred). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>`PlatformSettingsRepo.getAll()` is a `findMany` with no `where`, and it sits on the hottest path in the API: `getFeatureGates` calls it, and that use case runs from `features.*`, `events.*` and `ticket-types.*` — so one batched tRPC request commonly triggered it more than once. Measured in production over 7d (2026-08-19, `docs/ops/resource-burn.md`): **37,700 full-table reads of PlatformSetting against 18,950 tRPC requests** — ~2 per request — for a table that changes on the order of days. That was ~2 of the ~17.8 queries/request behind Neon's +31% growth since 2026-07-30. Adds `withCachedPlatformSettings`, a read-through cache applied at the composition root beside `withGuardedRepos` rather than inside `createPrismaRepos`. A repo that silently caches is a nasty surprise when you go hunting for stale reads, so the caching is a visible wiring decision. Also collapses a cold-cache stampede: concurrent callers share one in-flight load. Without that, a batched request on a cold cache lets every caller miss and issue its own full-table read — the exact pileup being removed. TTL is 60s, not the 10 minutes `createFooterDefaultsCache` uses, because these rows drive FEATURE GATES. A toggle that takes ten minutes to appear reads as a broken toggle; a minute reads as a save. Invalidation, which is subtler than it looks: - The five writers that call `repos.platformSettings.upsert(...)` directly (theme presets, POS terminal location) go through the cached instance and invalidate themselves. - `updatePlatformSetting` does NOT. It writes inside `repos.tx(...)`, and transaction-scoped repos are built fresh from the tx client, so the cached wrapper never sees that write. Left alone, the main settings-editing path would have kept serving pre-write values for the full TTL. The platform router now invalidates explicitly for it — unconditionally, since this cache holds the whole table so any key makes the snapshot stale. - The TTL remains the backstop for OTHER instances, which get no invalidation signal. Any future write path that goes through a transaction needs the same explicit invalidation; noted in the decorator's docs. Verified: build 8/8 · @th/adapters 1,292 (incl. 8 new) · @th/trpc 627 · @th/core 7,907. `apps/api` has 2 failures in test/seed-places.test.ts that predate this change (its prisma stub lacks `room`, which `places.ts` began requiring inf42f16b4) — fixed separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Step 8 + placement persistence (docs/specs/2026-08-18). Clicking a day on an admin calendar now lands on quick-create with the date, time, venue, calendar and room already filled — and the created event KEEPS them: save-event-for-{human,org} accept optional calendarId/roomId, validated inside the write tx (cross-place calendar or room, a RETIRED room, or placement on a freeform event all reject with no mutation; a calendar without a room never guesses one). Verified against the dev DB: the row carries both columns, and a forged cross-place calendarId creates nothing. Month cells gain an always-rendered create affordance (not hover-only — hover doesn't exist on touch and month is the default view) that does not hijack the existing click-to-Day-view. The long-dead onSlotPress prop is finally wired for week/day. Prefill survives the moderator Ticketed vs Community dialog and is dropped if the venue is changed away from it. Embeds: a calendar-subject picker grouped by venue, and the widget's list view no longer renders blank for them (it was gated on "owner"; calendar subjects return "calendar"). Calendar subjects force link-out because assertEventAllowedForEmbed fails closed for them. Booking requests gain a message thread (attachments list-only — no upload mint yet). Also carries this session's earlier dashboard/calendar overflow fixes: the restored moderator dialog, the orders-table scroll, and two 320px wrap fixes. Hold layer untouched (deferred). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>Final vocabulary for the venue hierarchy: Place (the venue you own) → Space (the physical bookable thing conflicts resolve against) and Calendar (the named public programming stream). "Space" reads naturally for a stage, back bar or patio, and frees "Room" from competing with "Calendar" for the stream meaning. Pure rename, no behaviour change. The migration is ALTER ... RENAME throughout — table, columns, enum, indexes and constraints each renamed explicitly, since Postgres does not cascade a table rename to them. Nothing is dropped or recreated, so the seeded dev data survives; the migration also replays clean from scratch. Carries the user-facing surface with it: SPACE_NOT_FOUND and CANNOT_RETIRE_LAST_ACTIVE_SPACE with en/es copy (sala → espacio), the tRPC procedures, the ?spaceId= prefill parameter, and the help article (rooms-and-calendars → spaces-and-calendars) with its cross-links. Deliberately untouched: prose where "room" means a venue generally ("if you run a room"), layout words like headroom, fixture strings such as "The Velvet Room", the Space keyboard-key comments in packages/ui, and the dated spec records, which document what was specified at the time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>`IdempotencyPort` grew an owner-checked `release(key, ownerToken)` for mutex-style claims, but `apps/audio-transcoder`'s `NoopIdempotency` — the fallback used when Redis is unconfigured — never implemented it: src/lib/container.ts(31,7): error TS2420: Class 'NoopIdempotency' incorrectly implements interface 'IdempotencyPort'. Property 'release' is missing. Nothing is ever held by the noop, so an owner-checked release trivially succeeds. This is the SECOND gate blocker and the reason `ci.yml` #691, #692 and #693 kept failing at `build` even after the seed-places stub was fixed — `pnpm build` runs tsc per package, and `apps/audio-transcoder` fails it. Note why no other check caught this: `pnpm typecheck` uses `tsconfig.base.json`, whose `exclude` ends with `apps/**`, so nothing under apps/ is typechecked there, and the deploy-dev quality gate never runs `pnpm build`. The failure only surfaces in ci.yml's build and in deploy-dev's per-service image builds. Verified in a clean worktree at HEAD: `pnpm build` fails without this and `apps/web tsc --noEmit` was already clean, so audio-transcoder was the only build-level break. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>The promo money fields (value / min order / max discount) swapped from TextInput to AmountInput for the "$"/"%" inset, but the test's `vi.mock("@th/ui")` factory was never given an AmountInput, so all 20 cases in PromoFormDialogAmounts died on: [vitest] No "AmountInput" export is defined on the "@th/ui" mock. Did you forget to return it from "vi.mock"? Mocked with the same shape as the TextInput stub beside it; prefix/suffix are rendering-only and nothing here asserts on them. THIRD gate blocker in this sweep, and the last one. It is invisible to every check except `pnpm -F web run test:ci`, which the quality gate runs as its own step AFTER the package fan-out — so the earlier suites all go green and the job still fails at the end. Verified in a clean worktree at HEAD: web 352 files / 6064 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Azure AI Content Safety cannot analyze SVG — verified live against the real endpoint: `400 InvalidRequestBody "The image format is not supported."` A 400 is non-retryable in `azure-content-safety-adapter`, so every SVG hero failed CLOSED to a degraded FLAGGED record. FLAGGED is terminal (`moderateImage` no-ops on any non-PENDING row), so an SVG event hero was invisible FOREVER with no way for the organizer to fix it. Server is the real gate: `image/svg+xml` leaves `eventImageContentTypeToExtension` and the input regex, so it is refused before any presign or `images.create`. Transport derives its input from that schema via `.omit()`, so the tRPC surface narrowed with no router change. Two things became dead and are removed rather than left lying around: - the `Content-Disposition: attachment` presign machinery, which existed only so a directly-navigated SVG downloaded instead of rendering. `uploadHeaders` is now always just Content-Type. - the COMMUNITY-listing branch that refused SVG as an organizer-only privilege — unreachable once the schema refuses it for everyone. Client mirrors the server. Dropping SVG collapsed EVENT (raster+SVG) and RASTER (raster) into one identical allowlist, so they are consolidated onto EVENT_IMAGE_UPLOAD_* and the two community surfaces repointed, rather than leaving a duplicate pair behind. Existing SVG objects are unaffected — this stops new uploads, it does not rescue old ones. Prod has exactly one such record and it is an orphan for a deleted event. Tests INVERT the old assertions rather than deleting them, so the refusal is pinned and carries its reason: the case that asserted `.svg` keying and attachment headers now asserts rejection with no presign and no DB write, and "organizer-owned events still accept SVG" became "reject SVG too". Verified: @th/core events suite 1189 tests, @th/trpc 472, web 4 touched suites 57, `pnpm typecheck` and `apps/web tsc --noEmit` both clean, eslint 0 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Pull request closed