Introduce a shared magic score picker (geek vs sparkle by theme) wired across
public pages, balance rates incall/outcall columns, and fix pool math to track
actively mounted cards so async-loaded rate rows pick a real index. Adds
Playwright coverage for etiquette and rates hover animations.
- 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.
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).
- 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.
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.
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>
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>
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).
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).
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.
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).
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.
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>
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.
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.
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.
Render DataDegradedNotice on AudiencePage when audience/segment data is empty
while raw-event ingest is active (via useDataHealth), matching Traffic/Network.
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'.
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.
- 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>
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>
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>
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>
Render DataDegradedNotice on NetworkPage when IP-classification data is empty
while raw-event ingest is active (via useDataHealth), matching TrafficPage.
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'.