Experience #1 from the scan-experiences brief: when someone scans a card and
shares back, capture the meeting (with coarse geo + time) so the owner gets a
"People I met" list.
Backend (cardclaws-backend):
- migration 0006_connections.sql: connections table (card_id, owner_id, name,
email, note, country, city, met_at). Raw IP never stored — only resolved geo.
- POST /v1/profile/:handle/connect (public, rate-limited 5/min): captures the
connection with geo, emails the owner best-effort.
- GET /v1/cards/:id/connections (owner-only): the People-I-met list.
- ConnectionRow model + connections queries (insert, list_by_card);
profile_service::submit_connection; card_service::list_connections.
- 2 tests (capture→owner-list + validation, owner-only); gate green (118 tests).
- Verified live end-to-end.
Web (cardclaws-profile):
- [handle].astro: a "We met?" share-back form posting to /connect; astro check
clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
When a card is scanned, the public profile can open with a full-bleed,
AI-generated welcome hero before the contact info — experience #10 from the
scan-experiences brief, on the share-token experience selector foundation.
Backend (cardclaws-backend):
- migration 0005_welcome.sql: add cards.welcome JSONB; flows through
PublicProfile via serde flatten.
- POST /v1/cards/:id/welcome (owner-gated): refine + generate via the existing
AiClient (nano-banana), store {kind, message, imageDataUrl}.
- card_service::set_welcome + queries::update_welcome; CardRow gains `welcome`.
- 2 tests (generate→public-profile, ownership); workspace gate green (116 tests).
- Verified live with real Gemini end-to-end.
Web (cardclaws-profile):
- WelcomeHero.astro: full-bleed AI hero + greeting + "scroll to connect" cue.
- [handle].astro renders it when the card has a welcome; experience selector
(?x=profile suppresses) as the lever for the other experiences.
- PublicProfile gains an optional `welcome`; astro check clean.
MVP inlines the image as a data URL; production swaps to an R2 URL (isolated in
the welcome column).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- Idempotent hourly rollup (run_rollup, INSERT ... ON CONFLICT DO UPDATE into
analytics_rollups_hourly) + Tokio background task (startup then hourly)
- GET /v1/cards/{id}/analytics/geo — country-grouped visit counts
- GeoResolver trait: real MaxMind under the `geoip` feature, no-op default;
raw IP resolved then discarded (only hash + coarse country/city persisted)
- analytics_events now store country/city; rollups read query for the dashboard
2 new tests (geo grouping, rollup aggregation + idempotency); 75 backend tests.
fmt + clippy clean; geoip feature compiles.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- share_links model/queries; opaque 12-char base62 tokens (PRD §17.1)
- POST /v1/cards/{id}/share — create share link (owner-scoped, modality-validated)
- GET /v1/s/{token} — record modality-attributed event (qr→qr_scan, nfc→nfc_tap)
and 302-redirect to the profile; unknown/expired → 404
- GET /v1/cards/{id}/share-links — list a card's links
- analytics_events now carry the share_token correlation
- GET /v1/cards/{id}/analytics/feed — chronological event feed (§6.7.2)
6 new integration tests; 73 backend tests total. fmt + clippy clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>