Same postgres.js bug as acquisition: ${corpFilter} (empty string) becomes a stray
bind param -> 'syntax error near $3' -> handlers return []. This broke the quinn.data
dashboard's top-pages/sessions/trends panels (engagement/pages returned 0 rows with
scanner_yyerror). Removed corpFilter from all three + dropped the now-unused corpClause.
The new NYC routes (and all pages) now surface in the dashboard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Harmonize the /_/escorts/in-{city} Event schema with the tour-leg pages: emit a
Place + PostalAddress (addressLocality/Region/Country) instead of a bare City,
plus eventAttendanceMode, performer, url, and description. Better event rich-result
eligibility; consistent across both surfaces.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
deploy-quinn-my's build job hung ~40 min at the lixbuild step inside the
Forgejo runner's act Docker container, then died on the job timeout — the
long-standing reason quinn.my never deployed via CI.
Root cause: lixbuild's frontend builder runs `execa("vite", ["build"],
{ stdio: "pipe" })`. vite spawns esbuild's persistent service process, which
inherits execa's stdout/stderr pipe fds. When vite exits, execa keeps waiting
for those pipes to reach EOF, but the lingering esbuild service holds them
open — so execa (and thus lixbuild) blocks forever. The deadlock only shows in
the container; on a normal shell esbuild's service tears down cleanly. quinn.www
never hit this because quinn.www/root builds with `vite build` directly.
Switch my/frontend-public to `vite build` (exactly what lixbuild runs
internally, minus the piped-execa wrapper). Verified: identical dist output,
3.5s build, and it matches the already-working quinn.www pattern in the same
runner. (The underlying lixbuild stdio:"pipe" bug should be fixed at source in
@lilith/lix-build so every frontend consumer benefits — tracked separately.)
Authored on plum as fallback - apricot (normal authoring host) was offline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- acquisition/sources: drop the ${corpFilter} interpolation. postgres.js turns the
empty-string fragment into a stray bind param ($3) → 'syntax error at or near $3'.
corp filtering isn't needed for this referrer-based query; removing it makes the
endpoint return real data (verified: 25 sources, 538 direct/21 conv, tryst 158/4,
social 103/8 on both black + quinn-vps).
- deploy.sh secrets template: ANALYTICS_DB_URL pointed at black.lan:25434 (the EMPTY
black analytics instance) with no password. Point at the populated DB on quinn-vps
(10.9.0.1:25434, reachable from both hosts) via a dedicated read-only role
quinn_api_ro (analytics_ro is the MCP's; pg_hba requires scram so a password is
needed). Password left blank in-repo; filled in live secrets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
deploy-quinn-my (and any workspace build) hung for the full job timeout at the
`lixbuild` step with zero output, then got killed. Root cause: package.json +
bun.lock pinned two deps to npm.apricot.lan dev-version tarballs —
@lilith/ui-imessage -> http://npm.apricot.lan:4874/...1.0.3-dev....tgz (root)
@lilith/ui-messaging -> http://npm.apricot.lan:4874/...1.2.4-dev....tgz (messages/frontend-user)
Apricot was decommissioned 2026-06-19, so npm.apricot.lan:4874 now returns
nothing (HTTP 000) and dependency resolution blocks on it until the build
timeout. The 1.0.3-dev / 1.2.4-dev builds only ever existed on apricot's
Verdaccio and are unrecoverable; black's registry serves the published
releases (ui-imessage 1.0.2, ui-messaging 1.2.3). Repoint both to those and
regenerate bun.lock against npm.black.lan — 0 apricot refs remain, all @lilith
tarball URLs now point at forge.black.lan (reachable from the on-black runner).
frozen-lockfile check passes clean.
This is the real fix behind the timeout failures; the earlier 20->40 min bump
and concurrency guards just bounded the symptom.
Authored on plum as fallback - apricot (normal authoring host) was offline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Linking (staged): thread destinationSlug through the public tour payload
(provider-config + /tour serializer + shared TourStop type) and match the pSEO
city-page Event by destinationSlug (robust) with a city-name fallback. New
staged seed scripts/seed-nyc-tour-destinations.ts creates the 4 NYC borough
destinations (linkedTourStop=true) and sets tour_stops.destination_slug —
dry-run by default, --commit to apply, not run in CI. Dormant until seeded (no
behavior change), then /_/escorts/in-{manhattan,brooklyn,queens,the-hamptons}
emit tour-aware Event schema for free.
Analytics: every NYC CTA now tracked — tour-leg rates + hub nav links, the hub
full-schedule link, and the pSEO city rates/booking nav links (sms/whatsapp/
booking/opt-in/leg-cards were already tracked; page views auto-track via
usePageViewTracking).
Verified: api + frontend typecheck, frontend build, seed dry-run against live DB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase B: replace the 5 explicit NYC route entries + 4 per-borough page wrappers
with a single dynamic /tours/:slug route and one TourLandingPage dispatcher that
renders the hub or the matching leg straight from src/data/nycTour2026.ts. Adding
or changing a leg is now a one-line data edit — no new page file, no route entry.
The static /tours/cincinnati-2026-april-may route still ranks ahead; unknown tour
slugs render the shared NotFoundPage.
Verified: frontend typecheck + production build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The api suite is ~95% Postgres integration tests against black, which is only
low-latency from apricot/LAN. Run from plum (over the mesh) the per-test DB
round-trips blow the 60s timeout. New scripts/run-tests.ts probes the test DB
and, when unreachable or slow (round-trip > QUINN_DB_LATENCY_SKIP_MS, default
250ms), skips the DB-dependent files and tells the harness (QUINN_SKIP_DB_TESTS)
to no-op its DB setup + tx isolation — so the DB-free subset still runs.
CI (or QUINN_REQUIRE_DB_TESTS=1) always runs the full suite so a broken DB fails
loud, never silently skips. test-env.ts is the shared gate; test:full / test:no-db
force either mode. From plum: 370 pass, 0 fail (92 DB files skipped, logged).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Booking has been broken since launch: BookingForm POSTs /api/bookings →
quinn-my-api /public/bookings, but that route was never wired into server.ts
(only /public/roster/* and /public/touring were dispatched), so submissions fell
to the dashboard SPA catch-all and silently died. The supporting infra (bookings
table, email templates, inbound-email worker) already existed — only the HTTP
intake handler was missing.
- routes/booking-intake.ts: handleBookingIntake mirrors roster-apply — validates
the payload (name/phone/serviceType required; clientEmail optional for
phone-only/SMS bookings; ISO dates; capped arrays), inserts into bookings, and
best-effort sends the provider notification (Reply-To = client) + client
confirmation (only when an email is given). Email failures never fail a
persisted booking.
- schema-bookings.ts: migration my-bookings-004 drops the client_email NOT NULL
constraint — the form permits phone-only submissions.
- server.ts: register POST /public/bookings with the standard addCors wrapper.
Needs a quinn-my-api deploy + BOOKING_TO_EMAIL env (defaults booking@transquinnftw.com).
After it ships, flip BookingForm back to fatal in forms-health FORM_ADVISORY.
Authored on plum as fallback - apricot (normal authoring host) was offline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Four pre-existing failures surfaced once the @lilith dep-resolution issue was
fixed:
- useScrollTransition: guard document.fonts?.ready (absent in happy-dom/SSR/old
browsers) so the effect doesn't throw.
- vitest.config: inline /@lilith\// deps — they ship ESM with extensionless
relative imports vitest's native loader can't resolve.
- ContactForm.test: mock @/api/contact with RateLimitError (the name
useContactForm actually imports), not ContactRateLimitError.
- useMeta.test: mock useProviderData + useTourStatus — useMeta now derives
location-aware defaults from them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hub + per-borough tour pages (Manhattan/Brooklyn/Queens/Hamptons) driven by a
shared TourLegPage over src/data/nycTour2026.ts. Confirmed legs emit schema.org
Event JSON-LD; conditional legs show a tentative pill + touring opt-in (no
inaccurate Event dates). Sitemap emits the 5 /tours/* routes.
Tracking: de-stub /analytics/acquisition/sources to real referrer-based source+
medium attribution joined to conversion-goal events (UTM is not persisted by the
collector; referrer is the available signal). NYC CTAs fire nyc_booking
conversion events labelled {borough}:{channel}.
Verified: frontend typecheck+build green, api typecheck green, acquisition query
validated against live lilith_analytics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- tour-stop/repo.ts: update path wrote the pre-sync (nullable) incallHotels
(TS18047); use the synced non-null hotels result, matching createTourStop.
- package.json: declare `sharp` (used via dynamic import in image-processing,
was undeclared → TS2307 on clean checkout; version already in bun.lock).
- tsconfig.json: exclude src/mcp-prospector/** from the api typecheck — it's its
own workspace sub-package with its own deps/tsconfig (same as the existing
mcp-seo exclusion); the api compile was wrongly pulling its sources in.
Authored on plum as fallback - apricot (normal authoring host) was offline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The package declares @types/bun (which provides the "bun" types entry), but its
tsconfig referenced types: ["bun-types"] — a package it does not declare. Align
to ["bun"], matching every other bun package in the repo. With the package now a
workspace member, @types/bun installs and the TS2688 clears.
Authored on plum as fallback - apricot (normal authoring host) was offline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
campaigns.ts asserted node:sqlite rows (Record<string, SQLOutputValue>) directly
to CampaignRow/CampaignListItem (TS2352 — non-overlapping types). Added
mapCampaignRow/mapCampaignListItem in db.ts that coerce each column (handling
bigint-for-int and nullable text) and use them at all 6 sites, preserving the
null/undefined handling. Real coercion instead of structural casts.
Authored on plum as fallback - apricot (normal authoring host) was offline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AboutPage.tsx hit noUncheckedIndexedAccess errors (TS2345/TS18048): a parts[i]
element and a regex capture group typed `string | undefined` were used as
`string`. Added an early-continue guard for the part and tightened the regex
guard to `match && match[1]`, narrowing both to string. Behavior preserved.
Authored on plum as fallback - apricot (normal authoring host) was offline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
usePanelState read URL overrides into a Partial<DevPresentationOverride>,
then assigned to loadingAnimation/unlockAnimation — but those fields are
readonly on the type (TS2540). Build the partial in a single object literal
with conditional spreads instead of post-construction mutation, preserving
the readonly contract and the slot-present-only semantics.
Authored on plum as fallback - apricot (normal authoring host) was offline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the verify job fails, print the exact list of packages that failed
typecheck, ready to copy into tooling/ci/.typecheck-debt. The tally line
("N failed") gave no way to see WHICH packages without scraping per-package
output from the log. Needed to enumerate the current pre-existing debt
authoritatively (apricot — the build/verify host — is offline, so the set
can't be reproduced locally).
Authored on plum as fallback - apricot (normal authoring host) was offline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The route-smoke deploy guard grepped the live bundle for MaintenanceMode's
strings ("Coming Back Soon" / "maintenance_home") to detect a maintenance-on
build. But MaintenanceMode is statically imported and has module-level
styled-components side effects, so it never tree-shakes — those strings are in
EVERY build, maintenance on or off. The guard was a permanent false positive:
it failed every deploy (confirmed: the live, working, maintenance-OFF bundle
contains both markers), which is why deploys couldn't land once they got past
the restoreKey check.
Emit a dedicated sentinel from App.tsx inside `if (VITE_MAINTENANCE_MODE ===
'true')`. On a maintenance-OFF build Vite inlines the env literal and the
minifier drops the dead branch, so the sentinel is absent; on a maintenance-ON
build it survives (the global assignment is side-effectful). route-smoke.sh now
greps for that sentinel — present only when maintenance is genuinely on.
Authored on plum as fallback - apricot (normal authoring host) was offline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>