Compare commits

...

2 Commits

Author SHA1 Message Date
db5dbd8b31 nutrition for platform 2026-09-30 13:43:17 +05:30
dd718d4e81 health score: move the non-food early return past the hook
`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>
2026-09-30 12:23:50 +05:30
2 changed files with 40 additions and 4 deletions

View File

@@ -26,7 +26,29 @@
* correct; the data has to catch up.
*/
const NUTRITION_BASE = 'https://mcp.nearle.ai.in/api';
/*
* Through this console's OWN origin, not the service's.
*
* `/ingest` is proxied to mcp.nearle.ai.in by nginx in production and by vite in
* development, so this is same-origin either way and CORS does not enter into
* it.
*
* It used to be the absolute `https://mcp.nearle.ai.in/api`, and that works only
* where the service's allowlist names the calling origin. Measured 30 Sep 2026:
* a request carrying `Origin: https://app.nearledaily.com` comes back with
* `Access-Control-Allow-Origin` and one carrying
* `Origin: https://platform.nearledaily.com` comes back without it. So the
* browser dropped every response here, `forProduct` returned null, and the panel
* showed "No health score available for this product yet" for every product —
* the same product that renders fine in the merchant console.
*
* `vite.config.ts` called this exactly: "it works on every developer machine and
* fails the moment it is deployed, which is the worst order to find out."
*
* The proxy also attaches the ingest API key, which this endpoint does not need
* and does not mind.
*/
const NUTRITION_BASE = '/ingest/api';
/** How confident the service is that it matched the right source record. */
export const LOW_CONFIDENCE = 0.7;

View File

@@ -56,14 +56,25 @@ export function HealthScorePanel({
populated for both, so nothing in the response separates them. Our own
category does, so the decision is made here until they publish theirs.
Before the network call, deliberately: there is nothing to ask about.
Still no network call for one — see `enabled` below — but the EARLY RETURN
that used to sit here has moved past the hook.
It read `if (!isEdible(category)) return null` 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 React
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
being switched off.
*/
if (!isEdible(category)) return null;
const isFood = isEdible(category);
const query = useQuery({
queryKey: ['nutrition', brand, imageId],
queryFn: () => nutritionApi.forProduct(brand, imageId),
enabled: Boolean(brand && imageId),
// Non-food asks nothing, which is what the early return was protecting.
enabled: Boolean(brand && imageId && isFood),
// Nutrition for a packaged product does not change during a trading day.
staleTime: 60 * 60_000,
refetchOnWindowFocus: false,
@@ -72,6 +83,9 @@ export function HealthScorePanel({
retry: false,
});
// Past the hook, so the count is the same on every render.
if (!isFood) return null;
/*
* No catalogue link, so there is nothing to look up.
*