Skip to content

Release dev → main: card claim + booking + mobile /create fixes, suggested people/posts, CI/CD - #7

Merged
AtmegaBuzz merged 17 commits into
mainfrom
dev
Oct 1, 2026
Merged

AtmegaBuzz merged 17 commits into
mainfrom
dev

Conversation

@walker-null-byte

Copy link
Copy Markdown
Contributor

Promotes dev (717ef4b) to main. Merging auto-deploys prod (cards.zynd.ai) through deploy-single-box.yml; the deploy rebuilds cards-web and the cards-api container only. Persona, memory, pm2 config and Caddy aren't touched.

What ships

Cards: user-facing fixes and features

  • "Claim this card" in the profile header for the browser that published the card. It finishes the claim after the LinkedIn round-trip and then shows Edit. The old silent auto-claim is gone, as is the unused UnclaimedCardActions component.
  • OAuth callback now redirects to the public host. Prod currently sends users to https://0.0.0.0:3002/... after LinkedIn login.
  • Google Calendar booking link alongside Calendly, in onboarding, the edit page and the profile's Book-a-call card. It's stored as google_calendar_url. Booking URLs get normalized: https:// is added when missing and non-http schemes are dropped.
  • Card links: the LinkedIn and X URLs the user pasted now win over the LLM's rewrite, which was dropping suffixes like -798a6a228 and broke refresh-linkedin. GitHub's link comes from the API.
  • Resume PDF: profile, project and footer links are clickable.
  • /create works on phones (natural page scroll, 16px inputs, 40px tap targets, wrapping rows). The LinkedIn label no longer overlaps post text on the review screen.

Suggested people / posts (saraffa13, #5, #6)

  • New GET /cards/by-handle/{handle}/suggested-posts and /suggested-people, using the cards.keyword_posts daily cache.
  • QuickEnrich outside search. With the QUICKENRICH_* keys unset it returns [], it doesn't error.

Infra / CI (AtmegaBuzz)

  • CI on PRs (lint/pytest baselines, tsc, build) and single-box CD (deploy.sh, auto-deploy of main → prod and dev → dev).
  • The dev instance on dev.* domains, Caddy routes, and the landing page as the cards-web site root.

Verified on 717ef4b

  • cards-api pytest: 142 passed, 2 failed (the known test_x_bot.py baseline).
  • cards-web: eslint 2 errors (both pre-existing; CI baseline is 3), tsc --noEmit clean, next build passes with prod's env (NEXT_PUBLIC_SITE_URL=https://cards.zynd.ai).
  • Running on dev.cards.zynd.ai since the dev deploy: no 5xx or tracebacks in cards-api. Dev shares prod's database; all 3 published cards render, and /, /create, /directory, /find, /search, llms.txt and sitemap.xml return 200.
  • No dependency changes (package-lock.json and requirements.txt unchanged for cards-web and cards-api).

Before / after merging

  • DB: no migration to apply for this release. The card is JSONB, and cards.keyword_posts (migration 0003) already exists in aafo. Make sure 0003 is recorded as applied in packages/db so db:migrate doesn't try to create it again.
  • Secrets. Empty in both prod and dev; none of them block the deploy:
    • OPENAI_API_KEY (cards-api): without it /ask returns 500 and new cards publish without embeddings, so they're unsearchable.
    • GITHUB_TOKEN: unauthenticated GitHub scraping is limited to 60 requests/hour, shared by dev and prod on this box.
    • QUICKENRICH_BASE_URL, QUICKENRICH_API_KEY: QuickEnrich suggestions stay off without them.
    • INDEXNOW_KEY, BING_API_KEY: no search-engine pings without them.
  • After deploy: sign in with LinkedIn on cards.zynd.ai once. If Supabase refuses the redirect, add https://cards.zynd.ai/auth/callback to the Supabase Auth redirect allowlist.
  • Watch the deploy: tail -f /home/ubuntu/.zynd-box/deploy.log. Roll back with deploy.sh 582b2a8.

🤖 Generated with Claude Code

AtmegaBuzz and others added 17 commits September 29, 2026 14:32
persona.zynd.ai, persona.api.zynd.ai, cards.zynd.ai, cards.api.zynd.ai and
api.zynd.ai each get a site block; persona.zynd.ai keeps /api/* -> persona-api
so the OAuth callbacks and A2A URLs registered on that name still resolve, and
api.zynd.ai keeps the legacy /cards /ask /onboard /v1 paths from
infra/api-box/Caddyfile so existing clients survive a cutover.

Pin Caddy to Let's Encrypt (acme_ca): the Ubuntu-packaged Caddy 2.6.2 tries
ZeroSSL first and its legacy EAB endpoint now answers 422
caddy_legacy_user_removed, so every issuance attempt failed before it ever
reached a challenge, whatever DNS said. README gets the domain-to-env map and
a cutover caveat (a fresh memory DB is not the live one).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
cards.zynd.ai's `/` was a placeholder hero, while the real landing page
(the dashboard's /agent-card) sat at /agent-card inside a root layout that
also rendered <html>, so it never looked like the dashboard version.

Mirror the dashboard's layout: the landing page gets its own standalone
root layout at `/` (route group `(landing)`), and every other page moves
into a `(site)` group that keeps the existing root layout (globals.css,
zynd-ui.css, Providers). URLs are unchanged. The landing page code is
identical to the dashboard's except the LinkedIn-only sign-in and the
cards.zynd.ai metadataBase, both intentional for cards.

/agent-card now 308-redirects to /, and the auth-bar / next-cookie
fallbacks that pointed at /agent-card now point at /.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Deploying the single box by hand means remembering which services a change
touches and in what order to build and restart them. deploy.sh works that out
from the git diff between the old and new commit, rebuilds only the affected
services, and health-checks them.

Safety properties it is built around: it only deploys commits reachable from
origin/main, refuses to run over local modifications, restores the previous
.next if a web build fails so the old version keeps serving, and never applies
database migrations or touches Caddy/DNS (those stay a person's call, per
AGENTS.md section 6). `--plan` shows what would change without touching
anything, and `<sha>` rolls back to an earlier commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ures

AGENTS.md section 5 lists known, pre-existing failures per service, and
`npm run lint` already exits 1 on main, so a plain test/lint gate would be
red from day one and get ignored. These two scripts turn the baseline into a
ratchet: CI passes while the failure/error count is at or below the recorded
number and fails the moment it goes above it, so existing debt can't grow
silently. The number is meant to be lowered as failures get fixed, never
raised to make a build green.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ci.yml: web lint/typecheck/build and the three Python suites on PRs and pushes
to dev. Main has pre-existing failures (AGENTS.md section 5 baselines, plus 24
and 3 ESLint errors in persona-web and cards-web that AGENTS.md doesn't list),
so both gates are ratchets that fail only on new ones; a plain gate would be
red from day one. Baselines were measured on a clean clone with no .env, using
the exact install commands the workflow runs, and each gate script was shown to
pass at the baseline and fail one below it.

deploy-single-box.yml: on push to main, run ci.yml, then SSH to the box and run
infra/single-box/deploy.sh, which rebuilds and restarts only the services whose
files changed and health-checks them. A failed web build restores the previous
.next and restarts nothing. Manual runs can roll back to a commit on main. The
SSH key on the box is meant to be a forced-command key that can run only
deploy.sh; the one-time setup (that key, the docker group, three repo secrets)
is a person's step and is written up in infra/single-box/README.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…n old Next

The first real CI run failed on persona-web's build with "next/font/google
queries have exactly one entry" (Geist, Turbopack). It does not reproduce on a
Mac or on the Linux server: Google returned plain gstatic URLs to both, and
persona-web builds there with the same lockfile. cards-web, on next@16.3.6,
built fine on the same runner in the same run, and pulls Geist too. So it is
Turbopack's font handler in persona-web's exactly-pinned next@16.2.1 meeting
whatever Google returned to that runner.

Try Turbopack first (what production uses), then retry with webpack, which
passes on 16.2.1. A real code error fails both and stays red; only the bundler
bug is let through, with a warning annotation. Bumping persona-web's Next to
match cards-web is the real fix and is left to whoever owns that app.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The box now hosts two environments off the same code: main -> prod, dev -> dev,
sharing the same databases for now. Dev gets its own checkout
(/home/ubuntu/zynd-platform-dev), pm2 apps (api-dev, web-dev, cards-web-dev on
127.0.0.1:8100/3101/3102), containers in a separate compose project on prod's
network (memory API+MCP on 8101/8190, cards-api on 8102), and dev.* Caddy site
blocks.

No dev memory worker on purpose: its nightly cron jobs (decay, resolution,
recompute, orphan cleanup) would run twice on the shared database, so the dev
API shares prod's worker and Redis.

deploy.sh takes --env=dev|prod and keeps all per-environment settings in one
block; the deploy workflow now deploys on pushes to dev as well as main (CI is
called by it, so CI no longer triggers on push to avoid checking twice), and a
manual run picks the environment.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The first run that reached the SSH step failed with "No ED25519 host key is known
for *** and you have requested strict checking": the DEPLOY_HOST_KEY secret held
something that wasn't a usable key line. Accept the key from whatever was pasted
(a leading host name, a trailing comment, quotes, CRLF, or just the base64), and
otherwise fail with a message that says what a valid value looks like. Also check
the private key is intact without printing it, and compare the pinned key with
what the server actually presents, reporting both fingerprints on a mismatch.

README: document the two environments (main -> prod, dev -> dev), what "same DB
for now" means, and the --env option.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
added suggestion people and suggested posts
…booking link

Claiming was invisible: a creator visiting their own anonymously-published
card saw a generic "Sign In", and after signing in the claim ran silently
in the background with no feedback and no Edit button until a reload.
The header now shows "Claim this card" to whoever holds the card's
one-time claim token (only the publishing browser does). Clicking it signs
in with LinkedIn and finishes the claim on return (intent kept in
sessionStorage across the OAuth round-trip); signed-in creators get the
same button, and on success the page refreshes into the owner view. The
unused UnclaimedCardActions component is removed (it also carried one of
the baseline lint errors). Server-side claim rules are unchanged.

Booking: onboarding's "Got a Calendly?" becomes "Got a booking link?"
with Calendly and Google Calendar inputs; the edit page gets both too, and
the profile's Book-a-call card shows one CTA per link. Google links go in
a new google_calendar_url field rather than reusing calendly_url, because
the card JSON is read by AI agents and a mislabelled field would mislead
them. Both booking fields are normalized (missing https:// added, non-http
schemes dropped) so "calendly.com/me" no longer silently hides the card.
The card is stored as JSON, so no schema migration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The resume export drew GitHub, LinkedIn and the other profile links as
plain text, so a recruiter reading the PDF had to retype them. Each header
link is now its own link annotation (same look, same whole-link wrapping),
as are a project's URL (when the line isn't cut off) and the footer's
live-profile link. Only http(s) URLs get an annotation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e work on phones

Review screen: the platform label beside each scraped post was a fixed
26px column, which fits "X" but not "LINKEDIN", so the label ran into the
post text. It's now 60px (ellipsis past that) and sits on its own line on
phones.

Mobile: below 1040px the grid stacked but the root kept its fixed 100dvh
height with an internally scrolling shell, so on a phone the form/questions
were squeezed into a ~190px scrolling window and the footer overflowed
sideways. The stacked layout now grows and scrolls with the page. The
640px block also gets 16px inputs (iOS Safari zooms on focus below that),
>=40px tap targets, wrapping/ellipsis for long URLs and handles, wrapping
button rows, tighter paddings, a stacked footer, and the handle prefix
above its input. Desktop (>1040px) is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bind address

After LinkedIn sign-in the callback redirected to https://localhost:3102/…
on dev (https://0.0.0.0:3002/… on prod): behind Caddy, `next start` builds
request.url from its own listen address, and the route used that origin.
It now takes the host the browser actually used (X-Forwarded-Host / Host,
which Caddy sets and overwrites from clients), falling back to request.url
when there's no proxy (`next dev`) or the header isn't a plain host[:port].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…LLM's rewrite

A LinkedIn URL like /in/dilnawaz-hossain-asrafi-798a6a228/ scraped fine,
but the card (and the resume export) showed /in/dilnawaz-hossain-asrafi:
synthesis returns its own identity.links, the LLM "tidied" the slug, and
the pipeline only setdefault()-ed the real URL, so the guess won. That also
broke refresh-linkedin, which re-scrapes the stored link. The LinkedIn and
X URLs the user pasted (when they scraped) now overwrite the LLM's links,
minus share-sheet query strings; GitHub's link comes from the API user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat: integrate QuickEnrich API for suggested people functionality
@AtmegaBuzz
AtmegaBuzz merged commit 169e9c0 into main Oct 1, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants