Files

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_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.

    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 061).

  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: WhereWoofWhere 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.