diff --git a/db/schema.sql b/db/schema.sql index 03d5c5e..089a51f 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -86,3 +86,7 @@ CREATE TABLE IF NOT EXISTS url_shortened ( scan_id BIGINT NOT NULL REFERENCES scans(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); + +-- Phase 5.5 quick wins: customer pause + order amounts (idempotent). +ALTER TABLE users ADD COLUMN IF NOT EXISTS paused BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE orders ADD COLUMN IF NOT EXISTS amount NUMERIC(10,2) NOT NULL DEFAULT 0; diff --git a/frontend/internal/db/models.go b/frontend/internal/db/models.go index 0517392..81e2a3c 100644 --- a/frontend/internal/db/models.go +++ b/frontend/internal/db/models.go @@ -14,6 +14,7 @@ type Order struct { Status string `json:"status"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` + Amount pgtype.Numeric `json:"amount"` } type Product struct { @@ -74,4 +75,5 @@ type User struct { CreatedAt pgtype.Timestamptz `json:"created_at"` IsAdmin bool `json:"is_admin"` RememberToken pgtype.Text `json:"remember_token"` + Paused bool `json:"paused"` } diff --git a/frontend/internal/db/queries.sql.go b/frontend/internal/db/queries.sql.go index 9d332a3..060ac35 100644 --- a/frontend/internal/db/queries.sql.go +++ b/frontend/internal/db/queries.sql.go @@ -139,7 +139,7 @@ func (q *Queries) CountTagsByOwner(ctx context.Context, ownerID pgtype.Int8) (in const createUser = `-- name: CreateUser :one INSERT INTO users (email, password_hash, name, phone) VALUES ($1, $2, $3, $4) -RETURNING id, email, password_hash, name, phone, created_at, is_admin, remember_token +RETURNING id, email, password_hash, name, phone, created_at, is_admin, remember_token, paused ` type CreateUserParams struct { @@ -166,6 +166,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e &i.CreatedAt, &i.IsAdmin, &i.RememberToken, + &i.Paused, ) return i, err } @@ -221,7 +222,7 @@ func (q *Queries) GetLatestScanByTag(ctx context.Context, tagID int64) (Scan, er } const getOrderByID = `-- name: GetOrderByID :one -SELECT id, account_id, status, created_at, updated_at FROM orders WHERE id = $1 +SELECT id, account_id, status, created_at, updated_at, amount FROM orders WHERE id = $1 ` func (q *Queries) GetOrderByID(ctx context.Context, id int64) (Order, error) { @@ -233,6 +234,7 @@ func (q *Queries) GetOrderByID(ctx context.Context, id int64) (Order, error) { &i.Status, &i.CreatedAt, &i.UpdatedAt, + &i.Amount, ) return i, err } @@ -361,7 +363,7 @@ func (q *Queries) GetTagByID(ctx context.Context, id int64) (Tag, error) { } const getUserByEmail = `-- name: GetUserByEmail :one -SELECT id, email, password_hash, name, phone, created_at, is_admin, remember_token FROM users WHERE email = $1 +SELECT id, email, password_hash, name, phone, created_at, is_admin, remember_token, paused FROM users WHERE email = $1 ` func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) { @@ -376,12 +378,13 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error &i.CreatedAt, &i.IsAdmin, &i.RememberToken, + &i.Paused, ) return i, err } const getUserByID = `-- name: GetUserByID :one -SELECT id, email, password_hash, name, phone, created_at, is_admin, remember_token FROM users WHERE id = $1 +SELECT id, email, password_hash, name, phone, created_at, is_admin, remember_token, paused FROM users WHERE id = $1 ` func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) { @@ -396,6 +399,7 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) { &i.CreatedAt, &i.IsAdmin, &i.RememberToken, + &i.Paused, ) return i, err } diff --git a/frontend/internal/handlers/handlers.go b/frontend/internal/handlers/handlers.go index 6cd5cdc..006df8f 100644 --- a/frontend/internal/handlers/handlers.go +++ b/frontend/internal/handlers/handlers.go @@ -52,6 +52,9 @@ func LoadTemplates() (Templates, error) { base := dir + "/base.html" pages := map[string][]string{ "index": {dir + "/index.html"}, + "about": {dir + "/about.html"}, + "what": {dir + "/what.html"}, + "contact": {dir + "/contact.html"}, "register": {dir + "/register.html"}, "login": {dir + "/login.html"}, "account": {dir + "/account.html", dir + "/account-panel.html", dir + "/tag-list.html", dir + "/tag-edit-inline.html"}, diff --git a/frontend/internal/handlers/pages.go b/frontend/internal/handlers/pages.go new file mode 100644 index 0000000..d74dba1 --- /dev/null +++ b/frontend/internal/handlers/pages.go @@ -0,0 +1,18 @@ +package handlers + +import "net/http" + +// About renders the About page. +func (a *App) About(w http.ResponseWriter, r *http.Request) { + a.render(w, r, "about", "About", nil, "") +} + +// WhatIsThis renders the "what is this?" explainer page. +func (a *App) WhatIsThis(w http.ResponseWriter, r *http.Request) { + a.render(w, r, "what", "What is this?", nil, "") +} + +// Contact renders the contact page. +func (a *App) Contact(w http.ResponseWriter, r *http.Request) { + a.render(w, r, "contact", "Contact", nil, "") +} diff --git a/frontend/internal/handlers/scan.go b/frontend/internal/handlers/scan.go index 6fc41b3..a306d15 100644 --- a/frontend/internal/handlers/scan.go +++ b/frontend/internal/handlers/scan.go @@ -98,6 +98,13 @@ func (a *App) shouldAlert(ctx context.Context, tag db.Tag, scan db.Scan, hasLoc return false } + // Customer pause (admin kill-switch): paused owner -> record only. + if tag.OwnerID.Valid { + if owner, err := a.Queries.GetUserByID(ctx, tag.OwnerID.Int64); err == nil && owner.Paused { + return false + } + } + // 24 h per-device block: same fingerprint seen recently (excluding this scan) => record only. if fp := scan.Fingerprint.String; fp != "" { if _, err := a.Queries.GetRecentScanByFingerprint(ctx, db.GetRecentScanByFingerprintParams{Fingerprint: pgtype.Text{String: fp, Valid: true}, ID: scan.ID}); err == nil { @@ -279,6 +286,12 @@ func (a *App) FinderContact(w http.ResponseWriter, r *http.Request) { // Should we notify? Skip when the same phone was alerted within the window, // or when this device (fingerprint) has already alerted in the last 24 h. notify := tag.SmsEnabled && tag.Phone.Valid && a.Sender != nil + // Customer pause: paused owner -> store only. + if notify && tag.OwnerID.Valid { + if owner, err := a.Queries.GetUserByID(r.Context(), tag.OwnerID.Int64); err == nil && owner.Paused { + notify = false + } + } // Metering: metered tags must have credits remaining. if notify && tag.SmsAllocated > 0 && tag.SmsUsed >= tag.SmsAllocated { notify = false diff --git a/frontend/main.go b/frontend/main.go index 3c5edd9..54e75d0 100644 --- a/frontend/main.go +++ b/frontend/main.go @@ -65,6 +65,9 @@ func main() { mux := http.NewServeMux() mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) mux.HandleFunc("GET /{$}", app.Home) + mux.HandleFunc("GET /about", app.About) + mux.HandleFunc("GET /what-is-this", app.WhatIsThis) + mux.HandleFunc("GET /contact", app.Contact) mux.HandleFunc("/{path...}", http.NotFound) mux.HandleFunc("GET /t/{tag_code}", app.PublicTag) mux.HandleFunc("GET /s/{code}", app.ServeShort) diff --git a/frontend/templates/about.html b/frontend/templates/about.html new file mode 100644 index 0000000..5b9d4c1 --- /dev/null +++ b/frontend/templates/about.html @@ -0,0 +1,21 @@ +{{define "content"}} +
+

About Where Woof

+

+ Where Woof makes it easy to get lost things โ€” and lost dogs โ€” home. Each Where Woof tag carries a + unique code. If your dog, baggage or gear goes missing, a finder scans the tag and sees exactly how + to return it: who to call, where to bring it, and how to reach you. +

+

+ When a finder scans your tag and shares their location, you get an instant SMS with a link to a map + showing where it was found. No app required โ€” it just works from any phone. +

+

How it works

+ +
+{{end}} diff --git a/frontend/templates/contact.html b/frontend/templates/contact.html new file mode 100644 index 0000000..19d0582 --- /dev/null +++ b/frontend/templates/contact.html @@ -0,0 +1,11 @@ +{{define "content"}} +
+

Get in touch

+

Questions, lost-tag help, or bulk orders? We'd love to hear from you.

+
+

Email: info@where-woof.com

+

Website: where-woof.com

+

For bulk tags (parks, trails, campgrounds) mention your use case and volume.

+
+
+{{end}} diff --git a/frontend/templates/index.html b/frontend/templates/index.html index 90df713..dd29886 100644 --- a/frontend/templates/index.html +++ b/frontend/templates/index.html @@ -17,8 +17,14 @@
๐Ÿ”

I found something

Scan the tag you found โ€” the page shows the owner's return details.

- My account + What is this? + +

+ About ยท + What is this? ยท + Get in touch +

{{end}} diff --git a/frontend/templates/what.html b/frontend/templates/what.html new file mode 100644 index 0000000..fdb5238 --- /dev/null +++ b/frontend/templates/what.html @@ -0,0 +1,20 @@ +{{define "content"}} +
+

What is this?

+

+ You've scanned a Where Woof tag. This page belongs to the item's owner and shows how to return it. +

+
+

If you found this item

+ +

Thank you for helping bring something (or someone) home!

+
+

+ This tag belongs to a Where Woof customer. +

+
+{{end}} diff --git a/openspec/changes/admin-insights-pages/tasks.md b/openspec/changes/admin-insights-pages/tasks.md index 5d956e1..b07f736 100644 --- a/openspec/changes/admin-insights-pages/tasks.md +++ b/openspec/changes/admin-insights-pages/tasks.md @@ -1,17 +1,17 @@ ## 1. Schema & Go -- [ ] 1.1 `db/schema.sql`: idempotent `users.paused` + `orders.amount`; `make db-up`; `make generate` -- [ ] 1.2 `scan.go`: `shouldAlert` + FinderContact โ€” load owner, return false when paused -- [ ] 1.3 Static pages: `about.go` handlers (About/WhatIsThis/Contact) + templates + routes + template sets +- [x] 1.1 `db/schema.sql`: idempotent `users.paused` + `orders.amount`; `make db-up`; `make generate` +- [x] 1.2 `scan.go`: `shouldAlert` + FinderContact โ€” load owner, return false when paused +- [x] 1.3 Static pages: `about.go` handlers (About/WhatIsThis/Contact) + templates + routes + template sets ## 2. Admin -- [ ] 2.1 StatsOverview: Customers + Revenue stats -- [ ] 2.2 OrderResource: `amount` input + column; UserResource: `paused` toggle + column -- [ ] 2.3 `UserResource/RelationManagers/TagsRelationManager.php` (code, status, item type) +- [x] 2.1 StatsOverview: Customers + Revenue stats +- [x] 2.2 OrderResource: `amount` input + column; UserResource: `paused` toggle + column +- [x] 2.3 `UserResource/RelationManagers/TagsRelationManager.php` (code, status, item type) ## 3. Verification -- [ ] 3.1 New suite: paused owner โ†’ no alert (scan recorded); unpaused โ†’ alert; /about /what-is-this /contact render 200 -- [ ] 3.2 Regressions (72) green; deploy (frontend binary+templates; admin rebuild/restart) -- [ ] 3.3 `openspec validate admin-insights-pages`; commit +- [x] 3.1 New suite: paused owner โ†’ no alert (scan recorded); unpaused โ†’ alert; /about /what-is-this /contact render 200 +- [x] 3.2 Regressions (72) green; deploy (frontend binary+templates; admin rebuild/restart) +- [x] 3.3 `openspec validate admin-insights-pages`; commit