diff --git a/openspec/changes/tag-lifecycle/.openspec.yaml b/openspec/changes/tag-lifecycle/.openspec.yaml new file mode 100644 index 0000000..d7bc011 --- /dev/null +++ b/openspec/changes/tag-lifecycle/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-10 diff --git a/openspec/changes/tag-lifecycle/README.md b/openspec/changes/tag-lifecycle/README.md new file mode 100644 index 0000000..c786ddf --- /dev/null +++ b/openspec/changes/tag-lifecycle/README.md @@ -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 diff --git a/openspec/changes/tag-lifecycle/design.md b/openspec/changes/tag-lifecycle/design.md new file mode 100644 index 0000000..5426c83 --- /dev/null +++ b/openspec/changes/tag-lifecycle/design.md @@ -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. diff --git a/openspec/changes/tag-lifecycle/proposal.md b/openspec/changes/tag-lifecycle/proposal.md new file mode 100644 index 0000000..66883fc --- /dev/null +++ b/openspec/changes/tag-lifecycle/proposal.md @@ -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). diff --git a/openspec/changes/tag-lifecycle/specs/tag-lifecycle/spec.md b/openspec/changes/tag-lifecycle/specs/tag-lifecycle/spec.md new file mode 100644 index 0000000..78808ef --- /dev/null +++ b/openspec/changes/tag-lifecycle/specs/tag-lifecycle/spec.md @@ -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` diff --git a/openspec/changes/tag-lifecycle/tasks.md b/openspec/changes/tag-lifecycle/tasks.md new file mode 100644 index 0000000..27bc49e --- /dev/null +++ b/openspec/changes/tag-lifecycle/tasks.md @@ -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