Build-order steps 1-2 of docs/prospector-of-redirect-spec.md (pure functions,
no I/O, no send). classifyDecline() separates curious rate-askers (handled by
isBudgetBalker — quote, no redirect) from soft can't-afford vs lowball/haggle;
lowball wins ties (counter-number → disengage). rateAlreadyQuoted() is gate #1
(post-quote only, outbound-scan). Rotation pools are Quinn's verbatim approved
copy with a [link] token filled at staging time (never hardcoded). 31 tests.
LIVE sending, engine_drafts staging, of_redirected_at migration, and the
local-model/worker rails remain Quinn-gated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Complete the autoQueue toggle wiring: the api facade getSendRateLimit/
setSendRateLimit and /m/messages/send-rate-limit GET/PUT now carry autoQueue,
and the MCP client's setSendRateLimit passes it. Pairs with the MCP
set_send_rate_limit tool's autoQueue input (added separately) and the
mac-sync send_rate_config.auto_queue column.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the mac-sync send-rate cap (max N sends per window) through to the
quinn-messenger MCP so it's adjustable at runtime, routed through quinn.api
per the canonical messenger→quinn.api→mac-sync path.
- api: shared/mac-sync/send.ts gains getSendRateLimit/setSendRateLimit
(direct call to mac-sync /admin/send-rate-limit, deadline-guarded);
/m/messages/send-rate-limit GET/PUT surfaces them.
- mcp: get_send_rate_limit / set_send_rate_limit tools call those routes.
Backing cap + storage live in the mac-sync server (default 10/300s).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Added AdultSearch and SkipTheGames (the remaining verified:true escort platforms from the credentials/platforms list).
- All content (OnlyFans etc) and relevant escort platforms from the user's quinn-my credentials list are now in verified_profiles with site logo as placeholder banner.
- Total 10 entries.
- Updated seed for consistency.
- Verified in public provider-config data.
- Added 4 new rows via admin API (black) + psql (edge) so /banners now shows 8 platforms.
- Used /icon-512.png (website logo) as imgSrc for the new entries (triggers branded logo visual per previous requirement when custom banner not supplied).
- Updated seed-quinn-iter16.ts for dev/test consistency.
- Verified in provider-config data (local + public https).
- Platforms from user list + content/escort handoff data; URLs from canonical sources.
- Added real verified profile rows to canonical (black) and edge (vps) quinn.verified_profiles via direct admin surface + psql for cache.
- Updated seed-quinn-iter16.ts with matching real data (e2e marker preserved).
- Verified via public /www/provider-config and admin surface.
- Legacy quinn_admin table also synced on edge for fallback paths.
- Note: some banner imgs use site photos until platform-specific embed banners are added; hrefs for non-Tryst may need minor URL tweak post-verify.
- add site-settings singleton to admin registry + schema + migration
- add editor config + route + nav in admin frontend
- surface defaultSiteTheme via data-api serialize + shared types + validator
- carry through api /www/provider-config (the public edge-cached path on vps0)
- remove DEFAULT_SITE_THEME hardcode; ultimate fallback luxe-dark; registry comments updated for admin-driven live selector
- live bootstrap in quinn.www root + data hook to pick admin default without quinn.www rebuild (chrome + tokens update post-fetch)
- fixed incidental sortable test assertion to match current registry (pre-existing mismatch)
- other public hardcodes remain in deployment configs; see analysis
This makes the visitor-facing default theme choice Quinn-editable via admin UI and flows as public data through the quinn.api public surface (edge cacheable).
Gallery item DELETE calls regenerateManifest(), which requires
PHOTOS_DIR. Point tests at a temp directory in global-setup so the
admin-gallery-items CRUD test does not 500 in CI.
assembleProviderConfig now reads hero_strip_items; admin rate-cards,
site-text, and tour-stops tests were still on stale migration bundles.
www/payment-methods tests must query ?provider=quinn to match repo
defaults. Run each test file in its own bun process so the per-process
throwaway DB does not leak committed fixtures across files in CI.
CI verify was failing on black because integration tests omitted migrations
added after provider-config and admin gallery evolved (payment_methods,
photo_css_traps, analytics_markers). Centralize those bundles and bump
the verify job to 45m with a 90s per-test timeout so the full DB suite
can finish on the single capacity-1 runner.
Import GeoGranularity from geo.ts (not client.ts) so analytics MCP
typechecks. Tighten contact-form test mailer stub for
exactOptionalPropertyTypes. Replace grep -P in ./run ci:status with a
portable python parser against the Forgejo actions API.
CI verify only typechecked — the contact-form refactor dropped the required
`from` on sendMail (bookings already sets it) and nothing failed. Add the full
@features/api suite to ci.yml and tighten the contact-form test to assert
`from` plus a fire-and-forget flush tick.
- contact form: now uses same pattern as bookings (persist first, fire-and-forget bounded send)
- VIP unlock confirm (payments received, including wallet_topup): added decoupled email to Quinn on billingEntry write
- VIP priority requests: added notification on creation
- VIP quotes respond: improved from console.* to logger + withTimeout
- Extracted shared/timeout.ts (with unref) and updated bookings to use it
This ensures Quinn receives emails reliably for contact submissions, payments sent/confirmed, and VIP client activity without transient SMTP issues affecting the UX or dropping leads.
Additive nullable column + unique index + createContactSubmissionIdempotent
(ON CONFLICT DO NOTHING, returns existing row, skips notify email on replay).
Route reads optional Idempotency-Key header. Lets the edge outbox replay a
contact submission without creating a duplicate. Backward-compatible: direct
submissions (no key) insert normally. touring/waitlist already natural-idempotent
(UNIQUE(email,provider_slug) upsert), so contact is the only table needing this.
NB: hCaptcha is effectively disabled (frontend sends no token), so stale-token
replays are not rejected; if hCaptcha is ever enabled, add a trusted outbox-token
bypass for replays.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace stubbed audience geography with session_fingerprints queries
(country/region/city, pathPattern, activityWindow for live). Surface
US states, cities on Audience/Overview, page detail, and dashboard
snapshot. Extend quinn-analytics MCP to v0.3.1 with geo_breakdown,
audience_geo_summary, and geo_enrichment_status.
Deploy dashboard/API from main checkout — see handoffs/analytics-geo.md.
MCP v0.3.1 already on black :3914.
Add list/create/delete_short_link tools to quinn-admin MCP wrapping quinn.api
/admin/short-links. Relay short_link_click interaction events to the analytics
collector on every ftw.pw /s/:code redirect (alongside existing click_count).
Black prod had the table owned by the quinn superuser role, which blocked
the build/release CHECK migration and crashlooped quinn-api. Idempotent OWNER
fix is applied in initial, build_release, and a follow-up migration.
Add build/release event types, a service-token internal ingest route, and
Forgejo workflow steps that tag the analytics timeline after verify builds
and successful deploys. Dashboard chart shows gray build lines and green
release lines.
While apricot is down, deploy admin SPA + API to black at
admin.quinn.black.lan with LAN-only nginx, dnsmasq wildcard DNS,
DEV_AUTH_SKIP_HOSTS bypass, and CI auto-deploy on main pushes.
Integration test against a real Postgres exercising the full M1 flow:
- methods list returns only vip_unlock_enabled rows and a sanitized shape
(no legal-name / visibility / provider fields ever surface to the fan)
- a method not enabled for unlocks is rejected (CashApp gating)
- token header auth and service-token admin auth are enforced
- intent creation returns awaiting_payment + memo code + handle instructions
- admin confirm records exactly one paid ledger entry and is idempotent
under a duplicate confirm (no double-bill)
6 tests, green in ~2s.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the token-scoped VIP unlock surface and mounts it:
- client routes (X-VIP-Token header auth): list vip_unlock_enabled methods
(mirrors the footer's managed list), create/list/get unlock intents with
per-method payment instructions and a memo code for reconciliation.
- admin routes (service-token, guarded at /vip/unlock-admin/*): confirm a
received payment -> records a paid vip_billing ledger entry exactly once via
the atomic transition; cancel an open intent.
Registers unlockIntentMigrations after paymentMethodMigrations (FK ordering)
and adds the /vip/unlock-admin/* serviceTokenAuth guard alongside billing-admin.
Methods are off by default and only handle/label are surfaced — never a legal
name — so CashApp stays gated until the name change via a single admin toggle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A fan-initiated request to pay for VIP content access — the spine of the
unlock flow. Settlement is polymorphic on the chosen payment method's kind
(peer_app: memo-matched manual confirm; crypto: BTCPay webhook in M2).
confirmUnlockIntent is atomic and returns { transitioned }: the UPDATE only
fires while the row is still awaiting_payment, so concurrent confirms (manual +
future webhook) cannot double-settle, and the caller gates the paid-ledger
insert on the transition so it runs exactly once. Includes idempotent
re-confirm, close (expire/cancel), and a bulk stale-expiry sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds two columns to payment_methods, managed on the same admin page that
feeds the quinn-www footer:
- vip_unlock_enabled: second availability axis (independent of visibility),
controlling whether a method is offered for VIP unlock payments. Off by
default so a fan can never be charged through an un-opted-in method.
- exposes_legal_name: advisory flag driving an admin warning (e.g. CashApp
shows the account legal name on its own send screen); never rendered by us.
Migration is additive; the footer's visibility axis is untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an analytics-marker entity (types/schema/repo + migration) and a
record-analytics-marker helper, with an admin CRUD surface + AnalyticsMarkersPage
(nav in AdminLayout). Auto-record markers when tour stops and content drops are
created/changed. Expose them via /analytics/markers (analytics-queries +
user-data router proxy + useMarkers hook + api types), and render them as
reference lines with a legend on the dashboard Overview trend chart.
Include animation in the public /www promo-banner payload so the rotation can
render it, and cover create/update round-trip of the field in the repo test.
Move the prospector cockpit logic into the published @lilith/agent-prospector
package (^0.4.0) and reduce the MCP to a thin adapter — index.ts/client.ts shrink
by ~900 lines. Bump to 0.5.0, rename the bin quinn-drafts-mcp → quinn-prospector-mcp,
and update the README + coworker-agent docs to the cockpit_* tool surface.
When a stop's endDate equals the next stop's startDate (the travel/check-out
day), both overlap 'today'. Sort overlapping stops by latest startDate (confirmed
winning an exact tie) so the city most recently arrived at is active, not the
departing one. Applied in both deriveTourStatus (backend, with a new www-tour
test) and TourMap's client-side activeStop.
Add an animation field (none|roll|marquee|flip) to promo banners: types +
const list, an idempotent migration adding the column (DEFAULT 'none') and a
separately-applied CHECK constraint, repo read/create/update plumbing, and the
admin draft/patch zod schemas. Existing banners keep the legacy slide-up.
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).
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.
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>
- 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>
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>
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>