Files
where_woof/plans/scan-flow.md

5.7 KiB

Phase 2 — Scan Flow (geolocation → SMS alert, finder contact, branding)

Plan for OpenSpec change scan-flow (proposal/design/specs/tasks in openspec/changes/scan-flow/).

Context

Phase 1 (merged) gives us auth, tag setup/management, and the public tag page with tel: links. Phase 2 delivers the core product moment: a finder scans a lost item's tag, the scan (with location when permitted) is recorded, and the owner is alerted by SMS (SMSGlobal, pooled sender number) with item type + name, time, maps link, and tag page link. A finder can also leave their number for the owner. Branding: "WhereWoof" → "Where Woof" (werewolf wordplay), title "Where Woof !".

All decisions are settled in the change (proposal/design): SMSGlobal REST, pooled number (origin blank), real test number +61423274487, message includes item type + name (description truncated ~30 chars), finder number stored in scans.scanner_phone AND SMS'd to owner, location-aware throttle (within 10-min window per tag, re-alert only if new location is >250 m from last alerted location; same spot = recorded, no SMS), and sms_enabled per tag (some tags are purely informational — default on, toggle on the edit form).

Approach

  • internal/sms package (new):
    • Sender interface (Send(to, body string) error) — the seam for tests.
    • smslog sender (logs; used automatically when SMS_API_KEY is unset — dev/tests never send real SMS).
    • smsglobal HTTP client (POST api.smsglobal.com REST, API key+secret auth, JSON; field names confirmed against the owner's MXT docs at build time — they have a prior integration).
    • NormalizeAU (strip +/spaces; leading 061) + HaversineMeters (pure, unit-tested).
  • handlers/scan.go (new): POST /t/{tag_code}/scan — parse optional lat/lng, InsertScan, apply throttle, send alert per rules, set alert_sent. POST /t/{tag_code}/contact — validate finder number, store on latest scan, SMS the owner with it.
  • Public page (tag-public.html, active branch only): vanilla JS geolocation (prompt on load over HTTPS, fetch POST /t/{code}/scan with coords; on deny POST without coords and show re-check button; hide once shared), sms: link (prefilled body asking for the finder's number), finder contact form with inline error.
  • Schema: idempotent ALTER TABLE tags ADD COLUMN IF NOT EXISTS sms_enabled BOOLEAN NOT NULL DEFAULT TRUE; in db/schema.sql; make db-up migrates the existing DB. New sqlc queries: InsertScan, GetLastAlertByTag (latest scan with alert_sent=true), UpdateTagDetails extended with sms_enabled. make generate.
  • Tag management: sms_enabled checkbox in tag-edit.html (default checked), saved via UpdateTagDetails.
  • Branding sweep: WhereWoofWhere Woof in templates + README; <title>Where Woof !.
  • Wiring: App gains a Sender; main.go picks log vs smsglobal from env (SMS_API_KEY/SMS_API_SECRET/SMS_FROM); 2 new routes.

Files to create / modify

  • New: frontend/internal/sms/sender.go, smslog.go, smsglobal.go, geo.go, geo_test.go, normalize_test.go; frontend/internal/handlers/scan.go
  • Modified: db/schema.sql; frontend/internal/db/queries.sql (+ regenerated db.sql.go, models.go, querier.go); frontend/internal/handlers/handlers.go (App + Sender), tags.go (sms_enabled), main.go (sender + routes); frontend/templates/tag-public.html, tag-edit.html, base.html (title); README.md
  • Secrets (never in repo): SMS_API_KEY, SMS_API_SECRET, SMS_FROM (blank = pooled) — env vars / ~/.config/environment.d/10-secrets.conf pattern

Reuse

  • scans table already exists (schema.sql) — no new tables.
  • tel: link pattern from Phase 1 for the sms: link.
  • account-panel HTMX pattern; textOrNil/ownerID helpers in handlers.
  • /tmp/verify.sh — extend with scan scenarios (log-sender asserts, no real SMS).
  • make db-up / make generate / make psql targets.

Steps

  • 1. Schema: ALTER ... ADD COLUMN IF NOT EXISTS sms_enabled in db/schema.sql; make db-up on existing DB
  • 2. sqlc: add InsertScan, GetLastAlertByTag, UpdateTagDetails+sms_enabled; make generate
  • 3. internal/sms: Sender interface, smslog, smsglobal client (field names from MXT docs), NormalizeAU, HaversineMeters + unit tests
  • 4. handlers/scan.go: scan handler (record → throttle → alert → alert_sent) + contact handler; wire Sender into App + routes in main.go
  • 5. Public page: geolocation JS (prompt, POST coords, re-check button, hide after share), sms: link, finder contact form
  • 6. Tag management: sms_enabled checkbox in edit form + handler
  • 7. Branding sweep: "Where Woof" / "Where Woof !" across templates + README
  • 8. Extend /tmp/verify.sh (scan with/without location, same-spot throttle, >250 m re-alert, post-window alert, sms_enabled=false, finder contact, sms: link, re-check button, branding); run full suite
  • 9. Manual real-SMS check to +61423274487 (real creds, then remove); openspec validate scan-flow; commit

Verification

  • Unit: haversine known distances; NormalizeAU (+61423274487 / 0432374487 → 61423274487).
  • HTTP suite (log sender): scan with location → scan row + alert logged + alert_sent=true; same-location re-scan within 10 min → recorded, no alert; >250 m re-scan within window → second alert; scan after window → alert; sms_enabled=false → no alert; finder contact → stored + owner alert; sms: link, re-check button present; button hidden after share; branding strings present.
  • Manual: one real SMS to +61423274487 with real credentials.
  • openspec validate scan-flow; commit all.