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 +61432374487, 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/smspackage (new):Senderinterface (Send(to, body string) error) — the seam for tests.smslogsender (logs; used automatically whenSMS_API_KEYis unset — dev/tests never send real SMS).smsglobalHTTP client (POSTapi.smsglobal.comREST, 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; leading0→61) +HaversineMeters(pure, unit-tested).
handlers/scan.go(new):POST /t/{tag_code}/scan— parse optional lat/lng,InsertScan, apply throttle, send alert per rules, setalert_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}/scanwith 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;indb/schema.sql;make db-upmigrates the existing DB. New sqlc queries:InsertScan,GetLastAlertByTag(latest scan withalert_sent=true),UpdateTagDetailsextended withsms_enabled.make generate. - Tag management:
sms_enabledcheckbox intag-edit.html(default checked), saved viaUpdateTagDetails. - Branding sweep:
WhereWoof→Where Woofin templates + README;<title>→Where Woof !. - Wiring:
Appgains aSender;main.gopicks 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(+ regenerateddb.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.confpattern
Reuse
scanstable already exists (schema.sql) — no new tables.tel:link pattern from Phase 1 for thesms:link.account-panelHTMX pattern;textOrNil/ownerIDhelpers inhandlers./tmp/verify.sh— extend with scan scenarios (log-sender asserts, no real SMS).make db-up/make generate/make psqltargets.
Steps
- 1. Schema:
ALTER ... ADD COLUMN IF NOT EXISTS sms_enabledindb/schema.sql;make db-upon existing DB - 2. sqlc: add
InsertScan,GetLastAlertByTag,UpdateTagDetails+sms_enabled;make generate - 3.
internal/sms: Sender interface,smslog,smsglobalclient (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 inmain.go - 5. Public page: geolocation JS (prompt, POST coords, re-check button, hide after share),
sms:link, finder contact form - 6. Tag management:
sms_enabledcheckbox 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
+61432374487(real creds, then remove);openspec validate scan-flow; commit
Verification
- Unit: haversine known distances;
NormalizeAU(+61432374487 / 0432374487 → 61432374487). - 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 +61432374487 with real credentials.
openspec validate scan-flow; commit all.