Replace the hardcoded dark STYLE_GRADIENT map, cream text, and fixed CTA
colors with theme tokens (background.secondary/tertiary, text.primary,
primary fill + onPrimary, themed borders/shadows). The admin 'style' field
now selects which brand accent leads the bar's top edge rather than encoding
literal colors, so the banner re-skins with every site theme — including the
new Cali Barbie light variant, where it was previously an unreadable dark bar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inject __BUILD_INFO__ (version, BUILD_COUNT, short SHA, UTC build time) into the
bundle at deploy time via bun build --define, and surface it plus mode +
startedAt from /health, so we can confirm exactly which build is live. Falls
back to BUILD_* env then 'dev' for unbundled/local runs.
Reorient the prospector MCP around the /my/prospects cockpit API that powers
quinn.my Prospector: add cockpit_stream / cockpit_tour_board and friends with
human-readable formatters (new cockpit-format.ts), extend the API client, and
rewrite the README (quinn-drafts → quinn-prospector, black:3912).
Follow-through on the quinn-mcp fleet key rename (bc186901): port vars
(QUINN_MCP_DATA_PORT → QUINN_MCP_ANALYTICS_PORT in .env.ports,
quinn.mcp.data → quinn.mcp.analytics in ports.yaml, both still :3914) and the
remaining quinn-mcp@data → quinn-mcp@analytics references in the server header
comment, edge-visitors-aggregate.sh, and mcp-servers.md.
The admin SPA's useAuth hook probes /auth/refresh on mount and calls
/auth/logout on sign-out, but both were dropped in the per-feature → monolith
consolidation (they lived in the old @features/admin backend), so every page
load bounced back to SSO. Convert authSurface into createAuthSurface(opts) and
mount it before the /admin/* SSO guard: /refresh runs ssoRequired (401 on
missing/invalid cookie, 200 otherwise) and /logout expires the session cookie
via clearSessionCookie (idempotent 200).
client_bookings has FKs to calendar_events(id) and income_sessions(id), so its
migration must run after both. The old position (right after bookingMigrations)
only failed on a from-scratch build — incremental prod DBs already had the
target tables — but it breaks the ephemeral E2E test DB. Move it after
incomeSessionMigrations.
The data MCP is purely read-only analytics, so rename the package
(@lilith/quinn-data-mcp → @lilith/quinn-analytics-mcp), bin, server name,
logger prefix, and the .mcp.json client key to match. The systemd deploy
instance key stays `data` (quinn-mcp@data, black:3914) — noted in the deploy
script and mcp-servers.md. Updates all doc/content references (nyc-tour SEO,
twitter handoff, deploy comments).
Add a `city` column to rate_sections (NULL = default/home card) with an
additive migration and CMS field. The data-api serializes city-tagged bundles
(rateCardsByCity, a full ladder per city) and populates the flat rate fields
from the home/incallCity bundle. provider-config assembly re-selects the active
city's bundle (currentLocation → incallCity → null default) at request time and
collapses it into the flat fields the frontend already renders. RatesPage shows
the active city in its subtitle so visitors know which market the prices apply
to. Shared types gain RateCardBundle + ProviderData.rateCardsByCity.
Surface WGS-84 lat/lng end to end: destination repo hydrates the DB `lon`
column to `lng` (matching tour_stops), the entity + shared types carry
lat/lng, data-api serialize emits them, and provider-config assembly passes
through both destination and tour-stop coordinates. Destinations without
coordinates render as a card but no map pin.
Bump @lilith/quinn-data-mcp to 0.2.0 and add raw_events-backed analytics
queries: session engagement KPIs, acquisition sources, event/event-by-page
breakdowns, device split, navigation flow, and multi-step funnels, exposed as
new MCP tools. Clarify the ANALYTICS_DB_URL doc (canonical store on black;
vps-0 collector forwards, spooling only when black is unreachable).
Add NJ NANPA area codes (201/551/732/848/908/973/862/609/640/856) to the
NYC market map, and extend the NYC metro geo-alias rule to match New Jersey,
Jersey City, Newark, Hoboken, and Long Island for both city resolution and
asked-to-come recall. Covered by geo + geo-aliases tests.
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>