feat(visitors): add gate passes with host approval and honest overstay reconciliation (fixes #285) - #290
Open
MOHITKOURAV01 wants to merge 1 commit into
Conversation
…liation Replaces the paper register at the gate, which cannot answer the one question that matters during a fire drill: who is on campus right now. Visitors coming in and students going out are one model. The question does not care which direction somebody moved, and splitting them would mean writing the atomic check-out twice and having two answers to the same question. What the register could not do: - The live roll is a partial index on status: 'checked-in', so the evacuation list is one indexed lookup and is always current. - One open pass per person is a unique partial index rather than an application check. That is the difference between an invariant and a habit — an application guard holds only as long as every future code path remembers it. - Check-out is filtered on the pass still being checked in, so a double-tap on the gate tablet returns 409 instead of overwriting the departure time. A pass that was never checked in cannot be checked out at all. - A student is released only after a member of staff has authorised it, recorded against their name. No approval, no release. Stale passes are closed as 'auto-closed', never 'checked-out'. Checked out means somebody watched them leave; auto-closed means we assumed it. A register that records the second as the first is the paper one with a database underneath, and its count drifts into fiction within a week. The share of exits closed by assumption is on the stats page for the same reason. ID numbers are stored as the last four characters plus a keyed hash. The hash is what the uniqueness index is built on, so the full number never has to be kept — a gate terminal holding a database of government ID numbers is a liability with no operational benefit. Closes Sitaram8472#285
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.
Related Issue
Closes #285
Description
Replaces the paper register at the gate, which cannot answer the one question that matters when it matters: who is on campus right now.
Visitors coming in and students going out are one model here. The question does not care which direction somebody moved, and splitting them would mean writing the atomic check-out twice and having two answers to the same question.
What the paper register cannot do
The live roll is a query, not a guess. A partial index on
status: 'checked-in'makes the evacuation list one indexed lookup, always current. It is also the first tab in the panel — it is what somebody needs the moment an alarm goes off, and it should not be two clicks away.One open pass per person is enforced by the database.
Two gate terminals scanning the same person at the same moment would both read "not currently in" and both write. The index rejects the loser, and the controller translates the duplicate-key error into something the person at the desk can act on. Doing this in application code means it holds only as long as every future code path remembers the check — the difference between an invariant and a habit.
Check-out is atomic and cannot precede check-in. The update is filtered on the pass still being
checked-in, so a double-tap on the gate tablet matches nothing the second time and returns 409 rather than overwritingcheckOutAtand corrupting the recorded duration. A pass that was never checked in cannot be checked out at all — otherwise the register accumulates departures for arrivals that never happened.A student is released only after a member of staff has authorised it, recorded against their name. No approval, no release, enforced in the handler.
Reconciliation is honest about what it does not know
Stale passes are closed as
auto-closed, neverchecked-out.Checked out means somebody watched them leave. Auto-closed means we assumed it. A register that records the second as the first is the paper one with a database underneath, and its on-campus count drifts into fiction within a week — which is exactly how the current register fails.
The share of exits closed by assumption is surfaced on the stats page as "unobserved exits". It is the one figure there that says something about the process rather than about the visitors: a high number means the gate is not being worked.
Privacy
ID numbers are stored as the last four characters plus a keyed hash. The hash is what the uniqueness index is built on — the masked tail cannot carry that job, too many people share a last four — so the full number never has to be kept at all. A gate terminal holding a database of government ID numbers is a liability with no operational benefit.
Badge numbers are random rather than sequential: a sequential badge needs a read of the current count before the write, which reintroduces the race the check-in guard exists to avoid. The badge is a label a human reads off a lanyard, not a key.
Role scoping
Teachers reach exactly two things: the visits waiting on their approval, and passes they are hosting.
redactForstrips ID details, the security log and the movement trail from anyone who is not office or admin — a teacher approving a visit needs to know who is coming and why, and nothing else.Pages / Components Added or Modified
backend/models/VisitorPass.js— one model for both directions, the two partial indexes, overstay virtuals,redactFor()backend/controllers/visitorController.js— check-in, atomic check-out, approval, reconciliationbackend/routes/visitorRoutes.js—/api/visitorsbackend/server.js— route registration (2 lines)frontend/src/pages/VisitorDesk.jsx— the host's approval list; the full desk for office/adminfrontend/src/components/visitors/GateDeskPanel.jsx— live roll, register, all passes, statsfrontend/src/App.jsx—/visitorsroute (8 lines)Screenshots
Not attached — the roll is empty without seeded passes and I would rather add real screenshots than empty states. Happy to seed a demo set if that helps.
Checklist
npx eslint .passes — 18 problems, matching pristinemainnpx vite buildsucceeds;VisitorDeskcode-splits into its own chunknode --checkclean on every backend fileNotes for review
Two things I would like a second opinion on.
First, the register form currently takes a raw host user id and student user id rather than offering a picker. That is fine for a first cut but it is not what somebody at a gate wants to type. A lookup endpoint would fix it; I left it out to keep this PR to one concern.
Second,
GATE_SECRETfalls back toJWT_SECRETwhen unset, so the module works out of the box. A school that cares should set its own — rotating it is what invalidates every historical subject key.Merges cleanly against
mainand against every other open PR, in both orders. Does not touchTeacherDashboard.jsx.