`if (!isEdible(category)) return null` sat on the line above `useQuery`, which
made the hook conditional. React counts hooks per component instance, so one
drawer reused for two products -- a soap and then a biscuit, which is ordinary
browsing in the global catalogue -- went 0 hooks then 1 and threw "rendered more
hooks than during the previous render".
That does not degrade the panel, it unmounts the tree: the health score then
disappears for EVERY product until the page is reloaded, which reads exactly
like the feature having been switched off.
The guard now sits after the hook, and `enabled` carries the intent the early
return was protecting -- a non-food product still asks the service nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panel has been complete except for the one part that answers. Its
composer was deliberately disabled — it used to accept text, light up the
send button, and swallow the submit, because no assistant endpoint
existed. This connects it to the one that does.
It is enabled only when a model is configured AND the page has an agent,
checked at runtime via /assistant/status rather than assumed at build
time. The placeholder says which of the two is missing:
"Not connected yet" no model on this deployment
"No assistant for this page yet" no agent for this route
"Ask about this page" live
Two different facts, two different sentences. A person on Inventory can
move to Sales and get an answer today; a person whose deployment has no
model can do nothing from the browser. Flattening both into "not
connected" would be true and useless.
The old rule is kept: never accept a message nothing will read.
An answer shows its working — the tools it ran, their row counts, and a
link to the page holding the same rows. Buddy states things with the
confidence of a sentence, and the only honest way to present that is
beside the evidence, so a person can disagree with it. Refused calls are
shown too: an answer that quietly dropped one would look like Buddy chose
not to look.
A partial answer says so in amber, not as a grey hint. An answer cut off
mid-sentence reads as a complete one otherwise, which is the failure the
flag exists to prevent.
The thread clears on navigation. An answer about Sales sitting above the
Inventory page reads as being about what is on screen; losing the history
is the smaller cost.
Phase 2 ships one agent, covering Console and Sales. The rest arrive as
backend config, not as changes here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`npm run build` runs `tsc --noEmit` first, so all four stopped the deploy
before vite ever ran. They came in with the redesign commit.
KpiCard: the note pill was removed from the tile but `note` was still
destructured, and `noUnusedLocals` rejects that. The prop stays declared —
87 call sites across 21 files pass it — and is now documented as accepted
and ignored, the same way `fill` already was. Those 87 strings are written
and never shown; the comment says so rather than leaving it a puzzle.
InventoryPage: dropped an unused SectionHeader import, and `colour` is not
a BadgeProps field. The intent was a brand-coloured "In transit", so that
is now `variant="purple"` — a real variant in BadgeVariantMap, and the
brand is purple.
StoreAccountPage: `current` is a BRANCH, and `gettenantlocations` sends no
tenantname on it (checked on the wire against tenant 1147). So the second
half of `shopQuery.data?.tenantname || current?.tenantname` could never
fire. Removed rather than added to the type — a field the backend does not
send is exactly the bug the previous commit fixed on the summary endpoints.
Verified: tsc clean, 625 tests pass, vite build clean, and the lock file
installs under `npm@10.9.8 ci` — the builder's npm, which the image pins.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYEsb8PNZ19G9R8gUjTU7n
Opening dispatch showed "Pick one to see its stops" over an empty panel.
That wastes the first look: the whole day IS the answer to "where is my
work", and an operator usually only wants to narrow after seeing it.
Both views now open on the map, drawn from every stop in range. Picking a
rider or a shop filters it; it no longer summons it. The customer view keeps
its placeholder — one customer's drops are all at one address, so there is
no shape to open onto.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYEsb8PNZ19G9R8gUjTU7n
The deploy died on `npm ci` with "Missing: @emnapi/core@1.11.3 from lock
file". Nothing was wrong with the code — the lock really was incomplete,
and the Dockerfile was right to refuse it.
I broke it. Adding leaflet ran `npm install` under npm 11.6.2; the builder
is node:22-alpine, which ships npm 10.9.8. npm 11 prunes optional platform
packages that npm 10 still validates, and it dropped `@emnapi/core` and
`@emnapi/runtime` — transitive optional deps of
`@tailwindcss/oxide-wasm32-wasi`. Both were present in the lock at b760c1a
and absent from 34bf798 onward.
Regenerated with `npx npm@10.9.8 install --package-lock-only`, which
restores both entries and keeps leaflet 1.9.4 / @types/leaflet 1.9.22.
Verified by running the builder's exact command in a scratch directory:
the old lock reproduces the failure, the new one gives "added 212 packages"
under npm 10.9.8 AND under npm 11.6.2 — so it holds whichever npm the image
ships. No Dockerfile change was needed for that.
The Dockerfile comment did need one. It said the fix was "`npm install`
locally and COMMIT the updated package-lock.json", which is exactly the
step that caused this. It now says to prove the lock against npm 10.9.8 in
a scratch directory before pushing, and how to regenerate it if it fails.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYEsb8PNZ19G9R8gUjTU7n