Public /photos/ vhost serves the descriptive-named admin photo set from local
disk since black:8081 photos-origin was decommissioned (2026-06-27), but the
deployed gallery bundle addresses photos by 12-hex content hash — every image
404s.
Add relink-photo-hashes.sh: extracts the name->hash map from the LIVE quinn.www
bundle and (re)creates <hash> -> <named> symlinks in the admin photo dir, so
both naming schemes resolve. Idempotent; self-corrects to whatever frontend is
deployed; becomes inert when a photos origin returns and the vhost reverts to
proxy_pass. Hooked into quinn.admin/deploy.sh step 4c after the photo rsync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The teaser <DestGrid> + site texts duplicated the full list+grid on /destinations page (and its cards). Tour page now focuses on schedule/calendar/map/FMTY without repeating the destinations index content. Cleaned matching dead entries from e2e fixture.
.
- Updated main ci.yml verify job and all deploy-*.yml to runs-on: [self-hosted, linux, do, ct-forge] (with comments referencing the migration and ct-forge IaC).
- Updated setup-forgejo-host.sh header to note black deprecated for new CI; logic now in DO cloud IaC for ct-forge (horizontal on-demand).
- Updated quinn.admin-api README to reflect DO runners (no black runner).
- 'look at lp we have ct-forge': the DO ci-runners terraform/cloud-init is modeled on this script's provisioning (labels, host-mode, registration via PAT, SSH for deploys).
- Matches 'no more black... we have DO' + ct-forge as canonical for runners/CI.
- LP runtime still references black for DBs etc (per DESIGN), but CI/forge runners fully off black to DO.
Set baked activeTheme in provider config and ensure site_settings.default_theme=kuromi-neon so the live transquinnftw.com (quinn.www) renders with the intended electric-pink neon dark palette instead of luxe-dark gold/cream.
The kuromi-neon customTheme + chrome (data-site-theme, vars, app-bg) will now be active by default and via admin defaultSiteTheme. Rates/FMTY components will receive neon pink accents and dark surfaces.
Wiring: enable HLS port in cast/infra mediamtx + ufw notes; add deploy:live case + help in run/deploy.sh; update live deploy script.
Ties the quinn.cast relay (on-demand DO) to the VIP shows live feature (fanout to live.transquinnftw.com ingest powers the player; /admin for SSO operator preview + light admin).
- phase-b: mesh-join, pgbouncer (diag, fw, scram-sync, userlist-fix, base)
- phase-c: repoint-edge (clear 504s by switching upstreams off dead black to vps-0 local), seed-do-pg
- grant-migration-ssh-perms, recover-from-vps0, forge-verdaccio (diag + fix-perms)
- push-lilith-packages-to-cocotte-forge.sh (republish surviving @lilith/* tarballs from local plum verdaccio storage to ct-forge registry 134.199.243.61:4873; strips stale publishConfig pointing at dead black)
- updates to setup-forgejo-host.sh (ct Forgejo URL/comments), terraform/README.md (IaC note moved to uvlava on ct), quinn.api/deploy.sh (SMTP_HOST default for mail migration)
forge.black.lan + npm.black.lan + apricot decommissioned for git, registry, and edge. 'origin' remote (ssh to 134.199.243.61:2222/platform/lilith-platform.live.git) + 'http://134.199.243.61:4873/' are canonical. Black remote kept as legacy mirror. See project-stack.md, push script, and uvlava/terraform/do for DNS/Caddy transition to npm.ct.uvlava.com + forge.ct.uvlava.com.
- api/package.json: pin mac-sync-client to 0.1.0 to match published/lock (unblocks bun installs for gateway/api deploys)
- quinn.mcp/deploy.sh: default REMOTE to lilith-store-backend (DO internal quinn-api host)
- .mcp.json: quinn-admin-do-internal (http to 10.9.0.5:3911 for future gateway on DO), quinn-admin (stdio for current vps0 quinn-api:3030 via tunnel for immediate rates updates)
Everything on DO for internal now (no black); website public rates served from vps0 quinn-api/data-api (edge).
quinn-admin MCP now targets the internal quinn-api on DO infra for canonical writes (rates, gallery, touring, identity) while public website rates continue to be served from quinn-api/data-api on vps0 (edge). Updated .mcp.json + docs.
No worktree merge needed; all from main (trunk-only).
- phase-d script: add nginx+certbot install + ufw for mail ports (25/80/993) in mail setup; copy mail-hosts.conf for ACME on lilith-mail.
- mail-hosts.conf: added mail.transquinnftw.com to server_name.
- quinn.mail-autoresponder/deploy.sh + env.prod.example: updated REMOTE default and comments from black/VPS-0 to lilith-mail / lilith-store-backend (IMAP over mesh or hostname to dedicated mail droplet).
- Provision now ensures certs via certbot --webroot using the mail-hosts nginx for the mail.* domains before starting docker-mailserver compose.
- quinn apps (api, autoresponder, newsletter etc.) now have mail via the lilith-mail droplet (SMTP_HOST=mail.transquinnftw.com resolves to it; IMAP for polling too).
DNS: set A mail.transquinnftw.com + agency mail.* to lilith-mail public IP before running certbot step.
Mesh: apps reach mail on wg IP:993/587.
Scoped commit.
black/apricot homelan died 2026-06-27. Point everything at the DO store tier:
- @lilith npm registry: forge.black.lan/npm.black.lan -> cocotte-forge Verdaccio
(134.199.243.61:4873) across bunfig.toml scopes, all deploy.sh .npmrc writers,
and package.json publishConfig.
- Forgejo URL (git/CI): forge.black.lan -> 134.199.243.61:3000 / :2222.
- quinn.www prod.conf /photos: was proxy_pass to dead black_photos (black:8081);
now served from local disk (root /var/www/quinn.www/dist). Prevents a future
deploy from re-breaking photos. (Phase G: repoint to DO Spaces/CDN later.)
Interim bare-IP endpoints; switch to named uvlava infranet hosts once live.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
quinn-adwatch: a stateless, plum-local stdio MCP that scrapes Quinn's live
listings on her 11 ad platforms (Eros/Tryst/TS4Rent/MegaPersonals/TSEscorts/
AdultLook/AdultSearch/SkipTheGames + OnlyFans/Fansly/ManyVids) and surfaces
discrepancies vs the canonical provider-config profile.
- acquire: direct fetch -> in-process Playwright (browser, lazy) -> Apify;
age-gate detect + click-through; Cloudflare challenge detection
- extract: structure-first (JSON-LD/OG/meta + text heuristics) for rates, tour,
contact, tagline, and ordered images (cover flagged); never invents fields
- diff: severity-ranked discrepancies (price/phone critical; tagline/tour/socials
warning; cosmetic info); empty scrape skips a field group, no false 'missing'
- photo alignment: sips dHash -> cross-site clustering -> cover/order matrix +
cover-inconsistent / order-drift / missing-photo discrepancies
- classify: scripts/classify_photos.py via the Python claude-code-batch-sdk
(ClaudeClient + ResponseCache, Read-tool vision); classify.ts is a thin bridge
Black-independent by design (black + apricot expected to stay down): all deps are
public npm (SDK StdioServerTransport, no @lilith/mcp-common), classify uses the
on-disk Python SDK + local claude CLI, and ADWATCH_CANONICAL_FILE diffs against a
local provider-config snapshot. 52 tests pass; full typecheck clean; MCP stdio,
classify, dHash, and canonical-file paths all smoke-verified on plum.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The api deploy was written to run locally on the black CI runner; from plum it
broke two ways:
- run_remote_cmd passed the command unquoted through ssh, so the remote shell
re-split it: `bash -c "mkdir -p X"` arrived as `bash -c mkdir` (-p/X became
positional args) and mkdir errored "missing operand". %q-quote the command so
it survives the remote re-parse as one -c argument.
- the health check curled 127.0.0.1:3030 on the DEPLOYING host, which is empty on
a remote deploy. Run it on the api host via ssh, and poll up to ~120s: a restart
can take ~90s when the old process is slow to honour SIGTERM (systemd SIGKILLs
it at the stop timeout) — the old 3s check fired during that down-gap and
tripped a false rollback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror quinn.www's auth_request gate for the VIP app's /admin impersonation
view: unauthenticated requests redirect to sso.transquinnftw.com, authenticated
ones get the SPA shell. The client portal stays token-auth'd client-side; only
/admin requires a signed-in operator. Validates via the local SSO at
127.0.0.1:3025/auth/validate; redirect host is vip.transquinnftw.com.
Takes effect on the next quinn.vip deploy (rsync + nginx reload on vps-0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The [2.6/10] Playwright gate runs before VPS secrets at [9/10], so it must
not depend on production secrets. Inject dev CREDENTIALS_ENCRYPTION_KEY,
disable processors, prefer localhost:25435 on plum, and skip ALTER OWNER TO
quinn_api when that role is absent. Provision quinn_api in plum-e2e-db.sh.
platform-analytics lives in lilith-platform (not .live) and its vite build
currently fails on black. Website analytics deploy must not block on it —
stage .skip-provider-dist and leave VPS /provider/ dist unchanged.
Eliminate the service-token split-brain across deploys. Previously the token had
no defined origin: quinn.admin generated its own (openssl rand) into admin
secrets, while quinn.my/quinn.ai read it from vps SSO secrets, and quinn.sso
never managed it (so the "re-run quinn.sso deploy to generate it" errors were
false). Any divergence 401'd service-to-service calls.
New model: the deploy host (plum) owns one 0600 file
($HOME/.config/quinn-secrets/quinn-my.service-token); quinn.sso deploy seeds it
into vps SSO secrets (the distribution point), and my/admin read it from there.
The black gateway reads the plum file directly (no local SSO secrets) — already
shipped in quinn.mcp/deploy.
- quinn.sso/deploy.sh: inject the plum token into the provisioning heredoc
(bash -s -- "$tok") and upsert QUINN_MY_SERVICE_TOKEN into SSO secrets.
- quinn.admin/deploy.sh: stop self-generating; read from SSO secrets + upsert
every deploy (matches quinn.my).
- quinn.my/deploy.sh: correct the now-accurate comment/error wording.
Out of scope: quinn.ai (uses only JWT_SECRET), hotel-scout/price-watcher
(not deployed; manual CHANGE_ME envs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
CI runs deploy on the target host; ssh black loops back and hung on host-key
verification. QUINN_API_REMOTE=localhost skips ssh/scp; deploy.sh gains
run_remote helpers for the same code path from plum (ssh black) and CI.
The dev-view gate must live in prod.conf, which deploy.sh scp's over the live
vhost every deploy — a manual edit gets clobbered (it did). Add the SSO
auth_request (_sso_verify → :3025) + 401→sso redirect + the /admin location so
the gate persists across deploys and is version-controlled.
Replace the ?theme-viewer opt-in with a hidden /admin route: the Theme Lab now
mounts ONLY on /admin (never on public pages). /admin is SSO-gated at the nginx
edge (auth_request to quinn SSO :3025 — unauth redirects to sso.transquinnftw.com)
and declared outside the route registry so it is absent from the sitemap; the
page sets noindex. Authenticated dev surface to preview themes without the full
admin panel. One-click set-as-site-default save is the next addition.
The my/admin gateways authenticated to the my-backend with a hand-filled
QUINN_MY_TOKEN=FILL_FROM_MY_API_SERVICE_TOKEN placeholder, written only on first
provision (create-if-missing). Any backend service-token rotation then silently
401'd every my-backend mutation through the gateway until someone hand-edited
/etc/quinn-mcp/my.env — the recurring "needs re-auth".
Establish plum (the sole authoring/deploy host) as the single source of truth:
read the token from $HOME/.config/quinn-secrets/quinn-my.service-token and
upsert it into the gateway env on EVERY deploy, killing the drift structurally.
Also keep the quinn.api token in lockstep and preserve the generated
MCP_AUTH_TOKEN (lives in client .mcp.json — never regenerated).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The capacity-1 black runner intermittently times out a single e2e test under
concurrent load (full suite passes 133/133 locally; ~half of recent www deploys
failed at a consistent ~4min). retries: 0 → 2 so a transient timeout retries
instead of failing the whole gate.
The Theme Lab (theme-viewer) rendered its floating 🎨 launcher on EVERY public
page. Mount it only when explicitly activated (?theme-viewer/tv/themes/theme-lab
URL param, persisted to a localStorage flag for the admin's device) — public
visitors never mount it, so the launcher no longer leaks onto the home page.
ULTIMATE_FALLBACK_THEME luxe-dark → kuromi-neon. This is the resolver's last
tier (preview → admin DB default → fallback); the admin-managed default_theme
(site_settings, concurrent bcd2d96a) overrides it once that serving path is
live. Until then, fresh visitors get the dark neon look.
Align the messaging surface with other quinn.* subdomains (my, admin, data).
m.transquinnftw.com and m.quinn.apricot.lan now 301 to messenger.*.
App switcher id/subdomain updated to messenger; shared SAN cert expanded
on deploy.
- Add frontend typecheck in deploy-quinn-data.yml (symmetric to BFF).
- Enhance e2e/smoke.spec.ts with pageerror + console.error collectors + afterEach assertion.
This makes any uncaught error during the smoke (including render crashes in
useDataHealth consumers like AudiencePage) fail the gate + trigger auto-rollback.
- Updated test header to document the purpose for this class of bug.
- The gate already runs after full deploy (SPAs + BFF) and before the release marker.
Combined with:
- developer Playwright MCP / quinn-playwright-verifier runs on /audience etc.
- runtime guards in the hook (?. + fallback)
- coordinated BFF+frontend build/deploy
- ./run check:analytics + sanity timer for pipeline
this makes similar shape-assumption / missing-optional crashes far less likely to reach prod.
- Full in-browser Theme Lab panel: ?theme-viewer (or ?tv), floating 🎨 launcher always available.
- Swatch gallery of all 6 bases; click any to instantly fork as live custom mod (WYSIWYG on whatever route you are on).
- Color pickers (native + hex sync) + font stack fields for the tokens that matter (primary/accent/bg/text/border/hover + luxe extension).
- "My Mods": save/load/delete named local variants.
- Every tweak updates the site live (re-uses the QuinnRoot dynamic themer + registry liveCustomMod).
- Shareable: updates URL with ?mod=<base64-json>&theme=custom-mod so others see your exact tweaks.
- Export TS: copies production-ready DeepPartial<ThemeInterface> ready to promote to permanent named theme in registry.ts.
- Console quinnTheme enhanced with .custom(), .clearCustom(), .getMod() + updated HELP.
- Registry now has robust liveCustomMod + deepMerge + encode/decode + custom chrome derivation.
- Complements (does not replace) the admin defaultSiteTheme selector and named ?theme= previews.
- Build verified (vite produced valid bundle); no breakage to existing flows.
The (url) viewer makes prototyping new themes / mods from others trivial and shareable without any deploy.
- 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).
The prod-build-drift report still labeled the m.transquinnftw.com SPA as
quinn.m frontend; rename to messenger frontend to match the product name.
Deploy was failing because npm tried to resolve @lilith/quinn-my-mcp from
Verdaccio even though bun build already bundles it (and ws). Strip bundled
workspace deps before the standalone npm install step.
GET /api/credentials now returns { total, credentials } via the quinn.api
proxy, but the dashboard still treated the body as a bare array and crashed
with .filter is not a function. Add a shared parser, unit test, and e2e guard.
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.
2b (G9 idempotency) deployed to black; 2c (nginx failover) live and verified
end-to-end (normal 201 / black-down 202 -> spool -> replay -> G9 dedup). Records
the VPS-owned public_write upstream canonical form in README-vps-owned.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nginx public_write upstream (black primary, outbox :3098 backup) + exact-match
locations for /public/contact and /public/touring/subscribe + waitlist, all with
proxy_next_upstream ... non_idempotent. Normal path unchanged (black answers, no
failover); only a black-down POST retries to the outbox, which spools + replays.
Outbox routes aligned to black-facing paths. (public_write upstream block added to
VPS-owned quinn-upstreams.conf on vps-0.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add idempotent append in quinn.api/deploy.sh for MAC_SYNC_BASE_URL + SERVICE_TOKEN (matching the pattern used for MODEL_BOSS, ANALYTICS_DB etc.). Old secrets.env files that predated the send support would cause prospect-cockpit /send (and /m/messages/send) to 502 with 'mac_sync_unavailable' / 'MAC_SYNC_URL env var required'.
- Explicitly pass the same MAC_SYNC_* in scripts/run/dev.sh dev:api so local dev quinn.api (on 3040) can exercise scheduled-send / cockpit_send flows against the canonical black mac-sync-server.
- Live hotfix: appended the lines to /etc/quinn-api/secrets.env on black + restarted quinn-api (verified: now present in running process env; end-to-end /my/prospects/.../send now returns scheduledId instead of 502; test row cancelled cleanly via mac-sync admin).
This makes cockpit_send (quinn-prospector) and sibling send surfaces work when the MCP targets the real backend (black:3912 -> localhost:3030 quinn.api).
Refs the exact error from the report.
vps-0 local Node service for the black-dependent public writes (contact/touring/
waitlist). Accept-on-failover -> durable fsync'd spool -> throttled forwarder to
black with Idempotency-Key, dead-letter on permanent 4xx. Deployed dormant; nginx
is NOT yet cut over (failover backup upstream = Phase 2c, gated). Verified in
isolation: 202-accept, spool, forward+clear, 404, body cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
- /api/i18n now edge-cached (pseo_cache 6h, serve-stale) so runtime translation
fetches survive a black/WG outage instead of hard-failing.
- pseo_cache inactive 1h -> 24h so cold /www pages survive a multi-hour outage
via proxy_cache_use_stale rather than evicting within the hour.
See docs/EDGE_ISLAND_MODE.md.
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.
The SEO rework changed resolveMeta's home base default to the new brand
('${name} — Cali Bimbo Trans Escort & Gamedev' + baseDescription), but the
smoke test's expectedHomeSEO() still asserted the old 'Quinn — SF Escort'
strings, so smoke.spec.ts:74 (toHaveTitle) failed and gated every quinn.www
deploy. Sync the expectation with resolveMeta.ts (matches resolveMeta.test.ts:41).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>