Commit graph

782 commits

Author SHA1 Message Date
Natalie
925b2a1923 fix(ci/deploy): local-remote helpers for black-runner deploy jobs
Forgejo runs admin-api and admin-black-dev deploys on black; ssh black
hangs in act's clean ~/.ssh. Shared local-remote.sh + REMOTE_HOST=localhost
in those workflows. run-tests: await spawn exit, log and fail on any file.
2026-06-24 19:53:37 -04:00
Natalie
88ffdf1e35 fix(api/tests): isolate PHOTOS_DIR on CI runners; add designer migrations
Self-hosted runners inherit host quinn-api PHOTOS_DIR pointing at a checkout
path that does not exist during Forgejo actions, which made gallery-items
DELETE return 500. Test preload now always uses a temp photos dir. Include
designer-download-run migrations in the template superset.
2026-06-24 13:12:26 -04:00
Natalie
3af13f3df3 fix(api/test-db): declare template env before module-load ensureProcessDb 2026-06-24 11:39:49 -04:00
Natalie
6a155be0c9 perf(ci): template DB clone, parallel test workers, split CI/deploy queues
- Build one migrated template per run-tests invocation; per-file DBs clone
  via CREATE DATABASE TEMPLATE (~seconds) instead of replaying 148 migrations
- Run up to 4 test workers on CI (QUINN_TEST_WORKERS); sweep orphans once
- QUINN_CI_FAST smoke subset (12 files) on push/PR; full suite on dispatch
- ci-${{ ref }} concurrency separate from deploy-${{ ref }}; cancel stale runs
- Cache Playwright browsers on quinn.www deploy workflow
2026-06-24 03:59:48 -04:00
Natalie
889b11039d feat(prospect-qualification): OF-redirect decline matcher + rotation pool
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>
2026-06-24 00:20:48 -04:00
Natalie
6c80e6cb12 feat(send-rate-limit): thread autoQueue through quinn.api + MCP client
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>
2026-06-23 21:38:51 -04:00
Natalie
26fa2733ec feat(quinn-messenger): MCP control for outbound send-rate cap
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>
2026-06-23 15:56:40 -04:00
Natalie
c52dd84e40 feat: sync verified profiles with full list of platforms from quinn-my credentials (platforms_escort + content)
- 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.
2026-06-23 07:45:39 -04:00
Natalie
cbb0c8b893 feat: add OnlyFans, Fansly, ManyVids, MegaPersonals to verified profiles (with site logo as banner)
- 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.
2026-06-23 07:30:46 -04:00
Natalie
98daf8def0 feat(provider-website): populate Verified on section with live verified platforms (Tryst + TS4Rent + TSEscorts + AdultLook)
- 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.
2026-06-23 06:36:04 -04:00
Natalie
bcd2d96a1f feat(quinn-admin): move default theme selector from hardcoded quinn.www constant into quinn-admin feature (public data)
- 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).
2026-06-23 04:34:35 -04:00
Natalie
71538d9f07 fix(api): set PHOTOS_DIR in test preload for gallery DELETE
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.
2026-06-23 03:45:26 -04:00
Natalie
e8f5964fa2 fix(api): complete provider-config test migrations and isolate files
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.
2026-06-23 03:34:21 -04:00
Natalie
ea75da5654 fix(ci,api): repair test migrations and raise verify timeout
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.
2026-06-23 02:31:33 -04:00
Natalie
27469549ea fix(ci): repair mcp-server GeoGranularity import and ci:status on macOS
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.
2026-06-22 21:24:39 -04:00
Natalie
5f4d192e8b fix(ci,contact-form): run api tests in CI and assert notification from address
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.
2026-06-22 21:19:39 -04:00
Natalie
5a499e2b60 fix(ftw): resolve short-link slugs case-insensitively
Normalize codes to lowercase on create and lookup so ftw.pw/s/OnlyFans
matches onlyfans. Reject mint requests that differ only by casing.
2026-06-22 07:52:00 -05:00
Natalie
372fff891f test: add unit tests for shared/timeout (withTimeout) to increase coverage of the reliable background email pattern
4 tests: resolves/rejects before timeout, timeout error, race safety.
2026-06-22 04:42:16 -05:00
Natalie
1b4dd36751 feat(notifications): make contact, VIP payment confirms, priority requests, and quote responses send emails reliably via background withTimeout + structured logging (decoupled from user actions)
- 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.
2026-06-22 02:40:30 -05:00
Natalie
fea472fe27 merge claude/intelligent-tharp-d0347c: vip prepaid balance M1.5 (top-up settlement + spend loop) plus unlock spine 2026-06-22 02:06:47 -05:00
Natalie
eae2f0ef04 feat(api/contact): idempotency_key on contact_submissions (Phase 2b / G9)
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>
2026-06-22 02:06:19 -05:00
Natalie
ae872609f1 feat(api/vip-wallet): include wallet entity types (pre-existing WIP from prior step) 2026-06-22 01:55:24 -05:00
Natalie
b947908e8f fix(api/vip): pass targetRef only when present to satisfy exactOptionalPropertyTypes 2026-06-22 01:54:24 -05:00
Natalie
bb0df73654 test(api/vip): cover the wallet top-up + spend loop 2026-06-22 01:53:21 -05:00
Natalie
bf4aa7a075 feat(api/vip): wallet top-up settlement + pay-from-balance routes 2026-06-22 01:52:45 -05:00
Natalie
4da8c9c286 feat(api/unlock-intent): add wallet_topup target type 2026-06-22 01:52:07 -05:00
Natalie
76ca3e02bf feat(api/vip-wallet): wallet balance + ledger entity 2026-06-22 01:51:27 -05:00
Natalie
fd74f16faa feat(analytics): sub-country geo — regions, cities, live 30m, MCP tools
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.
2026-06-22 00:50:02 -05:00
Natalie
6647aca29e feat(analytics): add bluesky_post marker event type
Extend analytics timeline markers with manual Bluesky posts — same pattern
as instagram_post, with sky-blue chart lines and admin form option.
2026-06-22 00:35:14 -05:00
Natalie
d1504dcf40 feat(ftw): mint short links via quinn-admin MCP and track clicks in analytics
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).
2026-06-22 00:26:20 -05:00
Natalie
6551f78c91 fix(analytics): ensure analytics_markers owned by quinn_api for migrations
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.
2026-06-21 23:59:23 -05:00
Natalie
4684ddcac7 feat(analytics): auto-record build and release markers from CI
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.
2026-06-21 23:41:53 -05:00
Natalie
e4468790f1 feat(quinn.admin): serve main-branch dev preview on black without SSO
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.
2026-06-21 23:38:06 -05:00
Natalie
4f6b0daba8 test(api/vip): cover the unlock loop end-to-end
Some checks failed
CI / verify (push) Has been cancelled
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>
2026-06-21 23:19:39 -05:00
Natalie
fcb577ebe7 feat(api/vip): wire unlock surface for intents + admin confirm
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>
2026-06-21 23:07:07 -05:00
Natalie
75ae6203fc feat(api/unlock-intent): add vip_unlock_intents entity
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>
2026-06-21 23:06:50 -05:00
Natalie
f048934e01 feat(api/payment-method): add vip_unlock_enabled availability axis
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>
2026-06-21 23:05:58 -05:00
Natalie
7fbc8696b1 feat(analytics): timeline markers (deploys, tour stops, content drops)
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.
2026-06-21 22:57:26 -05:00
Natalie
0eb69cab3c feat(promo-banner): expose animation on public surface + test
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.
2026-06-21 17:36:45 -05:00
Natalie
1235696046 refactor(mcp-prospector): delegate to @lilith/agent-prospector
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.
2026-06-21 17:35:08 -05:00
Natalie
b617f4bcb9 fix(tour): arriving stop wins on transition days
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.
2026-06-21 17:33:48 -05:00
Natalie
b798b15d70 feat(promo-banner): per-banner entrance animation
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.
2026-06-21 17:33:39 -05:00
Natalie
03d86597c0 feat(api/health): report build stamp on GET /health
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.
2026-06-21 16:24:54 -05:00
Natalie
2a449596e9 feat(mcp-prospector): cockpit tools mirroring the quinn.my Prospector UX
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).
2026-06-21 16:24:43 -05:00
Natalie
178a491304 fix(api/auth): restore /auth/refresh + /auth/logout
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).
2026-06-21 15:07:46 -05:00
Natalie
42c980efdd fix(api/migrations): run clientBookingMigrations after its FK targets
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.
2026-06-21 15:07:31 -05:00
Natalie
93316b46ca feat(rates): per-city rate cards
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.
2026-06-21 13:49:48 -05:00
Natalie
1b6ff57022 feat(provider): plumb destination & tour-stop coordinates for map rendering
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.
2026-06-21 13:49:24 -05:00
Natalie
2f2eb44d35 feat(user-data/mcp): session, acquisition, event & funnel analytics tools
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).
2026-06-21 13:48:41 -05:00
Natalie
2a50977818 feat(api/geo): resolve NJ + Long Island to the NYC market
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.
2026-06-21 13:48:31 -05:00