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.
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'.
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.
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.
Add useDataHealth, which reuses the overview's raw_events session count as an
'is traffic flowing?' signal. Lets pages backed by derived tables
(acquisition/audience/network via session_fingerprints) distinguish a genuine
quiet period from a broken enrichment pipeline — the failure that silently
zeroed Traffic for weeks (empty [] looks identical to no visitors).
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.
Mirror the @features/api build stamp on the SSO service: inject __BUILD_INFO__
(version, BUILD_COUNT, short SHA, UTC time) via bun build --define in deploy.sh
and surface it plus service + startedAt from /health. Falls back to env then
'dev' for unbundled runs.
A browser can carry more than one quinn_sso_session (a stale host-only cookie
shadowing the good Domain-scoped one), and the shadow can sort first. Add
extractSessionCookies (all non-empty values) and make validateSession try each
until one verifies, instead of only the first. Root cause of the
my.transquinnftw.com login loop.
Add a framework-free resolveMeta (title/description/og/canonical/robots with
tour-aware title precedence) and a RouteMeta descriptor on the route registry
(namespace for admin-editable siteText overrides, title/description fallbacks,
ogImage, noindex). Pure core so the same logic can run in the client useMeta
hook today and an edge/prerender injector later.
Add utils/tourDates.ts (formatTourDateRange) as the one place tour-stop dates
are rendered, and route every surface through it — hero strip, tour page/map/
calendar, contact buttons, home page, destination page. Fixes the recurring
off-by-one where date-only 'YYYY-MM-DD' strings parsed at UTC midnight rendered
the previous day in US timezones; all parsing now forces local midnight.
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.
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>