4.8 KiB
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_enabledkill-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
-
Geolocation: vanilla JS on
tag-public.html(no framework). On load (active tag only, HTTPS),navigator.geolocation.getCurrentPosition; on successfetch 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: Alpinex-init— rejected, plain JS is clearer for one flow. -
internal/smspackage with aSenderinterface.type Sender interface { Send(to, body string) error }Implementations:
smsglobal.Client(HTTP POST toapi.smsglobal.comREST 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) andsmslog.Sender(logs; used whenSMS_API_KEYis unset, so dev/tests never send real SMS). Numbers normalised viasms.NormalizeAU(strip+/spaces; leading0→61). -
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 (ifsms_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 toalert_senton the new row. Haversine helper lives ininternal/sms(pure function, unit-testable). -
sms_enabledcolumn via idempotent migration.db/schema.sqlgainsALTER 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;UpdateTagDetailsgains the flag; edit form gets a checkbox. -
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 =
descriptiontruncated 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). -
Secrets via env only.
SMS_API_KEY,SMS_API_SECRET, optionalSMS_FROM(blank → pooled number). Dev uses the log sender automatically. -
Branding is a sweep:
WhereWoof→Where Woofin 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
localhostis 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_enabledcap it; plan-level scan limits arrive in Phase 5. - [160-char limit with maps link] → maps link shortened (
https://maps.google.com/?q=lat,lngis ~40 chars); template budgeted.
Migration Plan
- Re-apply
db/schema.sql(idempotent ALTER) on the existing dev DB; regenerate sqlc queries. - Land code with log sender; run automated suite (no real SMS).
- Manual check: one real SMS to
+61423274487with 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
descriptiondoubles as the item "name" in the SMS (assumed yes, truncated) or a dedicated name field arrives with the Laravel admin.