From d4679068ba5743a98cec89168536c52157d2b240 Mon Sep 17 00:00:00 2001 From: Sam Rolfe Date: Fri, 7 Aug 2026 16:10:52 +1000 Subject: [PATCH] scan-flow step 9: real SMS via SMSGlobal HTTP API (smshttp sender, proven live OK:0) + docs --- AGENTS.md | 2 +- README.md | 2 +- frontend/internal/sms/smshttp.go | 57 +++++++++++++++++++++++++++++ frontend/main.go | 8 +++- inbox.txt.tuxedo-lock | 0 openspec/changes/scan-flow/tasks.md | 2 +- plans/scan-flow.md | 2 +- todo.txt | 2 + 8 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 frontend/internal/sms/smshttp.go create mode 100644 inbox.txt.tuxedo-lock diff --git a/AGENTS.md b/AGENTS.md index ee6e434..30fccec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,7 @@ Where Woof — a **return-tag platform** (not a tracker): pre-coded QR/NFC tags ## Tools - `openspec` 1.3.1 (spec-driven dev), `plannotator` (plan + browser review), pi subagents (master/worker), `sqlc` 1.31.1 at `~/go/bin` (not on PATH — `make generate` handles it), `pgx` v5, `gorilla/sessions`, `bcrypt` -- **Secrets are env-only, never committed**: `DATABASE_URL`, `SESSION_SECRET`, `SMS_API_KEY`, `SMS_API_SECRET`, `SMS_FROM` (blank = SMSGlobal pooled number) +- **Secrets are env-only, never committed**: `DATABASE_URL`, `SESSION_SECRET`; SMS via **HTTP API** `SMS_USER`/`SMS_PASSWORD`/`SMS_FROM` (proven live 2026-08-07) with REST key/secret (`SMS_API_KEY`/`SMS_API_SECRET`) as unverified fallback - SMS is a swappable `internal/sms.Sender` — `LogSender` (default) / `smsglobal.Client` (real) ## Gotchas worth remembering diff --git a/README.md b/README.md index 3c059c5..a37caef 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Uncheck *Send me SMS alerts* on the edit form → scans are recorded but no SMS ### 7. Watching the SMS -In log mode: `tail -f /tmp/ww-server.log` and look for `SMS to`. With real SMSGlobal credentials (`SMS_API_KEY` / `SMS_API_SECRET` env vars), the owner's phone receives the text. +In log mode: `tail -f /tmp/ww-server.log` and look for `SMS to`. With real credentials set, the owner's phone receives the text — set `SMS_USER`/`SMS_PASSWORD`/`SMS_FROM` (SMSGlobal HTTP API, `from` = your verified number like `61423274487`) when running `make run`. ### Notes diff --git a/frontend/internal/sms/smshttp.go b/frontend/internal/sms/smshttp.go new file mode 100644 index 0000000..ec79312 --- /dev/null +++ b/frontend/internal/sms/smshttp.go @@ -0,0 +1,57 @@ +package sms + +import ( + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" +) + +// HTTPClient sends SMS via the SMSGlobal HTTP API (api.smsglobal.com/http-api.php). +// This is the PROVEN path — validated live 2026-08-05 with real credentials +// (response: OK: 0; Sent queued message ID ...). Matches the 2014 WhereWoof +// integration pattern: action=sendsms, user/password, from, userfield, to, text. +type HTTPClient struct { + user string + password string + from string // verified number or sender, international format without '+' + http *http.Client +} + +// NewHTTP returns an HTTP-API client. from is required (verified number or +// registered sender, e.g. "61423274487"). +func NewHTTP(user, password, from string) *HTTPClient { + return &HTTPClient{ + user: user, + password: password, + from: from, + http: &http.Client{Timeout: 15 * time.Second}, + } +} + +// Send posts a message to the destination (international format, no '+'). +func (c *HTTPClient) Send(to, body string) error { + form := url.Values{} + form.Set("action", "sendsms") + form.Set("user", c.user) + form.Set("password", c.password) + form.Set("from", c.from) + form.Set("userfield", fmt.Sprintf("MID%sWSIDwherewoof", time.Now().UTC().Format("20060102150405"))) + form.Set("to", to) + form.Set("text", body) + form.Set("maxsplit", "5") + + resp, err := c.http.PostForm("https://api.smsglobal.com/http-api.php", form) + if err != nil { + return err + } + defer resp.Body.Close() + b, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) + s := strings.TrimSpace(string(b)) + if !strings.HasPrefix(s, "OK:") { + return fmt.Errorf("smsglobal http: %s", s) + } + return nil +} diff --git a/frontend/main.go b/frontend/main.go index 56f4440..cec8ada 100644 --- a/frontend/main.go +++ b/frontend/main.go @@ -43,9 +43,13 @@ func main() { log.Fatal("templates:", err) } - // SMS sender: log-only unless real SMSGlobal credentials are configured. + // SMS sender: log-only unless real credentials are configured. + // Preferred: SMSGlobal HTTP API (SMS_USER/SMS_PASSWORD/SMS_FROM) — proven live. + // Fallback: REST key/secret (SMS_API_KEY/SMS_API_SECRET) — unverified endpoints. var sender sms.Sender = sms.LogSender{} - if key := os.Getenv("SMS_API_KEY"); key != "" { + if u := os.Getenv("SMS_USER"); u != "" { + sender = sms.NewHTTP(u, os.Getenv("SMS_PASSWORD"), os.Getenv("SMS_FROM")) + } else if key := os.Getenv("SMS_API_KEY"); key != "" { sender = sms.New(key, os.Getenv("SMS_API_SECRET"), os.Getenv("SMS_FROM")) } diff --git a/inbox.txt.tuxedo-lock b/inbox.txt.tuxedo-lock new file mode 100644 index 0000000..e69de29 diff --git a/openspec/changes/scan-flow/tasks.md b/openspec/changes/scan-flow/tasks.md index 8bdc033..5186115 100644 --- a/openspec/changes/scan-flow/tasks.md +++ b/openspec/changes/scan-flow/tasks.md @@ -23,5 +23,5 @@ - [x] 5.1 Extend the HTTP suite (`/tmp/verify.sh`): scan with location → scan row + alert logged + `alert_sent=true`; same-location re-scan in window → no second alert; >250 m re-scan in window → second alert; after 10 min → alert; `sms_enabled=false` → no alert; finder contact → stored + owner alert; `sms:` link and re-check button present; branding strings present - [x] 5.2 Unit test haversine (known distances) + NormalizeAU cases -- [ ] 5.3 Manual real-SMS check to `+61432374487` (real credentials, then removed) +- [x] 5.3 Manual real-SMS check to `+61432374487` (real credentials, then removed) - [x] 5.4 `openspec validate scan-flow`; commit diff --git a/plans/scan-flow.md b/plans/scan-flow.md index 573fdae..3913e80 100644 --- a/plans/scan-flow.md +++ b/plans/scan-flow.md @@ -46,7 +46,7 @@ All decisions are settled in the change (proposal/design): SMSGlobal REST, **poo - [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 +- [x] 9. Manual real-SMS check to `+61432374487` (real creds, then remove); `openspec validate scan-flow`; commit ## Verification diff --git a/todo.txt b/todo.txt index 419a2ae..255cddb 100644 --- a/todo.txt +++ b/todo.txt @@ -6,3 +6,5 @@ (F) Billing phase (Stripe AU, plans, scan limits) +project:where-woof +agent:where_woof (G) Deploy front-end to .13 + Caddy + DNS + InMotion redirect +project:where-woof +agent:where_woof x 2026-08-05 Set up Gitea for where_woof +project:where-woof +agent:where_woof +(B) Scan-flow: re-alert when different finder phone within 250 m window +project:where-woof +agent:right-monitor-pi-coding +(B) Scan-flow: browser fingerprint + 24 h block, store in DB +project:where-woof +agent:right-monitor-pi-coding