Skip to content

A shop can see its handsets, and stop one - #892

Merged
sridharkalaibala merged 5 commits into
developfrom
feat/a-shop-can-see-its-handsets
Sep 18, 2026
Merged

sridharkalaibala merged 5 commits into
developfrom
feat/a-shop-can-see-its-handsets

Conversation

@sridharkalaibala

Copy link
Copy Markdown
Contributor

Owner: "once logged in use jwt or proper app authendication system. map device to cloud account."

The first half was already true

A handset posts a username and a password once, is handed a bearer token, and presents that from then on. The password is never stored on the phone. Nothing here changes it.

The second half was not

A phone described itself on every order it sent, its model, its platform, a random id it keeps for itself, and an order was the only place any of it was ever written down. So a shop had a record of what a phone had done and no record of the phone, and no answer to the question that matters when one goes missing: stop that phone, leave the other four working.

Which is also what makes the thirty day token safe. A long credential is fine when it can be revoked and dangerous when the only control is an expiry, because an expiry does nothing tonight and everything in a month.

utils/handsets.js the register: remember, revoked, list, setRevoked
routes/handsets.routes.js GET /handsets, POST /handsets/:id/revoke, POST /handsets/:id/allow
middleware/auth.js the token names the phone, and the door checks it
users.controller.js a sign-in writes the phone down before it signs the token

The claim is in the token, not a header. A header is whatever the caller types, and a phone that names itself can name another one. It is read off the request rather than passed as an argument, so every other caller keeps the token it already had down to its claims: only the handset sign-in puts a device on the request.

The check sits in continueWithTenant, after the tenant is attached, because that is the first moment there is a shop to ask and every authenticated route passes through it. Answered for thirty seconds from a small cache, so a floor of phones does not put a query behind every tap; a revoke performed in the same process is felt immediately.

403 and DEVICE_REVOKED, not 401. The credential is perfectly good and signing in again with the same one would change nothing. captain says "the shop has turned this phone off" rather than "free a slot", which are two different nos that send somebody to different screens.

Two calls worth disagreeing with

Signing in again on a stopped phone lets it back. The premise of the whole feature is that a waiter does not know the shop's password, so anybody who can type it is somebody trusted with the till. Revoking is for the phone in a taxi, not for keeping a manager out of one. The row keeps that it was stopped.

Nothing here can lock a floor out. An app that sends no device signs in exactly as it did, a phone nobody has heard of is never refused, and a database that will not answer refuses nobody. A handset list is not a thing to stop a shop trading for.

One round lost

The first version stored the licence on the row and mobile-app-reachability caught it: that handler must never let the licence out. Each shop has its own database, so the row never needed it. There is now a test for that too.

21 tests. The phone's half is Posnic/captain#138.

Owner: "once logged in use jwt or proper app authendication system. map device
to cloud account."

The first half was already true and is unchanged. A handset signs in once with
a username and a password and is handed a bearer token, which is the only thing
it presents afterwards; the password is never stored on the phone.

THE SECOND HALF WAS NOT. A phone described itself on every ORDER it sent - its
model, its platform, a random id it keeps for itself - and an order was the
only place any of it was ever written down. So a shop had a record of what a
phone had DONE and no record of the phone, and no answer to the question that
matters when one goes missing: stop THAT phone, leave the other four working.

Which is also what makes the thirty day token safe. A long credential is fine
when it can be revoked and dangerous when the only control is an expiry,
because an expiry does nothing tonight and everything in a month.

  utils/handsets.js     the register: remember, revoked, list, setRevoked
  routes/handsets.js    GET / to see them, POST /:id/revoke and /:id/allow
  middleware/auth.js    the token names the phone, and the door checks it
  users.controller.js   a sign-in writes the phone down before signing

The claim is IN THE TOKEN rather than in a header, because a header is
whatever the caller types and a phone that names itself can name another. It
is set from the request, so every other caller keeps the token it already had,
down to its claims: only the handset sign-in puts a device on the request.

The check sits in continueWithTenant, after the tenant is attached, because
that is the first moment there is a shop to ask and every authenticated route
passes through it. A stopped phone gets 403 and DEVICE_REVOKED, not 401: the
credential is perfectly good and signing in again with the same one would
change nothing.

SIGNING IN AGAIN ON A STOPPED PHONE LETS IT BACK, deliberately. The premise of
the whole feature is that a waiter does not know the shop's password, so
somebody who can type it is somebody the shop trusts with the till. Revoking is
for the phone in a taxi, not for keeping a manager out of one. The row keeps
that it was stopped.

An app that sends no device signs in exactly as it did, and a database that
will not answer refuses nobody: a list is not a thing to lock a floor out for.

The licence is not on the row. Every shop has its own database, so the row is
already inside the only tenancy boundary there is, and mobile-app-reachability
holds that handler to never letting the licence out. The first version stored
it and that test said so.

21 tests, including that one.
The generated one, which is checked on every push: three endpoints and a
group. Prettier on the new file at the same time.
@sridharkalaibala

Copy link
Copy Markdown
Contributor Author

CI is currently red, so this is not ready to merge yet. The API job reports that README still claims 661 endpoints while generated API docs report 664; run npm run badges:fix. Desktop tests also report stale generated server strings; run node tests/tools/i18n-server-text.js --write. Finally, the root route-count assertion fails after the three new handset routes—please update it to reflect the current route inventory (or derive it rather than pinning a count).

The register and its three endpoints landed first, and for a few hours this
shop had a way to stop a phone that nothing could reach. That is the shape of
the kitchen announcement, which shipped complete and switched off for its whole
life because the only way to turn it on was a developer console. A feature
nothing can reach is a feature nobody has.

Manage > Handsets, beside Employees, because it answers the same kind of
question: who, and on what, is taking orders in this shop.

  Phone          the model, with the platform and app version beneath
  Used by        who last signed in on it
  Last used      in the words a person would use, exact time in the title
  Status         in use, or stopped
                 and the button for whichever it is

Stopping one is asked about, because it takes a waiter's phone out of service
mid-shift and whoever is doing it is in a hurry. The question says what happens
next rather than "are you sure", which answers nothing: it stops taking orders
at once, and the shop password lets it back. Letting one back is not asked
about, since nothing is lost by it and the person has already decided.

What a phone said about itself is escaped; what this file says is <lang>
markup, so the walker translates it in place. That is the house pattern and the
only one the coverage tool can see - a string built by concatenation inside a
<lang> element reaches _english.json with the join in it, which is a key no
translator can answer. There is a test for that, because it happened here.

Tamil and Hindi carry all fifteen new words. The other fifteen packs fall to
the translation pipeline; coverage stays inside its ratchet.

The server's five new sentences are in the server text list, regenerated.
npm run badges:fix. Two checks read this number from opposite ends: the
README claim, and docs/API.md generated from the routes themselves. Adding a
route and not the badge fails both, which is the point of them.
The conflict was in ta.json and hi.json and it was additive on both sides:
develop added its keys at the end, this branch added fifteen for the handset
screen. Taken as a union and sorted, so the next merge has less to argue with.

_english.json and the server text list regenerated afterwards, because both
are generated from what the union now holds.
@sridharkalaibala
sridharkalaibala merged commit ac70d7c into develop Sep 18, 2026
10 checks passed
@sridharkalaibala
sridharkalaibala deleted the feat/a-shop-can-see-its-handsets branch September 18, 2026 08:55
@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 18, 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