An admin can add a product but not a place to put it: the four categories come from src/catalog.ts's seed and there is no route that touches them. A shop that cannot open a new shelf cannot grow.
What done looks like:
[ ] GET /admin/categories lists every category with its slug, its blurb and how many active products it holds; POST /admin/categories makes one; POST /admin/categories/:id renames it, edits its blurb and moves its place in the nav.
[ ] Validation at the boundary: a slug is [a-z0-9-]{2,40}, unique, and never changed once products point at it (the URL is shared); a name is 1..60 characters, escaped everywhere it is shown.
[ ] A category with products cannot be deleted; it can be emptied, and an empty category is not shown in the nav or on the front page. The product form's category select offers every category, empty or not.
[ ] Every change writes an admin_change row through src/changes.ts naming who made it, and emits on the live bus so an open /admin follows it like the rest.
[ ] The terminal keeps up: the stock tab already reads the catalog, so src/tty.ts gains the same list and the same rename under an admin's key, refused for anyone else.
[ ] Tests: a created category appears in the nav only once it has an active product; a duplicate slug is refused; a rename leaves the slug alone and writes one change row; a non-admin gets nothing from every new route.
Files I expect to change: src/routes.ts, src/admin.ts, src/catalog.ts, src/pages/admin.ts, src/changes.ts, src/desk.ts, src/tty.ts, src/server.test.ts, src/units.test.ts.
Risks: product.category_id is a foreign key and the nav is rendered on every page — a category query per request must stay off the hot path (it is four rows; keep the one query the layout already makes). Hiding empty categories changes the front page, so the seed's four must still all show.
Suggested: mason, with sentinel reading the pull for the new admin boundary.
An admin can add a product but not a place to put it: the four categories come from
src/catalog.ts's seed and there is no route that touches them. A shop that cannot open a new shelf cannot grow.What done looks like:
GET /admin/categorieslists every category with its slug, its blurb and how many active products it holds;POST /admin/categoriesmakes one;POST /admin/categories/:idrenames it, edits its blurb and moves its place in the nav.[a-z0-9-]{2,40}, unique, and never changed once products point at it (the URL is shared); a name is 1..60 characters, escaped everywhere it is shown.admin_changerow throughsrc/changes.tsnaming who made it, and emits on the live bus so an open/adminfollows it like the rest.src/tty.tsgains the same list and the same rename under an admin's key, refused for anyone else.Files I expect to change:
src/routes.ts,src/admin.ts,src/catalog.ts,src/pages/admin.ts,src/changes.ts,src/desk.ts,src/tty.ts,src/server.test.ts,src/units.test.ts.Risks:
product.category_idis a foreign key and the nav is rendered on every page — a category query per request must stay off the hot path (it is four rows; keep the one query the layout already makes). Hiding empty categories changes the front page, so the seed's four must still all show.Suggested: mason, with sentinel reading the pull for the new admin boundary.