Skip to content

A menu can be filled in without three hundred presses - #745

Merged
sridharkalaibala merged 4 commits into
developfrom
feat/fill-in-a-menu-without-three-hundred-presses
Sep 15, 2026
Merged

sridharkalaibala merged 4 commits into
developfrom
feat/fill-in-a-menu-without-three-hundred-presses

Conversation

@sridharkalaibala

Copy link
Copy Markdown
Contributor

Nutrition shipped last night and is invisible on every shop in the estate, because the way to fill it in is eight numbers per dish and the assistant does one dish per press. Azure Costal Kitchen has 272 dishes. Nobody was ever going to press that button 272 times.

So: a pass over the whole menu, in Tools beside the other catalogue levers.

The thing that had to be settled first

The moment a pass can walk three hundred dishes unattended, the rule this whole feature was built on comes under a new kind of pressure.

Until now the question could not arise. The only way numbers reached a dish was a person typing them, or pressing Estimate and then Save - either way a person put them there, so every stored figure was somebody's word. A bulk pass breaks that, and an unchecked guess would start earning "Heart healthy" and "Diabetic friendly" on a live menu.

That is the same harm as a tick box for those words, arriving by a door that is much harder to see - and one I would have walked straight through.

A guess may be stored. It may not be published. Nutrition now records who said so, and dish-facts publishes nothing derived from an estimate: no badges, and not even the calorie figure, because a number on a menu is itself a claim. The shop sees them on its own screen, where they are a draft. Confirming is opening the dish and saving it - one deliberate act by somebody accountable.

Empty means kitchen, deliberately: every number stored before this field existed got there because a person typed it and saved, so reading the absence as "unverified" would strip the badges off every dish already done.

The server sets that word, never the caller. A client that could send 'kitchen' alongside its estimate would publish the guess with nothing anywhere to say so.

What the screen owes the shop

What it costs. One call to their own AI provider per dish, so the count is read first and put in the button: "Estimate 214 dishes". "Run this over your menu" with no number in front of it is not a choice anybody can make.

That it can be stopped. The browser walks the list one dish at a time, so Stop means stop and closing the window means stop. A server job could do neither, and would have to invent its own progress reporting besides.

That a refusal ends it. 400 from that route means the AI is refusing - no key, provider down, cap spent - and every remaining dish would fail identically. Asking three hundred times in a row is how a rate limit becomes a ban.

Two things that would have gone wrong quietly

The write is four fields by name, not a replay of the item form's upsert. That writes every field on the document, and anything the caller did not send comes back as a default - survivable when a person is looking at a form, not when a loop is walking a menu unattended. It skips any dish a person has already answered, so the pass is safe to run twice.

The entry explains itself instead of vanishing. The single-dish button hides when there is no AI key, but the only place that asks about availability is the item form, and this lives on the item list. A gate written the same way would have been dead code and the entry hidden on every shop forever. It names where to paste the key instead.

Checks

  • 10,619 API tests and 2,412 root tests pass
  • 9 new tests, most of them about the guess-may-not-publish seam
  • No local t() wrapper in the new file - that is how the requests dock ended up with its entire key set in no language pack. All 24 keys spelled out, translated into all 17 packs, coverage held at 93%
  • BaseModel.license, not .licenseId - the latter does not exist, so the filter would simply have had no licence clause

Nutrition shipped last night and is invisible on every shop in the estate,
because the way to fill it in is eight numbers per dish and the assistant does
one dish per press. Azure Costal Kitchen has 272 dishes. Nobody was ever going
to press that button 272 times, so the panel stays empty everywhere and the
feature may as well not exist.

So there is a pass over the whole menu, in Tools beside the other catalogue
levers. And the moment that exists, the rule the whole feature was built on
comes under a new kind of pressure.

A GUESS MAY BE STORED. IT MAY NOT BE PUBLISHED.

Until now the question could not arise: the only way numbers reached a dish
was a person typing them, or pressing Estimate and then Save. Either way a
person put them there, so every stored figure was somebody's word. A pass that
walks three hundred dishes unattended breaks that, and an unchecked guess would
start earning "Heart healthy" and "Diabetic friendly" on a live menu - the same
harm as a tick box for those words, arriving by a door that is much harder to
see, and one I would have walked through without noticing.

So nutrition now records WHO SAID SO, and dish-facts publishes nothing derived
from an estimate: no badges, and not even the calorie figure, because a number
on a menu is itself a claim. The shop sees them on its own screen, where they
are a draft. Confirming is opening the dish and saving it, which is one
deliberate act by somebody accountable.

Empty means kitchen, deliberately: every number stored before this field
existed got there because a person typed it and saved, so reading the absence
as "unverified" would strip the badges off every dish already done.

The server sets that word, never the caller. A client that could send
'kitchen' alongside its estimate would publish the guess and nothing anywhere
would say so.

THE OTHER THREE THINGS THE SCREEN OWES THE SHOP.

What it costs: one call to their own AI provider per dish, so the count is
read first and put in the button. "Run this over your menu" with no number in
front of it is not a choice anybody can make.

That it can be stopped: the browser walks the list, one dish at a time, so
Stop means stop and closing the window means stop. A job on the server could
do neither, and would have to invent its own progress reporting besides.

That a refusal ends it: 400 from that route means the AI is refusing - no key,
provider down, cap spent - and every remaining dish would fail identically.
Asking three hundred times in a row is how a rate limit becomes a ban.

The write is four fields by name, not a replay of the item form's upsert -
that writes every field on the document, and anything the caller did not send
comes back as a default. Survivable when a person is looking at a form; not
when a loop is walking a menu unattended. It skips any dish a person has
already answered, so the pass is safe to run twice.

The entry explains itself instead of vanishing. The single-dish button hides
when there is no AI key, but the only place that asks about availability is
the item FORM and this lives on the item LIST - a gate written the same way
would have been dead code and the entry hidden on every shop forever. It says
where to paste the key instead, which nobody finds on their own.

No local t() wrapper in the new file. That is how the requests dock ended up
with its entire key set in no language pack: the scanner reads literal
PosnicPro.i18n.t calls and a helper hides every one of them. All 24 keys are
spelled out, translated into all seventeen packs, and coverage held at 93%.
Nothing disagreed. This branch appended the nutrition-pass strings and develop
appended the bill-copies strings, both after the last existing key in all
seventeen packs, so every pack conflicted on the line that had to grow a
trailing comma.

Resolved as JSON rather than as text: each pack is this branch's keys in its
own order, then any key develop added that was not already there. Checked
first that no key had its value changed on both sides and none was deleted on
one side only - that would have been a real disagreement, not a collision of
two appends, and is not something to settle mechanically.

check-translations.js says All good and coverage is unchanged at 93%.
Owner: "whenever you show filter, no item in the list then dont show that
filter in menu. example heart healthy food not in our menu then dont show the
filter itself."

The sort-and-filter sheet was built that way already - every option is counted
against the menu first and only offered if a dish carries it. Two older chips
were not, because each was gated on a question slightly different from the one
its own filter asks.

VEG ONLY was gated on "does any dish have a diet mark at all". A steakhouse
marks every dish non_veg, so the mark is present on all of them, the chip
appears, and tapping it empties the menu and says "Nothing on the menu is
marked vegetarian." The gate now asks exactly what the filter asks - veg or
vegan - with unmarked still never assumed either way, because a shop that
never filled the field has promised nothing.

AVAILABLE NOW, on the public menu, was shown to every restaurant. It is hidden
in both directions now: nothing available means it empties the menu, and
everything available means it changes nothing at all. A control that cannot
alter what you are looking at is the same annoyance as one that empties it.

Checked against the real production menu rather than assumed. Azure Costal
Kitchen: 29 sections, 272 dishes, ZERO diet marks, 272 of 272 available, none
carrying a health claim. So the veg chip is already correctly hidden there and
the health filters never appear - but "Available now" is on that page right
now with nothing it could possibly do, which is the complaint exactly. Driving
the live page confirmed it: vegChip hidden, availChip SHOWN, 272 dishes.
…ithub.com:Posnic/POS into feat/fill-in-a-menu-without-three-hundred-presses
@sridharkalaibala
sridharkalaibala merged commit 3cafd01 into develop Sep 15, 2026
8 of 10 checks passed
@github-actions github-actions Bot added the ready for QA Merged to develop and live on develop.posnic.io - anyone can test it label Sep 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Merged to develop. Anyone can test this - you do not need write access.

Try it at https://develop.posnic.io, or run it yourself:

git fetch origin develop && git checkout develop
npm install && npm --prefix api install
npm run dev   # then http://localhost:3000

When you have tested it, say what you did and what happened, and set
QA passed or QA failed. If you cannot set labels, just comment -
a maintainer will.

Reporting that something is broken is as useful as fixing it. It is
better found here than by a shopkeeper.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for QA Merged to develop and live on develop.posnic.io - anyone can test it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant