openspec: laravel-admin change — Filament admin (users/tags/products/orders/dashboard) on shared Postgres

This commit is contained in:
2026-08-07 20:23:02 +10:00
parent 0ee211d156
commit 224545ac64
11 changed files with 195 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,3 @@
# laravel-admin
Laravel + Filament admin dashboard: users, tags (product/order assignment), products, orders, dashboard stats — shared Postgres, Docker dev on .27

View File

@@ -0,0 +1,41 @@
## Context
Phase 13 built the GOAT frontend + shared Postgres (users, tags, scans, products, orders on `.13:5434`). Phase 4 adds the Laravel admin: a management UI for users, tags (with the product/order linkage built in Phase 2.5), products/orders, and a dashboard. It reuses the same DB — no sync layer.
## Goals / Non-Goals
**Goals:**
- A working Laravel + Filament admin on .27 via Docker (no PHP installed natively).
- Reads/writes the shared Postgres on `.13:5434` (single source of truth).
- Admin login gated by `users.is_admin`; full CRUD for users/tags/products/orders; dashboard stats.
**Non-Goals:**
- Billing/payments (Phase 5).
- Photo uploads in admin (frontend has it; admin can reuse photo_url display).
- Public-facing Laravel pages (this is admin-only for now).
- Laravel owning migrations yet — `db/schema.sql` stays canonical; Eloquent reads the existing tables.
## Decisions
1. **Docker dev on .27**`admin/docker-compose.yml` with `php:8.3-fpm` + a composer bootstrap; `php artisan serve --host=0.0.0.0 --port=3030`. No PHP/Composer installed natively (keeps .27 clean). Docker volume mounts `admin/` so edits are live.
2. **Filament v3** for the admin panel (CRUD resources: User, Tag, Product, Order + dashboard stats). Rationale: the 2014 system needed forms-heavy admin; Filament is the Laravel-idiomatic generator and the best skills-update vehicle for Laravel conventions.
3. **Shared DB via Eloquent** — models map to the existing tables (users, tags, scans, products, orders). No migrations in Phase 4; `db/schema.sql` remains the source of truth (Laravel takes over migrations at deploy, generating from it). `users.is_admin` added via the existing idempotent ALTER in schema.sql.
4. **Auth** — Laravel's session auth + a middleware/policy: `Filament::auth()->user()->is_admin` gate. Seed one admin user (env-configured email, password from env).
5. **Connection**`admin/.env` `DB_CONNECTION=pgsql`, `DB_HOST=192.168.20.13`, `DB_PORT=5434`, `DB_DATABASE=wherewoof`, `DB_USERNAME=wherewoof`, password via env (gitignored `.env`).
## Risks / Trade-offs
- [Docker image pulls + composer install are slow first-time] → one-time cost; cached after.
- [Laravel writing to a schema it doesn't own] → read-only via Eloquent where possible; writes (tag product/order assignment, order status) are simple column updates already exercised by the GOAT app; migrations reconciled at deploy.
- [Shared users table across both apps] → `is_admin` flag only; no schema conflict.
- [NixOS Docker networking to .13] → plain TCP to 192.168.20.13:5434, already proven by the Go app.
## Migration Plan
1. `make db-up` (adds `users.is_admin`).
2. `admin/docker-compose.yml` up → composer create-project laravel/laravel admin → filament install → models/resources.
3. Seed admin user; verify login + CRUD against the shared DB.
## Open Questions
- Whether the admin gets public-facing pages later (likely not — the GOAT frontend serves the public).

View File

@@ -0,0 +1,34 @@
## Why
The product is owner-facing and working (Phases 13). What's missing is the **admin layer**: Where Woof needs a dashboard where the operator manages users, tags, products/orders, and sees what's happening (scans, alerts, lapsed payments). The 2014 system had this via `product_accounts`/`online_orders`; the schema for it (products, orders, tag linkage) was built in Phase 2.5 — Phase 4 delivers the management UI on top, as the long-planned **Laravel + Filament** system.
## What Changes
- **New Laravel app** (`admin/`) running via **Docker** on .27 for dev (php:8.3 + composer), connecting to the **shared Postgres** on `.13:5434` (same DB as the GOAT frontend — one source of truth).
- **Auth**: admin login (Laravel session auth) gated to admin users; new `users.is_admin` flag (idempotent ALTER) so the same `users` table serves both owners and admins.
- **Admin users**: list/search owner accounts, view details (tags, scans), suspend.
- **Admin tags**: list all tags (registry + owned), view each tag's scans/alerts, **assign `product_id` + `order_id`** (the Phase 2.5 linkage), suspend/unsuspend.
- **Products & orders**: CRUD product templates; record orders (status lifecycle pending/paid/lapsed/cancelled).
- **Dashboard**: stats — tags by status, scans in last 24 h, recent scans, lapsed orders.
- **No change** to the GOAT frontend or the shared schema beyond `users.is_admin`.
## Capabilities
### New Capabilities
- `admin-auth`: admin login + gate; `users.is_admin`.
- `admin-users`: manage owner accounts.
- `admin-tags`: manage tags incl. product/order assignment + suspend.
- `admin-products-orders`: CRUD products + orders.
- `admin-dashboard`: operational stats.
### Modified Capabilities
- `database`: ADDED — `users.is_admin` column (idempotent).
## Impact
- **New**: `admin/` Laravel app (Filament panel), `admin/docker-compose.yml`, models (User, Tag, Scan, Product, Order), Filament resources, `.env` for the shared DB.
- **Schema**: idempotent `ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin BOOLEAN NOT NULL DEFAULT FALSE;`
- **Secrets**: `DB_*` env in `admin/.env` (gitignored); no real secrets committed.
- No changes to the GOAT frontend code paths.

View File

@@ -0,0 +1,19 @@
## ADDED Requirements
### Requirement: Admin login
The system SHALL provide a login for admin users at `/admin/login`. Only users with `is_admin = true` SHALL be able to access the admin panel.
#### Scenario: Admin logs in
- **WHEN** an admin user logs in with valid credentials
- **THEN** they reach the admin dashboard
#### Scenario: Non-admin blocked
- **WHEN** an owner (not admin) tries to access the admin panel
- **THEN** access is denied (403 / redirect)
### Requirement: is_admin flag
The `users` table SHALL have an `is_admin` boolean, default false.
#### Scenario: Promote a user
- **WHEN** an existing user is marked admin
- **THEN** they can log into the admin panel

View File

@@ -0,0 +1,8 @@
## ADDED Requirements
### Requirement: Dashboard stats
The admin dashboard SHALL show operational numbers: tags by status (unset/active/suspended), scans in the last 24 hours, recent scans with tag + owner, and lapsed orders.
#### Scenario: View dashboard
- **WHEN** an admin opens the dashboard
- **THEN** the stats and recent activity are displayed

View File

@@ -0,0 +1,15 @@
## ADDED Requirements
### Requirement: Product CRUD
The admin SHALL create, edit, and list product templates (sku unique, name, item type).
#### Scenario: Create a product
- **WHEN** an admin creates a product with a unique sku and name
- **THEN** it appears in the products list and can be assigned to tags
### Requirement: Order CRUD with lifecycle
The admin SHALL create and edit orders with the status lifecycle `pending → paid → lapsed → cancelled`.
#### Scenario: Advance an order status
- **WHEN** an admin changes an order from pending to paid
- **THEN** the order reflects the paid status

View File

@@ -0,0 +1,22 @@
## ADDED Requirements
### Requirement: List and manage tags
The admin SHALL list all tags (registry + owned), searchable, with status and owner shown.
#### Scenario: Browse tags
- **WHEN** an admin opens the tags list
- **THEN** all tags are shown with owner, status, product/order
### Requirement: Assign product and order
The admin SHALL assign a `product_id` and `order_id` to a tag (the Phase 2.5 linkage).
#### Scenario: Link a tag to a sale
- **WHEN** an admin sets product + order on a tag and saves
- **THEN** the tag row carries both foreign keys
### Requirement: Suspend / unsuspend a tag
The admin SHALL suspend or unsuspend a tag (existing `SetTagStatus` behaviour).
#### Scenario: Suspend a tag
- **WHEN** an admin suspends a tag
- **THEN** the tag page shows unavailable and alerts stop

View File

@@ -0,0 +1,15 @@
## ADDED Requirements
### Requirement: List and view users
The admin SHALL list owner accounts (searchable) and view a user's details: contact info, tags owned, recent scans.
#### Scenario: Find a user
- **WHEN** an admin searches by email/name
- **THEN** matching accounts are listed
### Requirement: Suspend a user
The admin SHALL be able to suspend a user, which prevents new alert SMS for their tags (via existing tag status).
#### Scenario: Suspend then re-enable
- **WHEN** an admin suspends a user and later re-enables them
- **THEN** the state flips accordingly and tag alerts reflect it

View File

@@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: users.is_admin column
The `users` table SHALL include an `is_admin` boolean column, NOT NULL default FALSE, added idempotently.
#### Scenario: Fresh database
- **WHEN** `db/schema.sql` is applied to an empty database
- **THEN** `users.is_admin` exists defaulting to FALSE
#### Scenario: Existing database
- **WHEN** `db/schema.sql` is re-applied to an existing database
- **THEN** the column is added without error

View File

@@ -0,0 +1,24 @@
## 1. Schema & Scaffold
- [ ] 1.1 `db/schema.sql`: idempotent `ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin BOOLEAN NOT NULL DEFAULT FALSE;`; `make db-up`
- [ ] 1.2 `admin/docker-compose.yml` (php:8.3-fpm + composer bootstrap, port 3030, volume mount admin/); bootstrap `laravel/laravel` via composer image; `admin/.env` → shared DB on .13:5434 (gitignored)
## 2. Laravel Core
- [ ] 2.1 Eloquent models: User (is_admin), Tag, Scan, Product, Order with relationships (Tag belongsTo Product/Order; User hasMany Tag; Tag hasMany Scan)
- [ ] 2.2 Filament install (v3) + admin panel provider; admin auth gate (is_admin); seed admin user (env email/password)
## 3. Filament Resources
- [ ] 3.1 UserResource (list/search, view with tags+scans, suspend)
- [ ] 3.2 TagResource (list/search, view scans, product/order assignment selects, suspend/unsuspend)
- [ ] 3.3 ProductResource (CRUD sku/name/item_type)
- [ ] 3.4 OrderResource (CRUD, status lifecycle)
- [ ] 3.5 Dashboard widgets (tags by status, scans 24h, recent scans, lapsed orders)
## 4. Verification
- [ ] 4.1 Admin login works (is_admin user); owner (non-admin) blocked
- [ ] 4.2 CRUD smoke: create product, create order, assign product+order to a tag, suspend a tag, update order status
- [ ] 4.3 GOAT frontend unaffected (suites 53/53 on shared DB)
- [ ] 4.4 `openspec validate laravel-admin`; commit