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.
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>
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>
Add edge-watcher.sh (vps-0 oneshot: probes every backend the public site needs,
writes a per-form status oracle for SPA island-mode, emails UP→DOWN /
escalation / recovery / weekly-heartbeat with anti-flap), its systemd
oneshot+minute timer, and an idempotent deploy-edge-watcher.sh installer.
Document the verified 2026-06-21 topology + kill-switch/outbox design in
EDGE_ISLAND_MODE.md and update FORMS_AUDIT.md (forms now routed; no runtime
auto-disable yet).
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'.