## Context Phase 1 (frontend-foundation, merged) gives us auth, tag setup/management, and the public tag page with `tel:` links. Phase 2 adds the finder-facing scan flow and owner alerts. Confirmed with the owner: SMS provider is **SMSGlobal** (MXT), using their **shared/pooled sender number** (origin blank — upgrade to a registered sender later); real test number `+61423274487`. The `scans` table already exists; it needs no new columns. ## Goals / Non-Goals **Goals:** - Every scan recorded; finder location captured when permitted. - Owner alerted by SMS (SMSGlobal) with a useful, location-aware message. - Cost/spam protection: 250 m / 10-minute per-tag throttle; `sms_enabled` kill-switch per tag. - Finder can leave a number; owner is told. - Branding: "Where Woof" / "Where Woof !". **Non-Goals:** - Registered sender ID / short-code upgrades — later (pooled number now). - Scan-limit plans/billing enforcement — Phase 5. - Email alerts, NTFY, admin scan dashboard — later phases. - Inbound SMS (owner replying to finder) — later. ## Decisions 1. **Geolocation: vanilla JS on `tag-public.html` (no framework).** On load (active tag only, HTTPS), `navigator.geolocation.getCurrentPosition`; on success `fetch POST /t/{code}/scan {lat,lng}`; on error/deny, POST without coords and show the **re-check button**; on success hide it. No Alpine needed — this is one small script. *Alternative:* Alpine `x-init` — rejected, plain JS is clearer for one flow. 2. **`internal/sms` package with a `Sender` interface.** ```go type Sender interface { Send(to, body string) error } ``` Implementations: `smsglobal.Client` (HTTP POST to `api.smsglobal.com` REST API, API key + secret auth, JSON body; **exact field names confirmed against the owner's MXT docs at build time** — they have an existing integration) and `smslog.Sender` (logs; used when `SMS_API_KEY` is unset, so dev/tests never send real SMS). Numbers normalised via `sms.NormalizeAU` (strip `+`/spaces; leading `0` → `61`). 3. **Location-aware throttle (the interesting rule).** On each scan POST: fetch the tag's most recent scan that had an alert (`alert_sent = true`). If none, or older than 10 minutes → alert (if `sms_enabled`). If within 10 minutes: alert only when the new scan has coordinates **and** haversine distance from the last alerted location **> 250 m**. Otherwise record the scan, no alert. Alert outcome written to `alert_sent` on the new row. Haversine helper lives in `internal/sms` (pure function, unit-testable). 4. **`sms_enabled` column via idempotent migration.** `db/schema.sql` gains `ALTER TABLE tags ADD COLUMN IF NOT EXISTS sms_enabled BOOLEAN NOT NULL DEFAULT TRUE;` — re-applying to the existing dev DB migrates cleanly (spec: database). sqlc regenerated; `UpdateTagDetails` gains the flag; edit form gets a checkbox. 5. **Message template** (SMS ≤ 160 chars): ``` Where Woof: your {Dog}, {Shadow} was scanned at {3:05 pm}. Location: https://maps.google.com/?q={lat},{lng} See: https://where-woof.com/t/{TEST000001} ``` Name = `description` truncated to ~30 chars; item type title-cased. No location → omit the Location line and append "Finder didn't share a location." (first scan only — throttle suppresses repeats). 6. **Secrets via env only.** `SMS_API_KEY`, `SMS_API_SECRET`, optional `SMS_FROM` (blank → pooled number). Dev uses the log sender automatically. 7. **Branding** is a sweep: `WhereWoof` → `Where Woof` in templates/README/title (`Where Woof !`). No functional impact. ## Risks / Trade-offs - [SMSGlobal REST field names/version uncertain] → confirm against the owner's existing MXT integration during execution; keep the Sender interface thin so swapping is trivial. - [Pooled sender number may be unreliable/delayed for AU delivery] → accepted; registered sender is a known upgrade path. - [Geolocation requires HTTPS] → dev on `localhost` is exempt by browsers; prod goes behind Caddy with TLS before this is exposed publicly. - [Finder location is private data] → stored in `scans` (lat/lng), only surfaced to the owner via the SMS; revisit privacy wording when public. - [SMS cost abuse] → throttle + `sms_enabled` cap it; plan-level scan limits arrive in Phase 5. - [160-char limit with maps link] → maps link shortened (`https://maps.google.com/?q=lat,lng` is ~40 chars); template budgeted. ## Migration Plan 1. Re-apply `db/schema.sql` (idempotent ALTER) on the existing dev DB; regenerate sqlc queries. 2. Land code with log sender; run automated suite (no real SMS). 3. Manual check: one real SMS to `+61423274487` with real credentials from `.13`-safe env (local run, then removed). ## Open Questions - Exact SMSGlobal REST endpoint/field names for the owner's account (confirm at build time). - Whether `description` doubles as the item "name" in the SMS (assumed yes, truncated) or a dedicated name field arrives with the Laravel admin.