Categories and tags #23

Closed
opened 2026-08-16 15:56:33 -05:00 by bermudalamb · 6 comments
Owner

Add support for categories and tags for each item.

Each item should now support to new elements.

  1. Category -> Rules that dictate how the app automatically organizes items into a folder like architecture. This is not a physical storage architecture. It only metadata for organizing the items into a tree-like structure.
  2. Tags -> flexible labels used to categorize, color-code, and filter documents. Unlike rigid categories, a single item can have multiple tags, allowing for multi-dimensional organization without duplicating files. Tags should be a multi-select item for each item, with the ability to add tags on the fly.

The main UI should provide filter options for both of these new elements, and another new filter option should be added for price range.

# Add support for categories and tags for each item. Each item should now support to new elements. 1. Category -> Rules that dictate how the app automatically organizes items into a folder like architecture. This is not a physical storage architecture. It only metadata for organizing the items into a tree-like structure. 2. Tags -> flexible labels used to categorize, color-code, and filter documents. Unlike rigid categories, a single item can have multiple tags, allowing for multi-dimensional organization without duplicating files. Tags should be a multi-select item for each item, with the ability to add tags on the fly. --- The main UI should provide filter options for both of these new elements, and another new filter option should be added for price range.
bermudalamb added this to the Initial Build project 2026-08-17 08:38:46 -05:00
bermudalamb self-assigned this 2026-08-17 08:38:54 -05:00
bermudalamb moved this to In Progress in Initial Build on 2026-08-17 08:39:27 -05:00
Author
Owner

Design clarifications (round 1)

Working through this issue's requirements before implementation. Q&A recorded here for traceability.


Q1 — Categories: what does "rules that dictate how the app automatically organizes items" mean in practice?

The issue's wording could describe two very different systems, so this needed pinning down:

Option Meaning
A. Manual tree assignment Admin defines a hierarchical category tree. Each item is assigned to a node in the admin item form. The tree is the organization — no rule engine.
B. Rule engine, auto-assign Admin defines predicates (name contains 'table' AND price > $200 → Furniture/Tables); the app evaluates them and files items automatically.
C. Manual tree now, rules later Ship A, design the schema so a rule engine can be layered on without migration pain.

Answer: A — Manual tree assignment.

Matches the issue's own clarification that categories are "only metadata for organizing the items into a tree-like structure." No rule engine will be built.


Q2 — Category tree: depth, and how many categories per item?

Option Meaning
A. Arbitrary depth, one per item Self-referencing parent_id; nest to any depth. Each item assigned to exactly one node. Uncategorized items permitted.
B. Exactly two levels Fixed parent > child only. Simpler, but a third level needs a schema/UI change.
C. Arbitrary depth, many per item Many-to-many join. More flexible, but blurs the line with Tags.

Answer: A — Arbitrary depth, one item per category.

categories(id, name, parent_id -> categories.id, sort_order)
items.category_id -> categories.id   NULL allowed (Uncategorized)

Filtering on a parent node will include all descendants.

Option C was explicitly rejected because the issue contrasts "rigid categories" against multi-tag flexibility — keeping categories single-valued preserves that distinction.


Q3 — Tags: how should creation and colors work?

Option Meaning
A. Admin-only, auto color Tags typed inline in the item form's multi-select, created on the fly. Color auto-assigned deterministically from the tag name.
B. Admin-only, manual color Same, but the admin explicitly picks each color.
C. No tag registry Free text on the item, no central table. No rename/recolor/delete; typos become permanent duplicates.

Answer: A, plus B's override — auto color on creation, overridable from a palette.

A new tag gets a deterministic color derived from its name (stable and distinct with zero extra steps), and the admin can override that color from a palette in a Tags admin tab. That tab also handles rename and delete. Option C was rejected outright since the issue explicitly asks for color-coding.


Still open

Storefront filter layout (category / tags / price range) — being reviewed as side-by-side mockups. Outcome will be posted here.

## Design clarifications (round 1) Working through this issue's requirements before implementation. Q&A recorded here for traceability. --- ### Q1 — Categories: what does "rules that dictate how the app automatically organizes items" mean in practice? The issue's wording could describe two very different systems, so this needed pinning down: | Option | Meaning | | --- | --- | | **A. Manual tree assignment** | Admin defines a hierarchical category tree. Each item is assigned to a node in the admin item form. The tree *is* the organization — no rule engine. | | B. Rule engine, auto-assign | Admin defines predicates (`name contains 'table' AND price > $200 → Furniture/Tables`); the app evaluates them and files items automatically. | | C. Manual tree now, rules later | Ship A, design the schema so a rule engine can be layered on without migration pain. | **Answer: A — Manual tree assignment.** Matches the issue's own clarification that categories are "only metadata for organizing the items into a tree-like structure." No rule engine will be built. --- ### Q2 — Category tree: depth, and how many categories per item? | Option | Meaning | | --- | --- | | **A. Arbitrary depth, one per item** | Self-referencing `parent_id`; nest to any depth. Each item assigned to exactly one node. Uncategorized items permitted. | | B. Exactly two levels | Fixed parent > child only. Simpler, but a third level needs a schema/UI change. | | C. Arbitrary depth, many per item | Many-to-many join. More flexible, but blurs the line with Tags. | **Answer: A — Arbitrary depth, one item per category.** ``` categories(id, name, parent_id -> categories.id, sort_order) items.category_id -> categories.id NULL allowed (Uncategorized) ``` Filtering on a parent node will include all descendants. Option C was explicitly rejected because the issue contrasts "rigid categories" against multi-tag flexibility — keeping categories single-valued preserves that distinction. --- ### Q3 — Tags: how should creation and colors work? | Option | Meaning | | --- | --- | | **A. Admin-only, auto color** | Tags typed inline in the item form's multi-select, created on the fly. Color auto-assigned deterministically from the tag name. | | B. Admin-only, manual color | Same, but the admin explicitly picks each color. | | C. No tag registry | Free text on the item, no central table. No rename/recolor/delete; typos become permanent duplicates. | **Answer: A, plus B's override — auto color on creation, overridable from a palette.** A new tag gets a deterministic color derived from its name (stable and distinct with zero extra steps), and the admin can override that color from a palette in a Tags admin tab. That tab also handles rename and delete. Option C was rejected outright since the issue explicitly asks for color-coding. --- ### Still open Storefront filter layout (category / tags / price range) — being reviewed as side-by-side mockups. Outcome will be posted here.
Author
Owner

Design clarifications (round 2)

Q4 — When several tags are selected at once, how do they combine?

Option Meaning
A. AND — has all of these tags An item must carry every selected tag to appear. Selecting more tags narrows the result set.
B. OR — has any of these tags An item matching any selected tag appears. Selecting more tags widens the result set.

Answer: A — AND, "has all of these tags".

Selecting vintage + oak returns only items carrying both. The filter panel will label this explicitly ("Tags — must have all") so that selecting a second tag and watching the grid shrink reads as intentional rather than broken.

Category, tags, and price range also combine with AND across the three filters.


Q5 — Storefront filter layout

Three layouts were mocked up side by side over a realistic item grid:

Option Shape Trade-off
A. Sidebar filter rail Tree, tag pills, and price slider permanently in a left column Most discoverable, but costs ~25% of grid width (4 cards/row → 3) and needs a separate mobile treatment
B. Horizontal filter bar Three compact dropdowns in a row above the grid Full grid width, wraps on mobile, but the category tree is hidden until the cascader is opened
C. Drawer + active-filter chips A "Filters" button opens a slide-over panel; applied filters shown as removable chips above the grid Full grid width and identical behaviour at every screen size; filtering is a two-step interaction

Answer: C — Drawer with active-filter chips.

Mobile rendering is under review before this is locked; the sub-choice is whether the panel arrives as a full-height side drawer or a bottom sheet. Both are a single antd Drawer differing only in placement, so no second implementation is involved either way. Outcome will be posted here.


Settled so far

  • Categories: admin-managed tree, arbitrary depth, one category per item, NULL allowed
  • Tags: central registry, many per item, auto-assigned colour overridable from a palette
  • Multi-tag filtering: AND
  • Storefront filters: category + tags + price range, combined with AND, applied server-side
  • Layout: drawer + chips (mobile drawer style pending)
## Design clarifications (round 2) ### Q4 — When several tags are selected at once, how do they combine? | Option | Meaning | | --- | --- | | **A. AND — has all of these tags** | An item must carry *every* selected tag to appear. Selecting more tags narrows the result set. | | B. OR — has any of these tags | An item matching *any* selected tag appears. Selecting more tags widens the result set. | **Answer: A — AND, "has all of these tags".** Selecting `vintage` + `oak` returns only items carrying both. The filter panel will label this explicitly ("Tags — must have all") so that selecting a second tag and watching the grid shrink reads as intentional rather than broken. Category, tags, and price range also combine with AND across the three filters. --- ### Q5 — Storefront filter layout Three layouts were mocked up side by side over a realistic item grid: | Option | Shape | Trade-off | | --- | --- | --- | | A. Sidebar filter rail | Tree, tag pills, and price slider permanently in a left column | Most discoverable, but costs ~25% of grid width (4 cards/row → 3) and needs a separate mobile treatment | | B. Horizontal filter bar | Three compact dropdowns in a row above the grid | Full grid width, wraps on mobile, but the category tree is hidden until the cascader is opened | | **C. Drawer + active-filter chips** | A "Filters" button opens a slide-over panel; applied filters shown as removable chips above the grid | Full grid width and identical behaviour at every screen size; filtering is a two-step interaction | **Answer: C — Drawer with active-filter chips.** Mobile rendering is under review before this is locked; the sub-choice is whether the panel arrives as a full-height side drawer or a bottom sheet. Both are a single antd `Drawer` differing only in `placement`, so no second implementation is involved either way. Outcome will be posted here. --- ### Settled so far - Categories: admin-managed tree, arbitrary depth, one category per item, `NULL` allowed - Tags: central registry, many per item, auto-assigned colour overridable from a palette - Multi-tag filtering: AND - Storefront filters: category + tags + price range, combined with AND, applied server-side - Layout: drawer + chips (mobile drawer style pending)
Author
Owner

Design clarifications (round 3)

Q6 — On mobile, how should the filter panel arrive?

Follow-up to Q5. Both options are the same antd Drawer with a different placement, so neither adds a second implementation.

Option Shape Trade-off
C1. Side drawer Slides in from the right at near full-screen, sticky footer with "Clear all" / "Show N items" Reads as a dedicated page of filters; the category tree gets full vertical room for deep nesting
C2. Bottom sheet Rises from the bottom at ~78% height with a grab handle Controls sit in thumb reach and the grid stays partly visible, but a deep tree gets cramped

Answer: C1 — side drawer.


Q5 outcome — closed

Storefront filter layout is now fully settled: drawer + active-filter chips, with the drawer entering from the right at every screen size. The closed state is a "Filters (N)" button with applied filters rendered as removable chips beside it on desktop, wrapping to their own line on mobile.

The drawer's tag section is labelled "Tags — must have all" so the AND semantics from Q4 are visible at the point of use.


Requirements now fully settled

Area Decision
Categories Admin-managed tree, arbitrary depth via self-referencing parent_id, exactly one category per item, NULL allowed (Uncategorized)
Category filtering Selecting a node matches that node and all its descendants
Tags Central registry, many per item, created on the fly from the item form
Tag colours Auto-assigned deterministically from the tag name, overridable from a palette in an admin Tags tab
Tag filtering AND — an item must carry every selected tag
Storefront filters Category + tags + price range, combined with AND, applied server-side
Layout Drawer + removable chips; drawer enters from the right at all screen sizes

Next step is the written design spec, which will be committed to the repo and linked here before any implementation starts.

## Design clarifications (round 3) ### Q6 — On mobile, how should the filter panel arrive? Follow-up to Q5. Both options are the same antd `Drawer` with a different `placement`, so neither adds a second implementation. | Option | Shape | Trade-off | | --- | --- | --- | | **C1. Side drawer** | Slides in from the right at near full-screen, sticky footer with "Clear all" / "Show N items" | Reads as a dedicated page of filters; the category tree gets full vertical room for deep nesting | | C2. Bottom sheet | Rises from the bottom at ~78% height with a grab handle | Controls sit in thumb reach and the grid stays partly visible, but a deep tree gets cramped | **Answer: C1 — side drawer.** --- ### Q5 outcome — closed Storefront filter layout is now fully settled: **drawer + active-filter chips**, with the drawer entering from the right at every screen size. The closed state is a "Filters (N)" button with applied filters rendered as removable chips beside it on desktop, wrapping to their own line on mobile. The drawer's tag section is labelled "Tags — must have all" so the AND semantics from Q4 are visible at the point of use. --- ### Requirements now fully settled | Area | Decision | | --- | --- | | Categories | Admin-managed tree, arbitrary depth via self-referencing `parent_id`, exactly one category per item, `NULL` allowed (Uncategorized) | | Category filtering | Selecting a node matches that node and all its descendants | | Tags | Central registry, many per item, created on the fly from the item form | | Tag colours | Auto-assigned deterministically from the tag name, overridable from a palette in an admin Tags tab | | Tag filtering | AND — an item must carry every selected tag | | Storefront filters | Category + tags + price range, combined with AND, applied server-side | | Layout | Drawer + removable chips; drawer enters from the right at all screen sizes | Next step is the written design spec, which will be committed to the repo and linked here before any implementation starts.
Author
Owner

Implemented

Branch: feature/categories-and-tags — three commits, not yet pushed.

Design spec committed to docs/superpowers/specs/2026-08-17-categories-and-tags-design.md.

Schema

New migration 1786974336000_add-categories-and-tags, with a real down (unlike the baseline):

categories(id, name, parent_id -> categories ON DELETE CASCADE, sort_order)
tags(id, name, color)
item_tags(item_id, tag_id)  PK both
items.category_id -> categories ON DELETE SET NULL

Two partial unique indexes stop siblings sharing a name — a single plain constraint would let duplicate root categories through, since NULL parent_id compares unequal to every other NULL.

Deleting a category cascades to its subcategories and leaves their items in place, merely uncategorized.

API

GET /api/items accepts category, tags, min_price, max_price, combined with AND.

  • Category matching walks the subtree with a recursive CTE, so selecting a parent includes everything filed beneath it. Chosen over a materialized path column because reparenting stays a single UPDATE with no stored paths to rewrite and drift.
  • Tag matching uses a count check rather than ANY() alone, which would have returned items carrying only one of the selected tags.
  • Malformed params return 400 instead of being ignored, so a broken filter link shows itself rather than quietly listing the whole catalogue.

GET /api/filters serves the drawer its tree, tags, and price bounds in one request. Admin CRUD lives under /api/admin/categories and /api/admin/tags, inheriting the existing authentik boundary with no nginx change.

Fix required by the feature

SELECT_WITH_IMAGES aggregated images through LEFT JOIN + GROUP BY. Adding a second one-to-many join for tags to that shape multiplies rows together — an item with 2 images and 3 tags would have repeated every image three times. Both copies now use scalar subqueries, which drops the GROUP BY entirely. There is a regression test for exactly this case.

UI

Storefront: a "Filters (N)" button opens a right-hand drawer holding the category tree, colour-coded tag pills labelled "Tags — must have all", and a price range. Applied filters appear as removable chips. Filter state lives in the URL query string, so filtered views are shareable and the back button works. Item cards show their category and tags.

Admin: new Categories and Tags tabs; the item form gains a category TreeSelect and a tags input that creates new tags inline.

Bug caught during verification

The admin category tree used defaultExpandAll, which antd evaluates once at mount. Any branch created afterwards rendered collapsed, making its children unreachable — you could add a subcategory and it would vanish. Expansion is now controlled state, and creating or moving a node expands its parent. The storefront drawer was unaffected because it remounts each time it opens. Covered by a regression test in both specs.

Verification

Suite Result
Backend unit 43 passed (3 suites)
Backend integration 55 passed (3 suites)
Frontend e2e 21 passed, stable over 4 consecutive runs
tsc --noEmit backend / frontend clean
npm run build clean

41 of the integration tests are new, covering tree CRUD, sibling name collisions, cycle rejection on reparent, cascade-delete fallout, descendant matching, tag AND semantics, price bounds, the filters combined, and on-the-fly tag creation.

Deploy note

This release needs a migration against the live database:

sudo docker exec redefined-designs-syn node migrate.js up
## Implemented Branch: `feature/categories-and-tags` — three commits, not yet pushed. Design spec committed to `docs/superpowers/specs/2026-08-17-categories-and-tags-design.md`. ### Schema New migration `1786974336000_add-categories-and-tags`, with a real `down` (unlike the baseline): ``` categories(id, name, parent_id -> categories ON DELETE CASCADE, sort_order) tags(id, name, color) item_tags(item_id, tag_id) PK both items.category_id -> categories ON DELETE SET NULL ``` Two partial unique indexes stop siblings sharing a name — a single plain constraint would let duplicate root categories through, since `NULL` parent_id compares unequal to every other `NULL`. Deleting a category cascades to its subcategories and leaves their items in place, merely uncategorized. ### API `GET /api/items` accepts `category`, `tags`, `min_price`, `max_price`, combined with AND. - Category matching walks the subtree with a recursive CTE, so selecting a parent includes everything filed beneath it. Chosen over a materialized path column because reparenting stays a single `UPDATE` with no stored paths to rewrite and drift. - Tag matching uses a count check rather than `ANY()` alone, which would have returned items carrying only one of the selected tags. - Malformed params return `400` instead of being ignored, so a broken filter link shows itself rather than quietly listing the whole catalogue. `GET /api/filters` serves the drawer its tree, tags, and price bounds in one request. Admin CRUD lives under `/api/admin/categories` and `/api/admin/tags`, inheriting the existing authentik boundary with no nginx change. ### Fix required by the feature `SELECT_WITH_IMAGES` aggregated images through `LEFT JOIN` + `GROUP BY`. Adding a second one-to-many join for tags to that shape multiplies rows together — an item with 2 images and 3 tags would have repeated every image three times. Both copies now use scalar subqueries, which drops the `GROUP BY` entirely. There is a regression test for exactly this case. ### UI Storefront: a "Filters (N)" button opens a right-hand drawer holding the category tree, colour-coded tag pills labelled "Tags — must have all", and a price range. Applied filters appear as removable chips. Filter state lives in the URL query string, so filtered views are shareable and the back button works. Item cards show their category and tags. Admin: new Categories and Tags tabs; the item form gains a category `TreeSelect` and a tags input that creates new tags inline. ### Bug caught during verification The admin category tree used `defaultExpandAll`, which antd evaluates once at mount. Any branch created afterwards rendered collapsed, making its children unreachable — you could add a subcategory and it would vanish. Expansion is now controlled state, and creating or moving a node expands its parent. The storefront drawer was unaffected because it remounts each time it opens. Covered by a regression test in both specs. ### Verification | Suite | Result | | --- | --- | | Backend unit | 43 passed (3 suites) | | Backend integration | 55 passed (3 suites) | | Frontend e2e | 21 passed, stable over 4 consecutive runs | | `tsc --noEmit` backend / frontend | clean | | `npm run build` | clean | 41 of the integration tests are new, covering tree CRUD, sibling name collisions, cycle rejection on reparent, cascade-delete fallout, descendant matching, tag AND semantics, price bounds, the filters combined, and on-the-fly tag creation. ### Deploy note This release needs a migration against the live database: ```bash sudo docker exec redefined-designs-syn node migrate.js up ```
bermudalamb moved this to Ready for Review in Initial Build on 2026-08-17 09:49:57 -05:00
bermudalamb moved this to Review in Initial Build on 2026-08-17 10:43:06 -05:00
Author
Owner

Post-deploy findings and corrections

Three defects surfaced when this was deployed and tested. Branch fix/deploy-migration-safety, two commits, not yet pushed.


1. The storefront showed no inventory after deploy

No data was lost. The migration only runs CREATE TABLE IF NOT EXISTS and ALTER TABLE items ADD COLUMN IF NOT EXISTS — nothing in it removes rows.

The cause was code deployed ahead of its schema. The new item query references categories, item_tags, and items.category_id; against a database where the migration hadn't run, it failed. That failure was then invisible at every layer:

Layer Why it stayed silent
Express 4 Does not forward a rejected async handler, and no error middleware was mounted — so the request never responded at all
Frontend No .catch, and Empty rendered "No items yet — check back soon" — a hung request was indistinguishable from an empty catalogue
Deploy node migrate.js up was a separate manual step, easy to forget

To confirm on the NAS, if it hasn't been already:

sudo docker exec -it redefined-designs-db-syn psql -U redefined -d redefined -c "SELECT count(*) FROM items;"

Fixes — one per layer:

  • Migrations run at container start. CMD ["sh", "-c", "node migrate.js up && node dist/server.js"]. Code can no longer be ahead of the schema, and the manual step is gone from the deploy sequence. migrate.js now waits for Postgres to accept connections first (the NAS brings the DB container up slower than the app) and still exits non-zero, so a bad migration stops the container instead of serving against a half-migrated schema.
  • Async routes are wrapped and an error middleware is mounted, so a failing query returns 500 instead of hanging. Regression test asserts a query error answers rather than hangs — verified by removing the wrapper and watching the request hang.
  • The storefront separates "request failed" from "no items", with a retry. fetchItems/fetchFilterOptions now throw on a non-OK response instead of returning the parsed error body, which would otherwise have been set as the item list and crashed the grid on .map.

The general lesson, recorded in .claude/project-context.md: any deploy where new code needs something the environment doesn't have yet surfaces as silence, not an error, unless something is written to make it loud.


2. "Item added" was shown for a save that never happened

Confirmed and fixed. saveItem returned res.json() with no res.ok check, so a 4xx/5xx resolved normally and the caller reported success unconditionally. The same held for delete, image removal, and mark sold/available — the last two gave no feedback at all on failure.

All admin calls now throw on a non-OK response. The item form reports the error, keeps the form open so entered values aren't lost, and only claims success once the server has accepted the write. Covered by e2e tests that intercept the request with a 500 and assert the success toast never appears, plus one that confirms a successful save actually reaches the database rather than just firing a toast.

This was almost certainly the same root cause as #1 — the save was failing server-side against the un-migrated schema — but the false success claim was a genuine defect in its own right and is fixed independently.


3. Categories could not be created inline from the item form

Correct, and a real gap: tags could be invented from the item form but categories could not, so needing a new category meant abandoning a half-filled form for the Categories tab.

The category picker now has a "New category name" field. New categories are created at the top level; nesting stays in the Categories tab, which keeps the control to a single decision.

Two related problems found while building it:

  • The picker expanded the entire tree on open, which doesn't scale past a screenful and made the popup reflow while measuring. It now has search instead.
  • Typing in the new-category field re-rendered the whole Inventory component and rebuilt the category tree on every keystroke, visibly jittering the open popup. The control is now its own component with memoized tree data — the interaction went from ~35s to ~1.5s in tests.

The app also now honours prefers-reduced-motion by disabling antd transitions, which is an accessibility improvement and removes a class of animation-related test flake.


Verification

Suite Result
Backend unit 47 passed
Backend integration 57 passed
Frontend e2e 29 passed, stable over 3 consecutive runs
tsc --noEmit backend / frontend clean
npm run build clean

Note on PR #24

The docs: bring project context up to date commit was not included in the merge — it ended up dangling, reachable only through the reflog. git log on the branch looked normal because the commit simply wasn't on it. It has been recovered and is included in this branch. Worth running git branch --contains <sha> on any commit made after a push, before assuming it shipped.


Still open

A separate on-demand QA environment stack for reviewing changes online, requested during this round. Being scoped separately — it's an infrastructure change rather than part of this issue.

## Post-deploy findings and corrections Three defects surfaced when this was deployed and tested. Branch `fix/deploy-migration-safety`, two commits, not yet pushed. --- ### 1. The storefront showed no inventory after deploy **No data was lost.** The migration only runs `CREATE TABLE IF NOT EXISTS` and `ALTER TABLE items ADD COLUMN IF NOT EXISTS` — nothing in it removes rows. The cause was code deployed ahead of its schema. The new item query references `categories`, `item_tags`, and `items.category_id`; against a database where the migration hadn't run, it failed. That failure was then invisible at every layer: | Layer | Why it stayed silent | | --- | --- | | Express 4 | Does not forward a rejected async handler, and no error middleware was mounted — so the request **never responded at all** | | Frontend | No `.catch`, and `Empty` rendered "No items yet — check back soon" — a hung request was indistinguishable from an empty catalogue | | Deploy | `node migrate.js up` was a separate manual step, easy to forget | To confirm on the NAS, if it hasn't been already: ```bash sudo docker exec -it redefined-designs-db-syn psql -U redefined -d redefined -c "SELECT count(*) FROM items;" ``` **Fixes — one per layer:** - **Migrations run at container start.** `CMD ["sh", "-c", "node migrate.js up && node dist/server.js"]`. Code can no longer be ahead of the schema, and the manual step is gone from the deploy sequence. `migrate.js` now waits for Postgres to accept connections first (the NAS brings the DB container up slower than the app) and still exits non-zero, so a bad migration stops the container instead of serving against a half-migrated schema. - **Async routes are wrapped and an error middleware is mounted**, so a failing query returns 500 instead of hanging. Regression test asserts a query error answers rather than hangs — verified by removing the wrapper and watching the request hang. - **The storefront separates "request failed" from "no items"**, with a retry. `fetchItems`/`fetchFilterOptions` now throw on a non-OK response instead of returning the parsed error body, which would otherwise have been set as the item list and crashed the grid on `.map`. The general lesson, recorded in `.claude/project-context.md`: **any deploy where new code needs something the environment doesn't have yet surfaces as silence, not an error, unless something is written to make it loud.** --- ### 2. "Item added" was shown for a save that never happened Confirmed and fixed. `saveItem` returned `res.json()` with no `res.ok` check, so a 4xx/5xx resolved normally and the caller reported success unconditionally. The same held for delete, image removal, and mark sold/available — the last two gave no feedback at all on failure. All admin calls now throw on a non-OK response. The item form reports the error, **keeps the form open so entered values aren't lost**, and only claims success once the server has accepted the write. Covered by e2e tests that intercept the request with a 500 and assert the success toast never appears, plus one that confirms a successful save actually reaches the database rather than just firing a toast. This was almost certainly the same root cause as #1 — the save was failing server-side against the un-migrated schema — but the false success claim was a genuine defect in its own right and is fixed independently. --- ### 3. Categories could not be created inline from the item form Correct, and a real gap: tags could be invented from the item form but categories could not, so needing a new category meant abandoning a half-filled form for the Categories tab. The category picker now has a "New category name" field. New categories are created at the **top level**; nesting stays in the Categories tab, which keeps the control to a single decision. Two related problems found while building it: - The picker expanded the entire tree on open, which doesn't scale past a screenful and made the popup reflow while measuring. It now has search instead. - Typing in the new-category field re-rendered the whole Inventory component and rebuilt the category tree on every keystroke, visibly jittering the open popup. The control is now its own component with memoized tree data — the interaction went from ~35s to ~1.5s in tests. The app also now honours `prefers-reduced-motion` by disabling antd transitions, which is an accessibility improvement and removes a class of animation-related test flake. --- ### Verification | Suite | Result | | --- | --- | | Backend unit | 47 passed | | Backend integration | 57 passed | | Frontend e2e | 29 passed, stable over 3 consecutive runs | | `tsc --noEmit` backend / frontend | clean | | `npm run build` | clean | --- ### Note on PR #24 The `docs: bring project context up to date` commit was **not included in the merge** — it ended up dangling, reachable only through the reflog. `git log` on the branch looked normal because the commit simply wasn't on it. It has been recovered and is included in this branch. Worth running `git branch --contains <sha>` on any commit made after a push, before assuming it shipped. --- ### Still open A separate on-demand QA environment stack for reviewing changes online, requested during this round. Being scoped separately — it's an infrastructure change rather than part of this issue.
Author
Owner

Design mockups archived

Flagged in review: the wireframes behind the layout decisions were never committed.

Correct — they lived only in .superpowers/brainstorm/, which I had gitignored. That call was half wrong. The directory itself should stay ignored, because alongside the mockups it holds a brainstorming-session token, PID files, and absolute local paths:

.superpowers/brainstorm/.last-token              <- session key
.superpowers/brainstorm/<id>/state/server-info   <- same key, plus local paths
.superpowers/brainstorm/<id>/state/server.pid

What was wrong was letting the mockups die with it. They are now archived at docs/superpowers/specs/2026-08-17-categories-and-tags-mockups/, next to the design spec and linked from it:

File Question Outcome
filter-layout.html Where do the storefront filters live? C — drawer plus chips. A was a permanent sidebar rail; B a row of dropdowns
filter-layout-mobile.html How does the drawer arrive on a phone? C1 — side drawer. C2 was a bottom sheet

The rejected options are kept deliberately — the comparison is the part worth preserving, and it's what makes the recorded decision meaningful later.

The tool serves these as fragments inside its own frame, so committing them raw would have produced files that render as unstyled markup. Each is wrapped as a standalone page with the frame's style tokens and its toggleSelect helper inlined; they open directly in a browser with no server and no network access. Verified rendering with no console errors.

Convention recorded in .claude/project-context.md: .superpowers/ stays ignored, but design artifacts inside it get lifted into the specs directory before the session's scratch state is cleaned up.

## Design mockups archived Flagged in review: the wireframes behind the layout decisions were never committed. Correct — they lived only in `.superpowers/brainstorm/`, which I had gitignored. That call was half wrong. The directory itself should stay ignored, because alongside the mockups it holds a brainstorming-session token, PID files, and absolute local paths: ``` .superpowers/brainstorm/.last-token <- session key .superpowers/brainstorm/<id>/state/server-info <- same key, plus local paths .superpowers/brainstorm/<id>/state/server.pid ``` What was wrong was letting the mockups die with it. They are now archived at `docs/superpowers/specs/2026-08-17-categories-and-tags-mockups/`, next to the design spec and linked from it: | File | Question | Outcome | | --- | --- | --- | | `filter-layout.html` | Where do the storefront filters live? | **C — drawer plus chips.** A was a permanent sidebar rail; B a row of dropdowns | | `filter-layout-mobile.html` | How does the drawer arrive on a phone? | **C1 — side drawer.** C2 was a bottom sheet | The rejected options are kept deliberately — the comparison is the part worth preserving, and it's what makes the recorded decision meaningful later. The tool serves these as fragments inside its own frame, so committing them raw would have produced files that render as unstyled markup. Each is wrapped as a standalone page with the frame's style tokens and its `toggleSelect` helper inlined; they open directly in a browser with no server and no network access. Verified rendering with no console errors. Convention recorded in `.claude/project-context.md`: `.superpowers/` stays ignored, but design artifacts inside it get lifted into the specs directory before the session's scratch state is cleaned up.
bermudalamb moved this to Send back in Initial Build on 2026-08-17 11:17:49 -05:00
bermudalamb moved this to In Progress in Initial Build on 2026-08-17 13:30:20 -05:00
bermudalamb moved this to Review in Initial Build on 2026-08-17 13:30:27 -05:00
bermudalamb moved this to Ready for Release in Initial Build on 2026-08-17 13:30:40 -05:00
bermudalamb moved this to Released in Initial Build on 2026-08-17 14:00:35 -05:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bermudalamb/redefined-designs#23