Commit graph

2560 commits

Author SHA1 Message Date
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
9b68a27f9d docs + test: update VIP review with explicit test coverage audit + added unit test for timeout
- Updated the review doc with precise "what is tested" section based on live execution of wallet.test (5/5 ephemeral DB), contact service test (4/4), new timeout.test (4/4).
- README summary refreshed.

This directly addresses the verification request for unit/snapshot/integration/ephemeral DB proof of the claims.
2026-06-22 04:44:25 -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
445bfcae57 docs(vip): comprehensive review of VIP feature — payments/unlocks/wallet integration, security model, two user experiences + capabilities, cross-feature integrations. Illustrative flows and journeys included.
New file: vip-feature-review-payments-security-ux.md (detailed review + mermaid-like text flows, step-by-step client/admin examples, data model notes, state/gaps).
Updated: README.md (links the review + high-level summary of the two UXes, security, integrations post-M1/M1.5 + MCP content work).
2026-06-22 02:50:44 -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
6bf26998d6 feat(mcp/quinn-admin): add MCP tools for uploading content (upload_gallery_photo) and making posts (content drops with buy links + published_at for retro platform dates)
Supports the VIP prepaid wallet channel: create posts whose gallery media can be unlocked via balance-purchase or intents (targetRef to drop).

Also synced payment_methods tools for vip_unlock_enabled.
2026-06-22 02:21:19 -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
ef339b3cea fix(provider-website): guard indexed access in EtiquettePage luminance helper
The surfaceLuminance regex-match access m[1] is string|undefined under
noUncheckedIndexedAccess; guard with ?? '0' so the flourish typechecks clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 01:04:26 -05:00
Natalie
da332a3c86 feat(provider-website): token-driven theming for EtiquettePage flourish
Make the closing flourish auto-tune per site theme (sunny pink+sunshine Malibu
card on barbie-light, glowing pink on dark moods) with glow intensity adapting
to surface luminance (surfaceLuminance helper).
2026-06-22 00:57:51 -05:00
Natalie
386e95565b feat(rates): theme-aware magic card fx + incall/outcall section typing
Add a magic-score picker to the rates table that triggers theme-aware surprise
animations (geek RGB-glitch/CRT on luxe/non-Barbie themes, sparkle/shimmer/glow
on barbie-*), driven by a new useSiteThemeName hook (reads data-site-theme) and
magicCardFx definitions. Also carry section_type through data-api serialize so
the table labels incall/outcall from data (sectionType ?? inferSectionType).
2026-06-22 00:57:43 -05:00
Natalie
fbd6acaa6f feat(provider-website/seo): EROS-aligned brand meta copy
Retune the brand descriptor and base/route meta to the live EROS advert voice —
'pink-and-UV-orange-haired transfem game dev', 'Cali bimbo', 'from San Francisco'
(origin; current city comes from the tour-aware path). Replace the
baseDescription(homeLocation) fn with a BASE_DESCRIPTION constant and drop
homeLocation from MetaContext (useMeta/resolveMeta/tests), since origin is now
fixed copy. 'Gamedev' → 'Game Dev' throughout; index.html fallback kept in sync.
2026-06-22 00:57:34 -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
7a1534991e fix(ci): unblock typecheck for messages Thread and my frontend
Thread.tsx imported iMessage primitives from @lilith/ui-messaging where
they are not exported; point at @lilith/ui-imessage instead. Add
skipLibCheck to my/frontend-public and raise the tsc heap in
typecheck-all.sh so the CI sweep no longer OOMs on large frontends.
2026-06-22 00:10:22 -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
d34df62b4a fix(provider-website): use theme easing token in EtiquettePage shimmer
Swap the literal 'linear' in the ClosingTitle shimmerText animation for
theme.easing.linear.
2026-06-21 23:03:07 -05:00
Natalie
4e35e85590 fix(provider-website): use theme easing token in EtiquettePage Emblem float
Swap the literal 'ease-in-out' in the Emblem floatY animation for
theme.easing.easeInOut, matching the Spark twinkle.
2026-06-21 23:00:59 -05:00
Natalie
ac70a8b5b6 fix(provider-website): use theme easing token in EtiquettePage flourish
Swap the literal 'ease-in-out' in the Spark twinkle animation for
theme.easing.easeInOut.
2026-06-21 22:59:51 -05:00
Natalie
8a3d374598 feat(provider-website): add themed closing flourish to EtiquettePage
Add a ClosingFlourish thank-you section (hot-pink aurora, twinkling stars,
shimmer heading, reduced-motion-aware) below the etiquette sections, with
siteText-editable title/body/signoff/tagline.
2026-06-21 22:58:29 -05:00
Natalie
4e13b9531d feat(provider-website/edge): island-mode form gates wired into all forms
Wrap the app in EdgeStatusProvider and gate every public form (contact, booking,
roster, shop signup, touring opt-in) behind useFormGate — when the edge oracle
reports a form's backend unreachable, render FormUnavailableNotice (routes to SMS)
instead of posting into a 502. Serve the oracle at /edge/status.json from
nginx (alias to the watcher's state file). Fail-open throughout. Adds
EdgeStatusContext tests; marks Phase 1b in EDGE_ISLAND_MODE.md.
2026-06-21 22:58:19 -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
6f195474e3 feat(provider-website/edge): EdgeStatusContext island-mode form gates
Add EdgeStatusProvider + useFormGate, reading the vps-0 edge watcher's
/edge/status.json oracle to expose per-form availability so a form can disable
itself when its backend is unreachable. Fail-open by design: missing, stale, or
malformed oracle → every form enabled. Phase 1b consumer of the edge-watcher.
2026-06-21 22:12:40 -05:00
Natalie
fa016852e0 fix(provider-website): trim index.html base meta description
Drop the redundant 'Based in San Francisco,' from the static index.html meta
description (the resolver already supplies location context per route).
2026-06-21 22:11:18 -05:00
Natalie
d3341012dc feat(analytics): surface degraded-pipeline notice on Audience
Render DataDegradedNotice on AudiencePage when audience/segment data is empty
while raw-event ingest is active (via useDataHealth), matching Traffic/Network.
2026-06-21 22:11:10 -05:00
Natalie
00b6329e4e feat(my-api/health): report build stamp on GET /health
Mirror the api/sso build stamp on quinn-my-api: inject __BUILD_INFO__ via bun
build --define in deploy.sh and return version/buildCount/sha/builtAt + service
+ startedAt as JSON from /health (was bare 'ok'). Falls back to env then 'dev'.
2026-06-21 22:10:47 -05:00
Natalie
1d72c537ed feat(provider-website/seo): centralize per-route meta into routeMetaTable
Move per-route SEO meta out of the inline RouteMeta in registry.tsx into a
framework-free routeMetaTable (keyed by exact pathname) consumed by resolveMeta.
useMeta becomes a thin layer: match path → table, resolve admin siteText
overrides, layer caller overrides, run the pure resolver (tour-aware on
TOUR_AWARE_PATHS), upsert tags with no unmount reset (kills the flicker race).
Every static page drops to a bare useMeta(); entity pages pass titleFull. Add
resolveMeta/routeMetaTable/useMeta tests and refresh the index.html base copy.
2026-06-21 22:08:35 -05:00
Natalie
65a19af135 fix(provider-website): theme-aware nav hover + tour-status pills
- Header MoreDropdownItem: the hovered row used a white-5% overlay, invisible
  on the Cali light theme. Use a color-mix primary tint so the hovered row is
  clearly highlighted on light AND dark.
- HeroStrip StatusPill: same neon-on-dark colours that washed out on light;
  blend each status hue toward the theme text colour (matches the Badge fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:36:25 -05:00
Natalie
0afb11f134 fix(provider-website/Badge): theme-aware status badge colours
The semantic badges (available/active/sold-out/…) hardcoded light-on-dark
colours (e.g. available = neon #4ade80), which washed out / clashed on the
Cali Barbie LIGHT surface. Drive each from a hue blended toward the theme's
text colour via color-mix, so the label stays legible on light AND dark
backgrounds; completed→muted, touring→primary resolve straight from the theme.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:53:26 -05:00
Natalie
c1afe89588 feat(provider-website,admin): per-banner promo animation + auto-rotation
Adds the frontend half of the per-banner promo animation feature: an
Animation selector (none/roll/marquee/flip) in admin banner management and
an auto-rotating public bar that replays each banner's configured entrance.
Backend (column/migration/surfaces) already landed separately.

- admin: animation dropdown + list badge
- public: 6.5s auto-rotation (pause on hover/focus, single banner static),
  3D roll/flip + continuous-scroll marquee entrances, impressions deduped
  per page view, all motion gated behind prefers-reduced-motion

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 18:32:01 -05:00
Natalie
8de30ae986 fix(quinn.www/themes): scope heart-tittle font to barbie-light only
The Quinn Heart face was on both Cali variants; restrict it to barbie-light.
Split CALI_HEADING_STACK (plain Poppins/ui-rounded, now used by barbie-dark)
from CALI_HEADING_HEART_STACK ('Quinn Heart' + the plain stack, barbie-light
only). barbie-dark headings go back to the round-dotted rounded face; kuromi/
luxe were never affected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 17:57:48 -05:00
Natalie
7e1b046293 feat(provider-website/seo): tighten booking route meta copy
Shorten the booking route title/description to 'Booking' / 'Reserve a date with
Quinn.'
2026-06-21 17:39:57 -05:00
Natalie
7e684a1020 feat(analytics): surface degraded-pipeline notice on Network
Render DataDegradedNotice on NetworkPage when IP-classification data is empty
while raw-event ingest is active (via useDataHealth), matching TrafficPage.
2026-06-21 17:39:50 -05:00
Natalie
dff1918251 feat(provider-website/seo): add route meta for booking page
Tag the booking route with RouteMeta namespace/title/description.
2026-06-21 17:37:55 -05:00
Natalie
8ac6259586 feat(analytics): surface degraded-pipeline notice on Traffic
Add DataDegradedNotice and render it on TrafficPage when acquisition data is
empty while raw-event ingest is active for the window (via useDataHealth) — so a
broken enrichment pipeline shows a warning instead of an innocent 'no data'.
2026-06-21 17:37:04 -05:00
Natalie
ea6fea4383 feat(provider-website/seo): add route meta for tour + destinations
Tag the tour (tour-aware) and destinations routes with RouteMeta
namespace/title/description, building on the resolver core.
2026-06-21 17:36:58 -05:00
Natalie
03961e1eb8 fix(sso): return claims JSON from /auth/validate
nginx auth_request and status-only consumers ignore the body, but the
@features/api monolith's ssoRequired parses it and 401s on an empty/non-JSON
body — the empty 200 was the other half of the my/admin SSO login loop. Return
{ sub, admin } instead.
2026-06-21 17:36:52 -05:00