openspec: tag-lifecycle change (close/move/legacy redirect)

This commit is contained in:
2026-08-10 11:33:25 +10:00
parent daf758d336
commit 77a9f803f2
6 changed files with 124 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,3 @@
# tag-lifecycle
Owner can close a tag (status 'closed') and move tag data to a new tag (broken-tag replacement); old-URL compat redirect for physical QR tags

View File

@@ -0,0 +1,41 @@
## Context
Physical tags break; owners need to retire a tag and move its data to a replacement. Also, legacy printed URLs (`?x=5&productid=…`) must still reach tag pages so old physical tags work with the new system.
## Goals / Non-Goals
**Goals:**
- Owner-facing Close and Move actions on the account page.
- Closed tags: page shows unavailable, no alerts.
- Legacy `productid` URL redirect.
**Non-Goals:**
- Tag transfer between accounts (admin can reassign owner later) — not in scope.
- NFC/QR re-printing (future production slice).
## Decisions
1. **Status `closed`** (new enum value): schema CHECK updated via `ALTER TABLE tags DROP CONSTRAINT IF EXISTS tags_status_check` + re-add including `closed` (idempotent; CREATE TABLE also updated for fresh DBs).
2. **Alert gating**: `shouldAlert` returns false when `tag.Status == "closed"` (and "suspended" — already handled? currently suspended isn't gated in shouldAlert either — add both for safety; the public page already handles suspended).
3. **Close**: `CloseTag` (POST /account/tags/{id}/close) — owner-only (loadOwnedTag) → `SetTagStatus(id, 'closed')` → HTMX account-panel response.
4. **Move**: `MoveTag` (POST /account/tags/{id}/move, form `new_tag_code`) — load source (owner) → `GetTagByCode(target)` must exist, be unset, not same → `BindTag(owner, target)``UpdateTagDetails(target, source fields)``SetTagStatus(source, 'closed')` → panel (with error on failure).
5. **Compat redirect**: in `Home`, `productid` query param → 302 to `/t/{productid}`. (The QR likely encodes `/?x=5&productid=…`; the `x=5` param is ignored.)
6. **UI**: tag-list rows gain "Close" (confirm) and a small "Move" form (new tag code input). Public page: `closed` renders the same "unavailable" treatment as suspended, with owner-specific copy.
7. **Admin**: TagResource status Select + badge include `closed`.
## Risks / Trade-offs
- [Constraint drop/re-add could race] → dev-only, single migrator; fine.
- [Move copies photo_url] → the photo object key is preserved (MinIO) — works.
- [Old URL path weirdness (`/x=5&…` without `?`)] → if the QR literally encodes that malformed path, it 404s; the standard `?x=5&productid=` form is handled. Verify with the physical tags.
## Migration Plan
1. Schema ALTER (constraint) + `make db-up`.
2. Go handlers + redirect + templates; build.
3. Admin status option.
4. Verify (close, move, redirect, gating); regressions; deploy.
## Open Questions
- None blocking.

View File

@@ -0,0 +1,28 @@
## Why
Physical tags break (the user has a broken tag to test with). Owners need to **close** a tag (deactivate it, keeping data) and **move** a tag's details to a new tag (replacement tag) without re-entering everything. Also, the user's physical tags from the old system print URLs in the old format (`?x=5&productid=…`) — those need to redirect to the new `/t/{code}` pages so old tags work as-is.
## What Changes
- **`closed` tag status**: `tags.status` gains `closed` (constraint updated idempotently). Owner can close their own tag → public page shows "closed by its owner", data kept. Admin sees it in the status options.
- **Move**: owner enters a new tag code → the new tag (must exist in the registry, be unset) is bound and **copies all details** (item type, description, photo, phone, address, notes, sms toggle) from the old tag; the old tag is closed. Ideal for broken-tag replacement.
- **Old-URL redirect**: `/` with a `productid` query param redirects to `/t/{productid}` (legacy physical QR URLs).
## Capabilities
### New Capabilities
- `tag-lifecycle`: owner close + move actions.
### Modified Capabilities
- `database`: ADDED — `closed` in the tags status CHECK.
- `tag-management`: ADDED — close + move handlers; account UI actions.
- `public-tag-page`: ADDED — closed state renders "unavailable".
## Impact
- **Schema**: status CHECK gains `closed` (drop/re-add idempotent).
- **Go**: `CloseTag`, `MoveTag` handlers; Home compat redirect; account panel + tag-list template actions.
- **Admin**: TagResource status options + badge include `closed`.
- No changes to the alert pipeline (closed tags are gated by status via existing suspended/closed page behaviour — note: alert gating currently keys on `sms_enabled`/order; a closed tag should also stop alerts — add a closed check in shouldAlert).

View File

@@ -0,0 +1,30 @@
## ADDED Requirements
### Requirement: Close a tag
An owner SHALL be able to close one of their tags. Closing sets status `closed`: the public page shows the tag is unavailable, data is kept, and scans do not alert.
#### Scenario: Owner closes a tag
- **WHEN** the owner closes their tag
- **THEN** the tag status is `closed`, the public page shows it unavailable, and no alerts are sent
#### Scenario: Closed tag page
- **WHEN** a visitor opens a closed tag
- **THEN** they see an "unavailable / closed" message
### Requirement: Move tag data
An owner SHALL be able to move a tag's details to another tag code. The target must exist in the registry and be unset; after the move the target is active with all copied details, and the source tag is closed.
#### Scenario: Move to a replacement tag
- **WHEN** the owner moves tag A to unset tag B
- **THEN** tag B is bound with all of A's details, and tag A becomes `closed`
#### Scenario: Invalid target
- **WHEN** the target code does not exist, is already owned, or is the same tag
- **THEN** the move is rejected with an error
### Requirement: Legacy URL redirect
`GET /` with a `productid` query parameter SHALL redirect to `/t/{productid}`.
#### Scenario: Old QR URL
- **WHEN** a visitor opens `/?x=5&productid=CODE`
- **THEN** they are redirected to `/t/CODE`

View File

@@ -0,0 +1,20 @@
## 1. Schema
- [ ] 1.1 `db/schema.sql`: status CHECK gains `closed` (update CREATE + idempotent drop/re-add ALTER); `make db-up`
## 2. Go
- [ ] 2.1 `shouldAlert`: return false when status `closed` or `suspended`
- [ ] 2.2 `tags.go`: `CloseTag` + `MoveTag` handlers (owner-only, HTMX panel responses)
- [ ] 2.3 `Home` compat redirect: `productid` query → `/t/{productid}`
- [ ] 2.4 Templates: tag-list Close + Move actions; tag-public closed state; routes in main.go
## 3. Admin
- [ ] 3.1 TagResource status options + badge include `closed`
## 4. Verification
- [ ] 4.1 New suite: close → page unavailable + no alert; move → target bound w/ copied details + source closed; move errors (owned/unset-missing/same); `/?x=5&productid=CODE` → 302 `/t/CODE`
- [ ] 4.2 Regressions (72) green; deploy
- [ ] 4.3 `openspec validate tag-lifecycle`; commit