Release: the kitchen says when it is behind, and the spice choice can actually be switched on - #784
Merged
Merged
Conversation
Owner: "captain app keep disconnected... mobile till not responding after
seconds its connecting. its coz of wifi and rounter... i want right reason with
evidence."
The Wi-Fi was genuinely bad - measured on his shop, 516ms to the router itself
with packet loss, on a strong signal and an almost empty channel. But
underneath it sat a fault of ours that no router would have fixed.
WHAT WAS HAPPENING
The till caps the handsets it will answer, at six, and the list was keyed by IP
ADDRESS. Nothing expired it: entries went only when an admin pressed something,
and they were written to disk, so restarting the till did not clear them.
A phone's address changes constantly on ordinary Wi-Fi - a lease renews, it
roams between access points, it reconnects after the screen sleeps. Each new
address took a permanent slot. Ten wireless clients on one router used six in
days.
Then the refusal landed on the health check. Captain asks /runtime-info every
twenty seconds while connected and every four while it is not, and it reads any
non-ok response as "this server is unreachable":
if (!response.ok) return fail('REFUSED', String(response.status));
So the till said 403, the app dropped it, rescanned the network - 64 concurrent
probes, which on a weak router makes everything worse - found the same till,
asked again, and was refused again. That loop IS the disconnection.
TWO RULES
A health check is not a device. It is how something asks whether this till is
alive, it is public by design, and it is answered even when every slot is
taken. Otherwise the cap stops meaning "six handsets" and starts meaning
"nobody at all". A deliberate block still refuses it.
A slot nobody has used for a fortnight is released. It is not a handset, it is
last month's DHCP lease, and holding it costs a real phone its place.
And the refusal now says what to do. "Please contact your administrator" is, in
a shop where the owner IS the administrator, an instruction to contact himself
about a screen nobody has mentioned. It now names Hardware Manager, the Mobile
Devices list, and the fixed-address setting that stops it recurring.
The deciding moved out of the emit override into src/handset-slots.js, pure, so
it can be tested at all - and a test fails if it drifts back inline.
Desktop 2519 pass.
A handset is not its IP address
Owner: "desktop app system got logged out or screen lock, then also our app should keep wake." Stage 7 of the print roadmap, and the one failure that needs no queue to hurt. WHAT WAS HAPPENING Windows sleeps an idle machine, and a restaurant till is idle for long stretches between services. The moment it suspends the kitchen stops receiving tickets and handsets stop reaching it - while the screen still says Posnic and nothing reports a fault. The waiter presses send, the phone says sent, and no paper comes out. Nobody finds out until somebody walks to the printer. THE DISTINCTION THAT MATTERS Locking is not sleeping. A locked Windows session keeps running everything and keeps printing, so locking a till is fine and worth encouraging; LOGGING OUT ends the session and is the thing that kills it. Conflating the two sends a shop hunting the wrong fault, so a lock is logged saying exactly that. WHAT IS HELD, AND WHAT IS NOT prevent-app-suspension, never prevent-display-sleep. The system is kept from suspending; the screen may still blank, and should - a kitchen display that can never blank is one that burns in. The block is released on shutdown, because a till that looks switched off and still refuses to sleep is a machine nobody can explain. THE WAKE-UP IS WHERE DUPLICATES COME FROM A till asleep for two hours wakes with work behind it and a thirty second poll that did not run while it was suspended. Thirty seconds at a pass with no ticket is how a cook reprints by hand, and that is how a wake-up becomes the duplicate this whole area exists to prevent. So every collector runs on resume rather than at the next timer: kitchen tickets, floor bills, and cloud-relayed bills. One failing task cannot stop the others - the point of the moment is that everything catches up at once - and a rejected promise is caught, because an unhandled rejection on a till is a crash dialog in a restaurant. Nothing here can stop a till starting. powerSaveBlocker is genuinely absent under some remote sessions, so every call answers instead of throwing, and the startup call sits inside a try. Desktop 2530 pass.
#777 added src/handset-slots.js and this one added src/till-stays-awake.js, at the same line of build.files. Both ship. Nothing else disagreed. build.files is an explicit allowlist, so a file left out of it is simply absent from the installed app - the module resolves in development and is missing on a shop's machine. Worth resolving by keeping both rather than by taking a side.
The tick is per dish because only the kitchen knows which dishes it can
cook to order. But "per dish" and "one dish at a time" are not the same
thing, and the difference decides whether the feature is used at all.
Production carries 272 dishes. A restaurant that has to open every one of
them to tick a box does not turn this on: it leaves it off, and the
customer goes on typing "less spicy" into a note, which is exactly what
the spice level exists to replace.
The unit a kitchen thinks in is the section. Curries and biryanis can be
cooked to order; desserts and drinks cannot. So the items list gets the
same scope and the same check-then-apply shape as the bulk price and
stock tools beside it, on the same filter, and the shop corrects the
handful of exceptions on the dish itself.
Two details worth keeping. The check reports what would CHANGE rather
than what matches, so a second run says there is nothing left to do
instead of showing the same number again. And the write carries
spice_choice: { $ne: wanted }, so a dish that is already right is not
touched and keeps its updated_date, which is what the items list sorts on.
Not a one-way door either: the same tool takes the choice back off.
The till stays awake while it trades, and catches up the moment it wakes
Turning the spice choice on for a whole section, not 272 times
The print queue shipped without the rule the architecture document argued for. A job left in `printing` by a till that never came back was fair game again after two minutes. That reads as resilience and is the duplicate machine: A till claims a ticket. It sends the bytes. The printer takes the paper. The till dies before it can say so. Two minutes later the job looks abandoned, the next pass claims it, and the kitchen cooks the same order twice - because a cook prepares what arrives and does not compare it with what arrived a minute ago. Owner: "dupliate prints should not be there its loss for company. people wont care and keep preparing what receied." "Printed but never confirmed" and "never printed" are INDISTINGUISHABLE from the server, and the two wrong answers do not cost the same: a duplicate is silent and costs food, a miss is loud and costs a reminder. So the queue stops guessing. WHAT CHANGED A new status, needs_attention. A stale claim is swept into it instead of back to `queued`, and the claim query now matches `queued` only - if `printing` ever returns to that filter the duplicate returns with it, so a test names it. The sweep runs on the pass that would otherwise have re-claimed the job, not on a timer: the only moment that matters is when a till is about to take work, and a timer is one more thing to start, own, and discover has stopped. It does not count another attempt - that was counted at claim time, and counting twice retires a job on its third real try. Answering it is the only retry this design allows. "It printed" closes the job; "it did not" puts it back, which is a deliberate reprint by somebody who looked at the printer - the one kind of retry that cannot be wrong about what already came out. Two people answering at once cannot undo each other: the read checks the status and the write names it again. And out of attempts now asks a person too. Five failures is a printer somebody has to look at; `failed` put it where only a developer would find it. Owner's Q1, taken as mine to decide since he asked me not to wait: the till that took the job is the one asked. A shop has one counter and there is nobody else. The four print-queue messages are translated into all six server packs rather than left in English. API 10669 pass, desktop 2530 pass.
…asks A ticket nobody can confirm asks a person, rather than printing twice
A customer who waits forty minutes without being told blames the restaurant. One who was told chose to wait, and that is worth more than the handful of orders the warning costs. Capacity is the tables the shop has already typed in, load is every ticket still open in the kitchen, and the queue is roughly ceil(open / tables) rounds deep. Every channel counts: a dine-in table blocks the pass exactly as much as a phone does. A warning with no number is noise. "Delay expected" is either ignored or read as "do not order", because the reader has to imagine the wait and people imagine the worst. So the extra time is stated in minutes, and the minutes come from the shop's OWN median prep_minutes rather than a constant invented here - a tea stall and a grill house get different numbers, and a shop that has entered no prep times gets no figure at all and the weaker sentence instead. Same rule the health badges follow: never claim more than the numbers earn. It stays silent wherever it would be guessing: no restaurant module, no tables typed in, or a kitchen that is merely full rather than behind. Ten orders on ten tables is an ordinary busy evening, and crying wolf there teaches people to ignore the notice on the night it matters. It is a sentence and never a gate. Shown on the menu and again above the order button, and painted last in renderCart because everything there runs inside one try whose catch only logs - a decorative notice must not be able to abandon the bill.
The foundation the print queue needs before kitchen tickets can go anywhere
near it, and the reason this is a change on its own.
WHAT WAS WRONG
The till computes a key for every ticket - sale, type, and a hash of the
contents - and uses it twice: to skip one it has already printed, and to write
the durable claim in print-ledger.js that stops a restart reprinting the lot.
The SERVER never knew that key. It handed out sales and was told "these are
printed", and the two sides described the same piece of paper in different
words. So the question this whole area turns on had no answer:
Did the ticket the server expected to print actually print?
A queue cannot hold a thing it cannot name, and a shadow comparison between the
old path and a new one is impossible without one name.
AND THERE WERE TWO SCHEMES, NOT ONE
Found on the way. A sale with a print_jobs array is named saleId:type:hash; a
sale without one is named kot:saleId:token, type first, hashed over the sale's
own fields. Two names for one concept. Both are now in the module - both named,
rather than one unified and the other quietly left where the server still
cannot see it. Collapsing them into a single scheme would rename every ticket in
flight, so they stay as they are.
THE RISK, AND WHAT WAS DONE ABOUT IT
A changed key renames every ticket on ninety shops: the ledger stops recognising
what it already printed, and the next restart reprints a service. So the
extraction was proven byte-identical to the expressions it replaced rather than
eyeballed - 180 combinations of timestamp shape, type and contents for each of
the two schemes, zero disagreements, and the check is kept as a test.
The API ships outside the asar archive and cannot require the shell's modules,
so there is a second copy at api/src/utils/kot-job-key.js, compared byte for
byte by a test - the arrangement order-source.js already uses.
Desktop 2541 pass, API 10681 pass.
The first version asserted the shell and API copies were byte-identical, and the pre-commit hook broke it immediately: prettier formats them differently - the API half is linted by a second task with its own width - so byte identity cannot be held and would fail on every commit that touched either. It was also checking the wrong thing. What must never happen is two copies naming ONE ticket TWO ways, which is precisely the duplicate this module exists to make detectable. Formatting cannot cause that; a changed expression can. So it compares answers, over every shape of timestamp, type and contents, and over both naming schemes - the same guard order-source.js already uses for the same two-copy arrangement, and for the same reason.
The kitchen is busy, and the customer is told before they commit
…adow One name for one kitchen ticket, so the server can finally see it
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes develop to main. Five merged pull requests since the last release.
Merge with a merge commit, never a squash. The repository ruleset enforces that, so the button should only offer it.
The online ordering channel
#782 tells a customer the kitchen is behind, before they commit. A customer who waits forty minutes without being told blames the restaurant; one who was told chose to wait. Capacity is the tables the shop has already typed in, load is every open ticket in the kitchen whatever channel sent it, and the wait is stated in minutes taken from the shop's own median prep time rather than a constant. A shop that has entered no prep times gets the honest weaker sentence and no figure at all. A kitchen exactly full is not a warning: ten orders on ten tables is an ordinary busy evening, and a notice that fires every Friday is unread by the second week.
#780 makes the spice level usable. It shipped last release switched off on every dish in the world, and the only way to switch it on was opening dishes one at a time - 272 of them on this production shop. The items list now has Tools → Who may choose how hot: everything or one category, with the same check-then-apply as the bulk price and stock tools. The check counts what would change, so running it twice says "nothing to change" rather than showing the same number again.
Both stay invisible until a shop uses them. Production still carries 272 dishes with no prep times and no spice ticks, so nothing on any live menu changes the moment this merges.
From other sessions
#781 - a kitchen ticket nobody can confirm asks a person rather than printing twice. #779 - the till stays awake while it trades and catches up the moment it wakes. #777 - a handset is not its IP address, with measurements from the shop's own Wi-Fi.
Verified before opening
Root suite 2,608 of 2,611 on the develop tip; the three remaining are the release-artifact tests that need
frontend/publicbuilt, which CI does. API suite 10,698 of 10,698.After the merge
Worth a look on a restaurant with tables configured: put a few orders through the kitchen and the
/orderpage should start saying so once there are more open tickets than tables. If you would rather a shop could hide that notice, say so - it is a small follow-up, and I did not add a setting you had not asked for.