| Field |
Value |
| Date |
10 Jun 2026 |
| Status |
Complete |
| GitHub Commit |
531bcd0 |
| Deployed |
Dev + Prod (verified 409 on both) |
Added a Delete action to the Products page and fixed a pre-existing crash that
turned the page white when it was navigated to after viewing a customer.
- Every row on /products now has a Delete button (Trash2 icon) with a confirm
dialog, alongside Edit.
- ProductViewSet is a ModelViewSet, so DELETE /api/products/{id}/ already existed —
but ServiceCharge.product uses on_delete=PROTECT, so deleting a product still
assigned to a customer raised ProtectedError and returned HTTP 500.
- Overrode ProductViewSet.destroy() to catch ProtectedError and return a clean
HTTP 409 with a helpful message, e.g. Cannot delete "Australia Local Number -
Line Rental" — it is assigned to 12 customer service charges. Remove those first,
or set the product to Inactive instead.
- Products with no service charges delete normally (204). To retire an in-use
product without deleting, set it Inactive.
Symptom: clicking into /products showed a white page; a browser refresh fixed
it. Console: Uncaught TypeError: t.map is not a function.
Root cause — a react-query cache-key collision with mismatched shapes:
- Products.jsx used queryKey ['products'] with a queryFn returning r.data.results
(an array).
- CustomerDetail.jsx used the SAME key ['products'] but a queryFn returning r.data
(the paginated {count, results} object).
- After visiting a customer, the ['products'] cache held the object. Navigating to
/products served that cached object instantly (no loading state), and the shared
Table component ran data.map on a non-array → crash → blank page. A hard refresh
cleared the cache so Products' own queryFn ran first → array → worked, which is
exactly why refresh appeared to "fix" it.
Fix (three layers):
- components/Table.jsx coerces its data prop to an array — a non-array can no longer
white-screen any page that uses the shared table.
- Products.jsx derives a guaranteed array before rendering.
- CustomerDetail.jsx queryFn now returns the same array shape AND the same
?page_size=200 as Products, so the shared ['products'] cache is consistent no
matter which page loads first (also avoids a subtle list-truncation bug).
- A react-query queryKey must map to ONE data shape. If two pages share a key, their
queryFns must return the same shape and params, or whichever loads first poisons
the cache for the other. Prefer distinct keys when the queries genuinely differ.
- Deleting a product is blocked (409) while it is assigned to any ServiceCharge
(on_delete=PROTECT). Set the product Inactive to retire it instead.
- Table (components/Table.jsx) now defends against non-array data props.
- Claude Code on the dev server was migrated off the root-owned npm global install
(/usr/bin/claude, owned by root) to the user-local native build
(~/.local/bin/claude) via claude install. This fixes the recurring
"Auto-update failed: no write permission to npm prefix" message — the native
build lives in a user-writable path so auto-update works. ~/.local/bin was added
to PATH in ~/.bashrc; launch claude from a fresh terminal to use it. The old
/usr/bin/claude still exists (harmless). The current subcommand is claude install
(the older migrate-installer was removed).
Back to Sessions Index