Rewrite plan: return-tag platform (not tracker), GOAT + shared Postgres

This commit is contained in:
2026-08-05 12:48:58 +10:00
parent 9450834463
commit c2d6f00241
2 changed files with 117 additions and 188 deletions

View File

@@ -1 +1,8 @@
(B) Set up Gitea for where_woof +project:where-woof +agent:where_woof (A) openspec: init + project.md + change frontend-foundation +project:where-woof +agent:where_woof
(B) plannotator: review frontend-foundation plan +project:where-woof +agent:where_woof
(C) Execute Phase 1 (GOAT: scaffold, schema, auth, tag setup, public page) +project:where-woof +agent:where_woof
(D) Execute Phase 2 (scan flow: geolocation -> SMS, tel/sms links, finder contact) +project:where-woof +agent:where_woof
(E) Laravel admin phase (users, tags, dashboard) +project:where-woof +agent:where_woof
(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

View File

@@ -1,250 +1,172 @@
# WhereWoof — Local Pet Location Tracker # WhereWoof — Return-Tag Platform
## Architecture — Two Separate Systems WhereWoof is a **return-item tag platform** (PetHub / ReturnMe style). Physical tags are pre-coded with a unique QR code and NFC chip, each pointing to its own URL. A finder scans the tag on a lost item (dog, baggage, skis…) and immediately sees how to return it.
### 1. Front-Facing Website (GOAT stack) > **This is NOT a location/GPS tracker.** The tag has no electronics beyond the printed code and NFC. The system carries the *return details*, and — only when a finder permits — sends the *finder's* location to the owner so they can collect their item.
Quick win — lightweight, public-facing, pet owner experience. ## How it works — customer journey
| Detail | Value | 1. **Buy** — user buys a tag (pre-coded, unique `tag_code` printed on QR + NFC).
|--------|-------| 2. **First scan / first visit**`https://where-woof.com/t/<tag_code>` shows "This tag is not set up yet."
| **Stack** | Go + HTMX + Alpine.js + Tailwind CSS + SQLite (**GOAT stack**) | 3. **Set up** — the user creates an account or logs in → **adds the tag to their account** → fills in the item details.
| **Audience** | Pet owners (family members) Lost pets| 4. **Details form** — item type (dog / cat / baggage / skis / other), description, photo, phone number, address, notes.
| **Purpose** | View pet info, update location, quick CRUD | 5. **Done** — scanning the tag from now on shows the return details.
| **Auth** | Minimal — HTTP basic auth or Tailscale-only (household, not public) | 6. **Edit** — the tag page shows an edit icon for a logged-in owner → account CRUD page. The account page is also reachable from the main site (`where-woof.com`).
| **Deploy** | .13:3020 behind Caddy on .35 |
| **DNS** | `where-woof.com` |
## Scan flow — finder experience (front-end)
URL format. https://where-woof.com/x=5&productid=TXQ3NHR4OGNuZGVwdFdkcFBTbm5BZz094 When someone finds a lost item and scans the tag:
Will be redirected from live website to local .13 where caddy will handle local DNS.
**IMPORTANT INFO** 1. Page loads the return details: item type, description, photo, owner contact.
- Users scan QR code on tag or use NFC. This opens webiste at URL example. On load website asks to ALLOW USERS LOCATION to send info to owners. If request is granted location is sent back with productid and the server admin with send alerts via SMS, email of location of dog. Info of dog is displayed with phone number - press for automatic call hook into phone. Also a button if user declines location then they can press button to initiate again. Also input box for the person who has found the dog to input their mobile. Finder of dog is advised that number is not kept only sent to owner so they can contact you. 2. **Geolocation prompt** (browser permission). If granted, the location is sent immediately to the server → **SMS alert to the owner** with the coordinates. If rejected, the page still works fully.
3. **Ring owner**`tel:` link that dials the owner's number directly.
4. **Re-check location** — button to re-request the finder's location (in case they changed their mind).
5. **Send SMS**`sms:` link with a prefilled message asking the finder for their phone number (opens their SMS app).
6. **Finder contact input** — the finder can type their mobile number; it is forwarded to the owner (not stored long-term).
7. The location button is hidden once location has been shared.
### 2. Admin Dashboard (Laravel) > ⚠️ **Auto-reading the finder's phone number is NOT possible on the web** — there is no browser API for it (privacy restriction; only native apps can). Manual input only.
Separate system — full business management. Needs proper auth, billing, multi-user. ## Architecture — two systems, one shared Postgres
| Detail | Value | | System | Stack | Port / DNS | Audience | Purpose |
|--------|-------| |--------|-------|-----------|----------|---------|
| **Stack** | Laravel + MySQL/PostgreSQL + Blade (or Filament/Nova) | | **Front-end** | **GOAT**: Go + HTMX + Alpine.js + Tailwind + **Postgres** | `.13:3020``where-woof.com` | Finders + owners | Public tag pages, scan flow, account setup/CRUD |
| **Audience** | Staff / admins | | **Admin** | Laravel + Filament/Cashier + Postgres | `.13:3030``admin.where-woof.com` | Staff (Sam) | Users, tags, plans, billing, system management |
| **Purpose** | User management, subscriptions, payments, device provisioning |
| **Auth** | Laravel built-in (roles, permissions, password reset, 2FA) |
| **Deploy** | .13:3030 (or Docker container) behind Caddy on .35 |
| **DNS** | `admin.where-woof.com` |
**Why separate:** **One shared Postgres database.** Both apps are peers over it — the Go app reads/writes directly (via `pgx` / `sqlc`), Laravel manages the same schema, and **Laravel owns migrations** as the long-term convention. (Until Laravel exists, a canonical `db/schema.sql` is the source of truth.)
- The GOAT frontend is an HTMX experiment — fast UI, minimal JS, quick to build
- The admin needs forms-heavy workflows, role-based access, payment processing — Laravel excels here with Filament (admin panel generator), Cashier (Stripe/Paddle billing), and built-in auth
- Two codebases, one shared SQLite/MySQL for pet/location data (read by both)
--- **Why this design:**
- The GOAT stack is a **skills-update exercise** — the point is Go + HTMX + Alpine + Tailwind as the front-end stack. The "S" (SQLite) from the original AI-written plan is swappable and is **dropped in favour of Postgres**: a multi-tenant product needs one real database, not hundreds of per-user SQLite files (no cross-user queries, migration/backup nightmare, Laravel can't query them).
- Dev Postgres already available locally: Supabase Postgres on `.27:5434`.
- Both apps sharing one DB means no sync layer and one backup.
## Features **Domains:**
- Live site at InMotion Hosting redirects → `where-woof.com`**.35 (Caddy)** → **.13** (apps).
- `admin.where-woof.com` → .35 → .13:3030.
### Frontend (GOAT — Quick Win) ## Database schema (v2)
- **Add a pet** — form with name, type (dog/cat/other), photo URL, Address, Two phones
- **Update location** —
- **View last known** — table showing each pet's last reported location + time ago
- **Delete pet** — soft delete
### Admin (Laravel)
- **User management** — CRUD, roles (admin/staff/owner), password reset, 2FA
- **Subscription & payments** — Stripe/Paddle via Laravel Cashier, invoices, overdue tracking
- **Device provisioning** — register new trackers, assign to user accounts, activation keys
- **Dashboard** — overdue accounts, expiring subscriptions, new signups, active devices
- **Audit log** — who changed what, login history
### Future / Shared
- Location history log per pet (timeline view)
- "Notify me" → NTFY ping when a pet's location updates
- Map view (leaflet.js) of all current locations
- Multi-user family group linking
---
## Tech Details
### Stack Rationale
The **GOAT stack** — **G**o + **A**lpine.js + **T**ailwind + HTMX + SQLite:
- **Go `net/http`** — routing, form handling, template rendering. No framework.
- **HTMX** — `hx-post`, `hx-get`, `hx-delete` for inline form submission, live table updates, delete confirmations — no page reloads.
- **Alpine.js** — form validation, dropdown bindings, timestamp formatting (`x-data`, `x-model`, `x-init`).
- **Tailwind CSS** — utility-first styling via CDN, rapid UI without CSS files.
- **SQLite** — single file DB (`data.db`), zero config, survives restarts. Migrations via `schema.sql`.
### Database Schema
```sql ```sql
CREATE TABLE pets ( CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL, email TEXT NOT NULL UNIQUE,
type TEXT NOT NULL CHECK(type IN ('dog', 'cat', 'other')), password_hash TEXT NOT NULL,
chip_id TEXT UNIQUE, name TEXT,
phone TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE tags (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tag_code TEXT NOT NULL UNIQUE, -- printed on QR + NFC, public identifier
owner_id BIGINT REFERENCES users(id),
status TEXT NOT NULL DEFAULT 'unset'
CHECK (status IN ('unset', 'active', 'suspended')),
item_type TEXT CHECK (item_type IN ('dog', 'cat', 'baggage', 'skis', 'other')),
description TEXT,
photo_url TEXT, photo_url TEXT,
created_at TEXT DEFAULT (datetime('now')), phone TEXT, -- owner contact phone (shown via tel:)
deleted INTEGER DEFAULT 0 address TEXT,
);
CREATE TABLE locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pet_id INTEGER NOT NULL REFERENCES pets(id),
location TEXT NOT NULL, -- human-readable: "Backyard", "Vet, Main St"
notes TEXT, notes TEXT,
reported_at TEXT DEFAULT (datetime('now')) created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
); );
CREATE INDEX idx_locations_pet_time ON locations(pet_id, reported_at DESC); CREATE TABLE scans (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tag_id BIGINT NOT NULL REFERENCES tags(id),
scanned_at TIMESTAMPTZ NOT NULL DEFAULT now(),
lat DOUBLE PRECISION,
lng DOUBLE PRECISION,
location_shared BOOLEAN NOT NULL DEFAULT FALSE,
scanner_phone TEXT,
alert_sent BOOLEAN NOT NULL DEFAULT FALSE
);
``` ```
### Endpoints *Plans / subscriptions tables come later with billing (Phase 5).*
| Method | Path | Handler | UI Behavior | ## Accounts, plans & anti-abuse
|--------|------|---------|-------------|
| `GET` | `/` | `ServeIndex` | Full page: pet list table + "New Location" dropdown + "Add Pet" button |
| `GET` | `/pets/table` | `ServePetTable` | HTMX: re-renders the pet+location table |
| `POST` | `/pets` | `CreatePet` | HTMX: form submit → table update, form reset |
| `DELETE` | `/pets/:id` | `DeletePet` | HTMX: confirm → row fade-out |
| `POST` | `/pets/:id/locations` | `AddLocation` | HTMX: dropdown form submit → table cell updates |
| `GET` | `/pets/:id/history` | `ServeHistory` | HTMX: expand row to show location timeline |
### UI Sketch - One account can hold **1020 tags** (limit enforced).
- Business model: **annual fee + per-tag cost + max scans per plan**.
- **Scan limits are the anti-scam / cost protection** — every scan can trigger an SMS (which costs us money), so caps prevent SMS-bombing and tag re-selling abuse.
- Tag status: `unset``active``suspended` (admin can suspend).
- Billing: **Stripe (Australia)****NOT in the front-end phase**; phased later.
``` ## Endpoints — GOAT front-end
┌──────────────────────────────────────────────────────────────┐
│ WhereWoof [+ Add Pet] │
├──────────────────────────────────────────────────────────────┤
│ │
│ Quick Update: [Buddy ▼] is at [_______________] [Update] │
│ │
├──────────┬────────┬──────────────────┬──────────┬─────────────┤
│ Pet │ Type │ Last Seen │ Location │ Actions │
├──────────┼────────┼──────────────────┼──────────┼─────────────┤
│ 🐕 Buddy│ dog │ 10 min ago │ Backyard │ [History][✕]│
│ 🐈 Mittens│ cat │ 2 hours ago │ Couch │ [History][✕]│
│ 🐕 Rocky │ dog │ 3 days ago │ Vet │ [History][✕]│
└──────────┴────────┴──────────────────┴──────────┴─────────────┘
```
--- | Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/` | Main site home |
| `GET` | `/t/:tag_code` | Public tag page — setup prompt or details + scan flow |
| `POST` | `/t/:tag_code/scan` | Record scan + optional location → alert owner |
| `POST` | `/t/:tag_code/contact` | Finder leaves their phone number for the owner |
| `GET/POST` | `/register`, `/login`, `/logout` | Account auth (email + password, session cookie) |
| `GET` | `/account` | Account CRUD: my tags |
| `POST` | `/account/tags` | Add a tag to the account (enter tag code) |
| `GET/POST` | `/account/tags/:id/edit` | Edit tag details |
| `POST` | `/account/tags/:id/delete` | Remove tag |
## Directory Structure ## Admin — Laravel (later phases)
``` User management, tag management (suspend/transfer), plans & billing (Stripe/Cashier), scan history & alerts dashboard, audit log. Not built in Phase 1.
/home/sam/home_network/web_sites/where_woof
├── frontend/ # GOAT stack — public pet tracker
│ ├── main.go
│ ├── db.go
│ ├── handlers.go
│ ├── models.go
│ ├── go.mod
│ ├── templates/
│ │ ├── base.html
│ │ ├── index.html
│ │ ├── pet-table.html
│ │ └── location-form.html
│ ├── static/
│ │ └── style.css
│ └── data/
│ └── .gitkeep
├── admin/ # Laravel — staff dashboard
│ ├── (standard Laravel structure)
│ ├── app/Models/
│ │ ├── User.php
│ │ ├── Pet.php
│ │ ├── Subscription.php
│ │ └── Device.php
│ ├── app/Filament/ # Filament admin panel resources
│ ├── database/migrations/
│ └── routes/web.php
└── shared/ # Shared between frontend and admin
└── schema.sql # Core pet/location tables (used by both)
```
---
## Deployment ## Deployment
### Dev (local on .27) ### Dev (local on .27)
```bash ```bash
# Frontend (GOAT) cd frontend && go run . --port 3020 # GOAT front-end
cd /home/sam/home_network/web_sites/where_woof/frontend cd admin && php artisan serve --port 3030 # Laravel (later)
go run . --port 3020
# Admin (Laravel)
cd /home/sam/home_network/web_sites/where_woof/admin
php artisan serve --port 3030
``` ```
### Build + Push to .13 ### Build + push to .13
```bash ```bash
# Frontend — Go binary cd frontend
cd /home/sam/home_network/web_sites/where_woof/frontend
GOOS=linux GOARCH=amd64 go build -o where-woof . GOOS=linux GOARCH=amd64 go build -o where-woof .
rsync -avz where-woof templates/ static/ sam@192.168.20.13:/var/www/where-woof rsync -avz where-woof templates/ static/ sam@192.168.20.13:/var/www/where-woof
# Admin — Laravel
rsync -avz --exclude vendor --exclude node_modules \
/home/sam/home_network/web_sites/where_woof/admin/ \
sam@192.168.20.13:/var/www/where-woof
# On .13: composer install, php artisan migrate
``` ```
Admin (later): rsync + `composer install` + `php artisan migrate` on .13.
### Run on .13 ### Run on .13
```bash ```bash
ssh sam@192.168.20.13
# Frontend
cd /var/www/where-woof && ./where-woof --port 3020 cd /var/www/where-woof && ./where-woof --port 3020
# Admin (needs PHP-FPM + nginx, or Docker)
# Option A: php artisan serve --port 3030 (dev only)
# Option B: Docker with php:8.3-fpm + nginx
``` ```
### Caddy (.35) ### Caddy (.35)
```caddy ```caddy
# Frontend — public pet tracker
where-woof.com { where-woof.com {
reverse_proxy 192.168.20.13:3020 reverse_proxy 192.168.20.13:3020
} }
where-woof.home.lab { where-woof.home.lab {
reverse_proxy 192.168.20.13:3020 reverse_proxy 192.168.20.13:3020
} }
# Admin — staff dashboard
admin.where-woof.com { admin.where-woof.com {
reverse_proxy 192.168.20.13:3030 reverse_proxy 192.168.20.13:3030
} }
``` ```
### Pi-hole DNS ### DNS
- Live site (InMotion) → redirect / DNS update → `where-woof.com` → public DNS → .35 (Caddy) → .13.
- `where-woof.home.lab``192.168.20.35` - Pi-hole: `where-woof.home.lab` → 192.168.20.35.
- `where-woof.com` → public DNS → .35
- admin.where-woof.com → public DNS → .35
---
## Roadmap ## Roadmap
| Phase | What | Status | | Phase | What | Status |
|-------|------|--------| |-------|------|--------|
| 1 | Go scaffold, schema, pet CRUD, table view (frontend) | ⬜ | | 1 | **Front-end foundation**: Go scaffold, Postgres schema, auth, tag setup, public tag page, account CRUD (GOAT) | ⬜ |
| 2 | Quick location update, history timeline (frontend) | ⬜ | | 2 | **Scan flow**: geolocation → SMS alert, `tel:`/`sms:` links, finder contact, re-check button | ⬜ |
| 3 | HTMX polish — inline edits, fade transitions (frontend) | ⬜ | | 3 | HTMX / Alpine polish — inline edit, animations | ⬜ |
| 4 | Laravel admin scaffold — Filament, user CRUD | ⬜ | | 4 | Laravel admin: users, tags, dashboard (same Postgres) | ⬜ |
| 5 | Laravel billing — Cashier, subscriptions, overdue tracking | ⬜ | | 5 | Billing: Stripe (AU), plans, scan-limit enforcement, invoices | ⬜ |
| 6 | NTFY integration for location alerts (frontend) | ⬜ | | 6 | Photo uploads + object storage | ⬜ |
| 7 | Deploy frontend to .13, Caddy DNS, live | ⬜ | | 7 | Deploy front-end to .13, Caddy, DNS, InMotion redirect | ⬜ |
| 8 | Deploy admin to .13, Caddy DNS, live | ⬜ | | 8 | Deploy admin to .13 | ⬜ |
| 9 | Anti-abuse hardening: relay numbers, rate limits | ⬜ |
## Future ideas
- NTFY ping to owner when a scan happens (alongside SMS).
- Tag transfer / reset for resale (admin feature).
- Relay number so the owner's real mobile isn't exposed to scanners.