Skip to content

feat(visitors): add gate passes with host approval and honest overstay reconciliation (fixes #285) - #290

Open
MOHITKOURAV01 wants to merge 1 commit into
Sitaram8472:mainfrom
MOHITKOURAV01:feat/issue-285-visitor-gatepass
Open

feat(visitors): add gate passes with host approval and honest overstay reconciliation (fixes #285)#290
MOHITKOURAV01 wants to merge 1 commit into
Sitaram8472:mainfrom
MOHITKOURAV01:feat/issue-285-visitor-gatepass

Conversation

@MOHITKOURAV01

Copy link
Copy Markdown
Contributor

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.

visitorPassSchema.index(
  { subjectKey: 1 },
  { unique: true, partialFilterExpression: { status: 'checked-in', subjectKey: { $type: 'string' } } }
);

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 overwriting checkOutAt and 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, 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 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. redactFor strips 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, reconciliation
  • backend/routes/visitorRoutes.js/api/visitors
  • backend/server.js — route registration (2 lines)
  • frontend/src/pages/VisitorDesk.jsx — the host's approval list; the full desk for office/admin
  • frontend/src/components/visitors/GateDeskPanel.jsx — live roll, register, all passes, stats
  • frontend/src/App.jsx/visitors route (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 pristine main
  • npx vite build succeeds; VisitorDesk code-splits into its own chunk
  • node --check clean on every backend file
  • Responsive layout verified
  • Tested against a live database — the partial indexes in particular deserve a real Mongo instance before this merges
  • Screenshots attached

Notes 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_SECRET falls back to JWT_SECRET when 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 main and against every other open PR, in both orders. Does not touch TeacherDashboard.jsx.

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Visitor & Gate Pass management with host approval and open-visit reconciliation

1 participant