Commit Graph

4 Commits

Author SHA1 Message Date
Kwaku Danso 98678ff5a3 feat(tier2): finish the finish line — Block H follow-ups, Block G geolocation, cross-cutting
Three threads of work land here together to close out Tier 2.

### Block H follow-ups — day-of check-in
- Scanner is now an "open on your phone" magic-link flow. Hosts on
  desktop mint a scoped JWT via POST /events/{id}/scanner-ticket and
  render its URL into a QR; phone scans it and lands on /scanner with
  the ticket as bearer. The ticket carries Audience=scanner so it can
  never substitute for a session token.
- Plus-one confirmation at the door: scan → POST /check-in/preview to
  fetch guest + expected party size → confirm buttons ("Just them",
  "Party of N", custom) → POST /check-in. No more silent arrival_count=1.
- Offline scan queue: failed POSTs go into an IndexedDB store and drain
  on the 'online' event with poison-message protection.
- Day-of arrivals headline widget on the event overview, gated to the
  host's local calendar date so it doesn't dominate the page weeks out.
- Tab nav restyled with inline heroicons + scrollable segmented control;
  Check-in moves to the rightmost slot.
- PWA: manifest + service worker scoped to /scanner, generated 192/512
  icons (Go scripted renderer in scripts/gen-scanner-icons.go).
- Confirmation email QR was rendering broken because html/template
  rewrites data: URLs to #ZgotmplZ; mark the value as template.URL.
- Email "open your invitation" link 404'd because we had no token to
  put after /rsvp/. Threaded AccessLink through the RSVPConfirmed NATS
  event from the API at submit time.

### Block G remainder — geolocation + threshold preview
- Pluggable GeoResolver in the fraud engine (NullResolver, IPApiResolver
  for the free ip-api.com fallback, MaxMindResolver behind GG_GEOIP_DB_PATH).
  Wrapped in a Redis cache (30d TTL). Geo flows through both gRPC and
  NATS scoring paths.
- geo_jump scoring feature: >500km in <1h flags ("accessed from Lagos
  and Paris within 12 minutes"); >500km in <6h is a softer signal. The
  existing single-signal cap keeps a lone geo_jump in MEDIUM.
- FraudScored event carries geo_country/city/lat/lon; ApplyScore uses
  COALESCE so a later re-score without geo doesn't wipe earlier data.
- Threshold-slider live preview: GET /events/{id}/security/thresholds/preview
  returns band counts the host's existing access events would have
  fallen into under the proposed thresholds. Debounced (250ms) widget
  under the Advanced sliders so the host gets concrete feedback instead
  of guessing.

### Cross-cutting — audit, tier-gating, feature flags
- audit_log table + internal/audit.Recorder (async fire-and-forget on
  detached context so an audit blip never fails the real action). Wired
  into branding update, thresholds update, allowlist add/remove,
  collaborator invite/role-change/remove, message create/send-now/cancel.
- Tier-gating: extended billing.Limits with MaxCollaborators,
  CustomBranding, Scanner, Broadcasts. Free = none; Pro = 5 + all;
  Business = unlimited. Gates the scanner-ticket, message create,
  branding put, and collaborator invite endpoints with 402 +
  structured upgrade payload. Auto-reminders, fraud detection, and
  analytics deliberately stay on every tier — those are safety + visibility
  features, not upsell levers.
- Feature flags: feature_flags table + internal/flags.Store with 30s
  in-memory refresh, stable sha256(key + user_id) percent bucketing,
  unknown-key-defaults-on. Six Tier 2 flags pre-seeded. Three handlers
  (branding, broadcasts, scanner) check the kill switch ahead of the
  tier gate so ops can pull a feature back without a redeploy.

### Verified
- go test ./... + fraud-engine pytest (12/12 incl. 3 new geo_jump tests + 5
  new flags tests).
- docker compose build + up across api, fraud-engine, notifier, frontend.
- /health endpoints 200; migrations 0014 + 0015 applied; 6 flags
  seeded; audit_log table + partial indexes confirmed.
- Fraud-engine logs confirm geo resolver kind=CachedGeoResolver provider=auto.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 20:30:02 +01:00
Kwaku Danso b34715f152 fix(tier2-g): warmer Gate copy, generic example, plain-language advanced controls
Round-three pass on the Gate card based on the latest UX feedback.

- Tagline: "Keeps your guest list yours" -> "Only your guests get in"
- Walk-through example now uses a neutral placeholder name (Sam)
  instead of "Aunty Patience". GuestGuard's audience is wedding
  couples, corporate planners, and party hosts as much as family
  organisers, and Sam is at home in any of those contexts.
- The four-step "How does the Gate work?" expander was lightly
  rewritten for warmth and to remove em-dashes; the mechanism it
  describes is unchanged.
- Advanced strictness controls now open with a real explanation
  rather than a one-liner about "band thresholds". The intro maps
  the three sliders to the three reactions (watch / flag / refuse),
  explains what the 0-100 numbers mean, and reassures hosts that
  the presets above are still fine for almost everyone. Each
  slider also gets a one-line caption tying its level dot (green
  / amber / red) to the user-visible effect.
- The trusted-networks "What this is and isn't" panel had its quotes
  unwound into natural sentences; the empty-state copy was rewritten
  in the second person; "No decisions to review yet" reworded.
- Pass through every user-visible string and replaced em-dashes
  with periods, commas, or natural rephrasing. Em-dashes only remain
  in code comments now (developer-facing, not on screen).

No behaviour changes; no backend changes; no API changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 23:00:11 +01:00
Kwaku Danso dee3d738ac fix(tier2-g): explain the Gate clearly, kill the "everyone outside = fraud" misconception
The previous pass made the labels friendlier but didn't actually explain
what the Gate does. A real user (correctly) read "trusted networks" as
"any guest outside my IP is treated as fraud," which is the opposite of
what's happening — and missed the entire value prop: stopping forwarded
invitation links from being used by uninvited people. That's the central
pain GuestGuard solves; the Gate page has to lead with it, not bury it
under controls.

What changed on the Gate card:

- Big value-prop block at the top in plain language: "Every guest gets
  their own personal invitation link. The Gate watches each link and
  stops forwarded or shared invitations from being used by people who
  weren't on your list." Also explicit: "Real invited guests don't
  notice it. You don't need to set anything up — the Gate is on by
  default with sensible settings."

- New "How does the Gate work?" collapsible explainer with a 4-step
  walkthrough using Aunty Patience as the protagonist. Covers what
  signals the Gate looks at, when guests *do* get flagged, and the
  reassurance that normal day-to-day variation (Wi-Fi → mobile data,
  changing rooms) doesn't trigger anything.

- Preset descriptions rewritten to talk about the actual pain instead
  of generic strictness levels. "Stops forwarded links from being used
  by people who weren't invited" lands much harder than "Recommended
  for most parties."

- Trusted networks section opens with an explicit "What this is and
  isn't" panel that directly addresses the misconception: adding a
  network here is a *speed-up* for guests on your Wi-Fi, NOT a
  whitelist that classifies everyone else as suspicious. Empty state
  reads "No trusted networks — and that's fine. The Gate is doing its
  job" so a host doesn't feel they've missed a setup step.

- Pill on the section header tags it "Optional · most hosts don't need
  this" to drop its prominence. The button copy went from "Use my
  current network" (which read like "approve myself") to "Trust the
  network I'm on right now."

No backend changes. Internal Go names (FraudThresholds, /security/*
endpoints) untouched — never user-visible, renaming would churn for
zero end-user benefit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 22:36:19 +01:00
Kwaku Danso 3629ab8c79 fix(tier2-g): rebrand Security → Gate with event-planner language
The original Block G UI was correct but felt like a firewall config panel.
The audience for GuestGuard is wedding couples, party hosts, and event
planners — not network admins. Three concrete problems:

1. "Fraud detection" / "Risk score" frames every guest as a suspect.
   "Gate" is the metaphor every host already uses ("gate the guest
   list", "doorman"). It's the same idea, friendlier.

2. Three threshold sliders (0–100) are abstract. Hosts can't reason
   about "block at 85" without context. Replaced by four presets —
   Relaxed / Balanced / Strict / Very strict — each with a sentence
   that explains the kind of event it fits. The sliders survive under
   an Advanced disclosure for the rare power user.

3. CIDR notation is gibberish to a layperson. "Trusted networks" is
   what they're actually doing. A "Use my current network" button
   detects the host's apparent IP, widens IPv4 to /24 (typical home
   block), and pre-fills the form. Manual entry stays for cases
   where the host knows what they're doing.

Plus two leaks fixed on the guest-facing RSVP page: removed the
"Risk score 72 · high" line from both the confirmation and the
blocked-attempt cards. Guests should never see internal scoring
detail — the blocked message now reads "Something about this
attempt looked off" instead of "suspicious access attempt".

Backend
- New GET /me/public-ip (authed) — echoes the caller's apparent IP
  so the frontend's auto-detect button doesn't need a third-party
  ipify call

Frontend
- New components/GateCard.vue with preset cards + advanced disclosure
  + trusted-networks UX with auto-detect
- Removed components/SecurityCard.vue
- Event tab nav: Security → Gate. Hash-link alias preserves any
  /events/<id>#security bookmarks
- RSVP page: leaked risk-score lines removed; blocked-attempt
  message reworded for non-technical guests

Internal storage / API names stay (FraudThresholds, fraud_v2,
/security/* endpoints) — they're never user-visible and renaming
them would be a breaking change for no end-user benefit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 22:15:11 +01:00