openspec: frontend-foundation change — proposal, design, specs (4 capabilities), tasks

This commit is contained in:
2026-08-05 12:51:44 +10:00
parent c2d6f00241
commit b846c2c58e
9 changed files with 284 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-05

View File

@@ -0,0 +1,3 @@
# frontend-foundation
GOAT front-end foundation: scaffold, Postgres schema, auth, tag setup, public tag page, account CRUD

View File

@@ -0,0 +1,62 @@
## Context
WhereWoof (return-tag platform, see `where_woof.md`) has no code yet. This change builds the GOAT front-end foundation: a Go application serving the owner experience (auth, tag setup/management) and the public tag page, against a single shared Postgres database. The Laravel admin, scan flow (geolocation/SMS), billing, and deployment are later phases — this change deliberately excludes them.
## Goals / Non-Goals
**Goals:**
- A working Go app (`frontend/`) with register/login/logout, My Tags CRUD, tag setup, and the public `/t/:tag_code` page.
- Postgres schema (`db/schema.sql`) matching the settled data model (users, tags, scans).
- The GOAT experience: server-rendered templates + HTMX partial swaps + Alpine for small client behaviour + Tailwind styling.
- Clean seams so Phase 2 (scan flow) and Phase 4 (Laravel) can build on the same schema.
**Non-Goals:**
- Scan flow (geolocation, SMS alerts, `tel:`/`sms:` links) — Phase 2.
- Photo uploads (URL field only) — Phase 6.
- Laravel admin, billing, plans, scan-limit enforcement — later phases.
- Deployment/rsync/Caddy — Phase 7.
- Production tag provisioning UI (tags are pre-coded at manufacture; admin seeds them).
## Decisions
1. **Go stdlib `net/http` with Go 1.22+ ServeMux — no web framework.**
The enhanced mux supports method + wildcard patterns (`GET /t/{tag_code}`). Rationale: zero dependencies, the plan explicitly says "no framework", and it keeps the skills-update surface small. *Alternative considered:* chi — rejected as unnecessary for ~10 routes.
2. **Postgres access via `pgx` (pool) + `sqlc` (generated queries).**
`sqlc` generates type-safe query code from SQL files, so schema and queries stay in sync and compile-checked. Rationale: settled decision; sqlc is also a good skills-update item. *Alternative:* raw `database/sql` — more boilerplate, no compile-time safety.
3. **Sessions: signed cookie sessions (gorilla/sessions, cookie store).**
Cookie holds the user id, signed + encrypted with a server secret from env. Rationale: no session table needed in Phase 1, stateless, simple. *Alternative:* DB-backed sessions — better revocation control, but an extra table and lookup; revisit under anti-abuse (Phase 9).
4. **Passwords: bcrypt** (`golang.org/x/crypto/bcrypt`). Standard, no discussion.
5. **Tag code: opaque random string (URL-safe, ~10 chars), stored as TEXT, unique.**
Printed on QR + NFC at manufacture. Phase 1 uses a seed script with test codes (e.g. `TEST000001`); the public tag-creation UI belongs to the Laravel admin later. No public endpoint creates tags — that's the anti-abuse posture from day one.
6. **Schema ownership: `db/schema.sql` is canonical until Laravel exists.**
Idempotent (`CREATE TABLE IF NOT EXISTS`) and applied via a `make db-up` target against `DATABASE_URL`. Laravel takes over migration ownership in Phase 4 (settled decision), generating migrations from this file.
7. **Templates: `html/template` with a base layout + per-page templates; HTMX partials for the account list/edit flows; Alpine for the tag page's small behaviours; Tailwind via CDN.**
Rationale: matches the plan's UI sketch philosophy and keeps Phase 1 dependency-light. CDN Tailwind is fine now; a build step can replace it under Phase 3 polish.
## Risks / Trade-offs
- [Cookie-session tampering/revocation limits] → Secret from env, `HttpOnly` + `SameSite=Lax`; DB-backed sessions listed under Phase 9 hardening.
- [Tag-code enumeration — public page accepts any code] → Codes are long random strings; unknown codes return a generic "not found" page; rate limiting deferred to anti-abuse phase.
- [sqlc codegen adds a build step] → `make generate` target; queries kept in one `queries.sql`.
- [No framework = more wiring (routing, parsing, validation)] → Small surface (~12 routes, 4 forms); helpers in `handlers/`.
- [Tailwind CDN in production is a network dependency] → Accepted for Phase 1; replace with a compiled build under Phase 3.
- [Schema drift if Laravel later re-migrates] → Phase 4 generates Laravel migrations from `db/schema.sql`; drift caught by matching table definitions.
## Migration Plan
1. Apply `db/schema.sql` to a dev Postgres (`make db-up`); create the DB if missing.
2. Seed test tag codes (`make seed`) for manual verification.
3. Run the app (`DATABASE_URL=... go run .`), verify register → add tag → edit details → public page.
4. No rollback path needed beyond dropping the dev DB; production rollout is Phase 7.
## Open Questions
- Exact tag code format/length (manufacturer decision) — placeholder: 10-char URL-safe base32.
- Production Postgres hosting (likely a Docker PG on `.13`) — Phase 7.
- Session store choice revisited under anti-abuse hardening (Phase 9).

View File

@@ -0,0 +1,32 @@
## Why
WhereWoof is a return-item tag platform: buyers get pre-coded QR/NFC tags, a finder scans the tag on a lost item and sees how to return it. The product plan (`where_woof.md`) is settled; nothing of it is buildable yet. This change creates the front-end foundation — the GOAT (Go + HTMX + Alpine + Tailwind + Postgres) app that carries the owner-facing experience: tag setup, account, and the public tag page.
## What Changes
- New Go application in `frontend/` (Go `net/http`, HTMX, Alpine.js, Tailwind via CDN) talking to **Postgres** via `pgx`/`sqlc`.
- Shared database schema (`db/schema.sql`): `users`, `tags`, `scans` tables. Laravel will own migrations later; until then `schema.sql` is the source of truth.
- **User auth**: register / login / logout with email + password, session cookie.
- **Tag management (account CRUD)**: add a tag to the account by entering its `tag_code`, list my tags, edit details (item type, description, photo URL, phone, address, notes), remove a tag. Limit of 20 tags per account enforced.
- **Public tag page** `GET /t/:tag_code`: shows a "not set up yet" prompt when the tag is unset; shows the item's return details (with an edit affordance for a logged-in owner) when set.
- **Scan flow is NOT in this change** (Phase 2): no geolocation, no SMS alerts, no `tel:`/`sms:` links yet — the page renders details only.
## Capabilities
### New Capabilities
- `user-auth`: register, login, logout, session handling for owners.
- `tag-management`: bind a tag_code to an account, list/edit/remove tags, 20-tag limit, item details fields.
- `public-tag-page`: unauthenticated view of a tag by code — setup prompt or return details.
- `database`: shared Postgres schema (users, tags, scans), unique tag codes, tag status lifecycle.
### Modified Capabilities
<!-- none — no existing specs yet -->
## Impact
- **New code**: `frontend/` Go module (main.go, db, handlers, templates, static). No framework beyond stdlib.
- **New schema**: `db/schema.sql` + Postgres database (dev: Supabase Postgres on `.27:5434`).
- **Config**: `DATABASE_URL` env var; port 3020 default.
- **No changes** to admin (Laravel — later phase), deployment (rsync/Caddy — Phase 7), or billing (Phase 5).

View File

@@ -0,0 +1,33 @@
## ADDED Requirements
### Requirement: Shared Postgres database
The system SHALL use a single shared PostgreSQL database for all data. The canonical schema SHALL live in `db/schema.sql` until the Laravel admin takes over migration ownership.
#### Scenario: Schema applied to a fresh database
- **WHEN** `db/schema.sql` is applied to an empty database
- **THEN** the `users`, `tags`, and `scans` tables exist with the defined columns and constraints
### Requirement: Users table
The `users` table SHALL store id, unique email, password hash, name, phone, and created_at.
#### Scenario: Unique emails
- **WHEN** a second row is inserted with an email that already exists
- **THEN** the database rejects the insert
### Requirement: Tags table
The `tags` table SHALL store a unique public `tag_code`, owner reference, status (`unset`, `active`, `suspended`), item type, description, photo URL, phone, address, notes, and timestamps.
#### Scenario: Unique tag codes
- **WHEN** a second row is inserted with a tag_code that already exists
- **THEN** the database rejects the insert
#### Scenario: Status values
- **WHEN** a row is inserted with a status not in the allowed set
- **THEN** the database rejects the insert
### Requirement: Scans table
The `scans` table SHALL record tag scans with timestamp, optional latitude/longitude, whether location was shared, optional scanner phone, and whether an alert was sent.
#### Scenario: Recording a scan
- **WHEN** a scan of a tag is recorded
- **THEN** a row exists with tag reference, timestamp, and location flags

View File

@@ -0,0 +1,41 @@
## ADDED Requirements
### Requirement: Public tag page by code
The system SHALL serve an unauthenticated page at `/t/<tag_code>` for any valid tag code.
#### Scenario: Valid code
- **WHEN** a visitor opens `/t/<tag_code>` for a tag that exists
- **THEN** the tag page is rendered
#### Scenario: Unknown code
- **WHEN** a visitor opens `/t/<tag_code>` for a code that does not exist
- **THEN** a friendly "tag not found" page is shown
### Requirement: Unset tag shows setup prompt
When the tag is not yet bound to an account (status `unset`), the tag page SHALL tell the visitor the tag is not set up and show how to claim it.
#### Scenario: First scan of an unset tag
- **WHEN** a visitor opens the page of a tag with status `unset`
- **THEN** the page shows a "this tag is not set up yet" message and a link to register/log in
### Requirement: Set tag shows return details
When the tag is bound to an account and has details (status `active`), the tag page SHALL show the item's return details: item type, description, photo, phone, address, notes — for any visitor without login.
#### Scenario: Finder views an active tag
- **WHEN** a visitor opens the page of a tag with status `active`
- **THEN** the page shows the item's return details without requiring login
#### Scenario: Suspended tag
- **WHEN** a visitor opens the page of a tag with status `suspended`
- **THEN** the page shows only that the tag is unavailable
### Requirement: Owner edit affordance
A logged-in owner viewing their own tag page SHALL see an edit link to the account CRUD page for that tag.
#### Scenario: Owner is logged in
- **WHEN** the authenticated owner opens the page of their own tag
- **THEN** an edit link to the tag's edit page is shown
#### Scenario: Visitor is not the owner
- **WHEN** a visitor who is not the owner opens the page
- **THEN** no edit link is shown

View File

@@ -0,0 +1,44 @@
## ADDED Requirements
### Requirement: Add a tag to the account
An authenticated owner SHALL be able to bind a tag to their account by entering its tag code. A tag that is already bound to another account SHALL be rejected.
#### Scenario: Bind an unset tag
- **WHEN** an owner enters the code of a tag that is not yet bound to any account
- **THEN** the tag becomes owned by that account and is listed under My Tags
#### Scenario: Bind an already-owned tag
- **WHEN** an owner enters the code of a tag already bound to another account
- **THEN** the operation is rejected with an error message
### Requirement: List my tags
The account page SHALL list all tags owned by the authenticated owner, with their status and a link to edit each.
#### Scenario: Owner with tags
- **WHEN** an owner opens the account page
- **THEN** all of their tags are shown with status (unset/active) and an edit link
### Requirement: Edit tag details
An owner SHALL be able to set or edit the return details of a tag they own: item type (dog, cat, baggage, skis, other), description, photo URL, phone number, address, and notes.
#### Scenario: Complete tag setup
- **WHEN** an owner fills in all detail fields for a tag and saves
- **THEN** the tag status becomes active and the public page shows the details
#### Scenario: Partial details saved
- **WHEN** an owner saves a tag with only some fields filled
- **THEN** the tag is saved and the public page shows only the filled fields
### Requirement: Remove a tag
An owner SHALL be able to remove a tag from their account. Removing a tag SHALL revert it to the unset state so it can be bound to another account.
#### Scenario: Remove and re-bind
- **WHEN** an owner removes a tag and another account later enters its code
- **THEN** the second account can bind it
### Requirement: Tag ownership limit
An account SHALL be limited to 20 owned tags.
#### Scenario: Limit reached
- **WHEN** an owner who already owns 20 tags tries to bind another tag
- **THEN** the operation is rejected with an error message

View File

@@ -0,0 +1,34 @@
## ADDED Requirements
### Requirement: Account registration
The system SHALL allow a new owner to register with a unique email address and a password. The password SHALL be stored only as a salted hash.
#### Scenario: Successful registration
- **WHEN** a visitor submits a new email and password on the register page
- **THEN** an account is created and the visitor is logged in
#### Scenario: Duplicate email
- **WHEN** a visitor registers with an email that already exists
- **THEN** registration is rejected with an error message
### Requirement: Login and logout
The system SHALL allow an owner to log in with email and password and to log out, maintaining an authenticated session via a signed session cookie.
#### Scenario: Successful login
- **WHEN** an owner submits the correct email and password
- **THEN** a session cookie is set and the owner is authenticated
#### Scenario: Incorrect password
- **WHEN** an owner submits a wrong password
- **THEN** login is rejected with an error message and no session is created
#### Scenario: Logout
- **WHEN** an authenticated owner clicks logout
- **THEN** the session is destroyed and the owner is redirected to the home page
### Requirement: Session-protected routes
Routes that manage tags SHALL require an authenticated session and redirect unauthenticated visitors to the login page.
#### Scenario: Unauthenticated access to account
- **WHEN** a visitor who is not logged in opens the account page
- **THEN** they are redirected to the login page

View File

@@ -0,0 +1,33 @@
## 1. Scaffold & Database
- [ ] 1.1 Initialize Go module `frontend/` (go 1.22+), directory layout: `internal/db`, `internal/auth`, `internal/handlers`, `templates/`, `static/`
- [ ] 1.2 Create `db/schema.sql` (users, tags, scans per database spec) — idempotent `CREATE TABLE IF NOT EXISTS`
- [ ] 1.3 Add `Makefile` targets: `db-up` (apply schema), `seed`, `generate` (sqlc), `run`
- [ ] 1.4 Add `sqlc.yaml` + `internal/db/queries.sql` covering: create user, get user by email/id, bind tag, list tags by owner, get tag by code, update tag details, remove tag; run `sqlc generate`
- [ ] 1.5 Implement config (DATABASE_URL, SESSION_SECRET, port 3020) and `main.go` wiring pgx pool + router + static/template serving
## 2. Auth (user-auth spec)
- [ ] 2.1 Register: handler + template (email, password, name), bcrypt hash, unique-email error, auto-login after register
- [ ] 2.2 Login/logout: handlers + gorilla/sessions cookie store (HttpOnly, SameSite=Lax), redirect to account after login
- [ ] 2.3 RequireAuth middleware for account routes + `currentUser` helper injected into all templates
## 3. Tag Management (tag-management spec)
- [ ] 3.1 Add-tag handler: bind by tag code — reject already-bound tags, enforce 20-tag limit, show errors
- [ ] 3.2 My Tags page: list owned tags with status + edit links
- [ ] 3.3 Tag edit page + form: item type (dog/cat/baggage/skis/other), description, photo URL, phone, address, notes; saving sets status `active`
- [ ] 3.4 Remove-tag handler: reverts tag to `unset` (re-bindable)
## 4. Public Tag Page (public-tag-page spec)
- [ ] 4.1 `GET /t/{tag_code}` handler with tag lookup + friendly "tag not found" page for unknown codes
- [ ] 4.2 Render logic: `unset` → setup prompt with register/login links; `active` → return details; `suspended` → unavailable message
- [ ] 4.3 Owner edit affordance: edit link shown only when logged-in user owns the tag
- [ ] 4.4 Base layout + home page with Tailwind (CDN) styling; wire static assets
## 5. Seed & Verification
- [ ] 5.1 Seed script inserting test tag codes (e.g. TEST000001..TEST000010)
- [ ] 5.2 End-to-end manual verification of every spec scenario: register, duplicate email, login/logout, unauth redirect, bind unset tag, bind owned tag rejected, 20-tag limit, edit → active, remove → re-bind, unknown code page, suspended tag
- [ ] 5.3 Run `openspec validate frontend-foundation`; commit all artifacts and code