References are immutable, because clients now store them

012 turned three descriptive columns into identifiers other systems
keep: in agent.json on a shop counter, in a saved URL, in a scheduled
report. All three were already treated as stable and none of it was
enforced.

- clients.slug is an MQTT topic segment the broker ACL is written
  against. Rename one and that tenant's whole estate is silently refused
  by the broker, with no way to tell the agents.
- sites.slug is what a shop PC calls itself - agent.json holds
  "site_id": "chennai", never the uuid. A rename orphans the PC from the
  shop it is standing in.
- site_cameras.camera_id lands in visits.camera_id, which is text and
  not a foreign key. A rename orphans every visit already attributed to
  the old name: the footfall is still there and no longer joins to a
  camera. This was half-enforced in handleUpdateCamera and nowhere else,
  which is the shape of a rule that holds until somebody adds a second
  write path.
- visitors.number is assigned once from the tenant's counter and read
  back as V-42.

A trigger, not a CHECK: a CHECK cannot see the old row and the rule is
about the transition. The DISPLAY name is deliberately not frozen -
"TeNext Chennai", "Front door" - it is what a person reads, nothing keys
on it, and a system that cannot fix a typo in a shop's name has confused
the two.

Also records why the uuid stays where a slug would do. The length was
never the problem; needing it was, and that is fixed. Replacing it would
touch eight foreign keys on a live database to shorten a field clients
are already told not to use, and a sequential id would make any future
tenancy hole walkable by counting. It is NOT because ids must be minted
offline - sites, visitors and visits are all created server-side with a
database in hand, and claiming otherwise would defend the status quo
rather than explain it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
This commit is contained in:
2026-09-07 12:17:49 +05:30
parent 08873f4a67
commit ce0223006b
4 changed files with 210 additions and 0 deletions

View File

@@ -2047,6 +2047,53 @@ the engine's own diagnostic dashboard.
human name. The prop carrying it is `customerRef`, not `ref` — React reserves
that name, so it would never have reached the component.
### Why the uuid stays, when the slug would do
Asked directly: `site_id` is 36 characters, why not a small number?
The honest answer is that **the length was never the problem — needing it was**,
and that is already fixed: `?site=chennai` and `/api/sites/chennai/check` work,
and the shop PC has always identified itself by slug (`agent.json` holds
`"site_id": "chennai"`, never the uuid). The uuid in a *response* is the stable
key for a client that wants to store one.
Two reasons not to replace it, and one reason that is NOT among them:
- **Enumeration.** `/api/sites/3/check` makes any future tenancy hole walkable
by counting; a uuid makes it require a leak first. Every handler scopes by the
session's client today, so this is defence in depth rather than the control —
but this database holds biometric templates, and defence in depth is the point
of a second layer.
- **The payoff is now zero.** Eight tables carry a foreign key to `sites(id)`,
against a live database, to make a field shorter that a client is already told
not to use.
- **NOT because ids must be minted offline.** Sites, visitors and visits are all
created server-side with a database in hand. That argument holds for the
agent's `event_id` — which is derived precisely so it needs no coordination —
and it does not hold here; claiming it would be a defence of the status quo
rather than a reason for it.
What DID need fixing is that the references were only stable by accident.
Migration 013 makes `clients.slug`, `sites.slug`, `site_cameras.camera_id` and
`visitors.number` immutable in the database, because 012 turned them from
descriptive columns into identifiers other systems store:
- `clients.slug` is an MQTT topic segment the broker ACL is written against.
Rename one and that tenant's whole estate is silently refused by the broker,
with no way to tell the agents.
- `sites.slug` is what a shop PC calls itself. A rename orphans the PC from the
shop it is standing in.
- `site_cameras.camera_id` lands in `visits.camera_id`, which is text and not a
foreign key. A rename orphans every visit already attributed to the old name:
the footfall is still there and no longer joins to a camera. This was
half-enforced in `handleUpdateCamera` and nowhere else — the shape of a rule
that holds until somebody adds a second write path.
A trigger rather than a CHECK, because a CHECK cannot see the old row and the
rule is about the transition. **The display name is deliberately NOT frozen** —
"TeNext Chennai", "Front door" — it is what a person reads, nothing keys on it,
and a system that cannot fix a typo in a shop's name has confused the two.
### Three uuids on one arrival, three different answers
Asked of the row the feed actually returns, and they do not get the same reply: