# 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/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 `0` → `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, 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**: `WhereWoof` → `Where 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

- [x] 1. Schema: `ALTER ... ADD COLUMN IF NOT EXISTS sms_enabled` in `db/schema.sql`; `make db-up` on existing DB
- [x] 2. sqlc: add `InsertScan`, `GetLastAlertByTag`, `UpdateTagDetails`+`sms_enabled`; `make generate`
- [x] 3. `internal/sms`: Sender interface, `smslog`, `smsglobal` client (field names from MXT docs), `NormalizeAU`, `HaversineMeters` + unit tests
- [x] 4. `handlers/scan.go`: scan handler (record → throttle → alert → `alert_sent`) + contact handler; wire Sender into App + routes in `main.go`
- [x] 5. Public page: geolocation JS (prompt, POST coords, re-check button, hide after share), `sms:` link, finder contact form
- [x] 6. Tag management: `sms_enabled` checkbox in edit form + handler
- [x] 7. Branding sweep: "Where Woof" / "Where Woof !" across templates + README
- [x] 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.
