admin-insights-pages: dashboard customers+revenue (orders.amount), users->tags relation, pause customer (gates alerts), About/What-is-this/Contact pages

This commit is contained in:
2026-08-10 09:19:14 +10:00
parent d80c08306c
commit e1044f8c32
12 changed files with 119 additions and 14 deletions

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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"},

View File

@@ -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, "")
}

View File

@@ -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