Operational Services — WiFi, Maintenance, Support & Gate — Technical Reference¶
Part of the Aashray Technical Reference. Pairs with Business Logic — Services, which describes these rules in plain terms with no code detail. Related technical detail currently lives inline in: Accounts, Identity & Auth, Admin Panel Map, API Reference (not yet split out into their own technical files).
This file is 100% derived from reading the backend and admin source on main — no operational/staff-knowledge content lives here (that's in the business-logic companion). It covers the four "everyday operations" services that hang off a member's identity rather than off a booking: campus WiFi credentials (temporary + permanent), maintenance requests (facility repairs routed to departments), support tickets (free-form help requests), and the physical gate check-in/check-out system that toggles a member's on-premises status. All four are anchored to the same hub — CardDb.cardno — and are almost entirely stateless w.r.t. the booking flows, except that WiFi and maintenance eligibility read a member's current check-in state, and the gate flow writes check-in state back onto room/flat bookings.
Verification status: audited & anchored per
docs/DOCS-METHODOLOGY.mdon 2026-07-07 againstaashray-backendmain(withaashray-admincross-checked). Every rule carries a[src: …]anchor. Correction this pass: the deep-cleaning WhatsApp recipient list was documented as two cardnos but is actually a ~20-entry hardcoded whitelist — fixed below. All discrepancies (inverted WiFi status, gate entry/exit asymmetry,gateExitnull-guard, no support admin surface, etc.) confirmed real against code.
Two auth realms appear throughout:
- Member realm — routes under /api/v1/{wifi,maintenance,support} guarded by validateCard (middleware in middleware/validate.js). It reads cardno from req.params/req.body/req.query, loads the CardDb row into req.user, and 404s (Cardno not provided / User not found) if absent. There is no password/JWT on the member realm — possession of a valid cardno is the only credential.
- Admin realm — routes under /api/v1/admin/* guarded by auth + authorizeRoles(...) (in middleware/AdminAuth.js): a JWT Bearer token identifies an AdminUsers row, AdminRoles supplies the role list, and each router allow-lists specific roles. The admin web panel (repo admin-main) is plain HTML/JS calling these endpoints with CONFIG.basePath = https://aashray.vitraagvigyaan.org/api/v1/admin.
1. WiFi¶
Data model¶
Campus network access is backed by two tables:
- wifi_pwd (WifiDb): pwd_id PK, cardno (nullable FK → card_db.cardno), password (not null), roombookingid (nullable FK → room_booking.bookingid), status ENUM(active,inactive) default active, updatedBy, timestamps. WifiDb.belongsTo(CardDb).
- permanent_wifi_codes (PermanentWifiCodes): id PK, cardno (not null FK → card_db.cardno), username (not null), code/ssid (nullable), status ENUM(pending,approved,rejected,reset,deleted) default pending, requested_at(NOW default)/reviewed_at/reviewed_by/admin_comments. CardDb.hasOne(PermanentWifiCodes) + PermanentWifiCodes.belongsTo(CardDb).
Inverted status semantics on wifi_pwd — this is a footgun worth flagging on its own: status='active' means "unclaimed / available in the pool," and status='inactive' means "claimed by a member." The naming is the opposite of what a reader would assume from the words alone; the generate flow succeeds only if the pool still has rows with status='active'. [src: models/wifi.model.js:8-39; controllers/wifi/wifi.controller.js:64,68 (claims 'active' → sets 'inactive')]
Member-app behavior¶
The WiFi screen (Home → "Wifi" tile, app route (home)/wifi) shows a Permanent section to everyone and a Temporary section only to non-residents (res_status not PR and not SEVA KUTIR). On load it fires GET /wifi (temporary list) and GET /wifi/permanent (permanent status cards). Actions: request a permanent code (device-type picker for PR/Seva Kutir; non-residents submit silently with device Mobile), reset an approved permanent code (confirm dialog), and generate a temporary code (disabled once the 1-code limit is hit). All codes/usernames/SSIDs are tap-to-copy. App-side instruction copy claims permanent codes are "valid for 1 year" and temporary codes "valid for 2 weeks with a data limit" — neither validity window is enforced or stored by the backend (see Discrepancies).
Backend rules & logic¶
Temporary codes (wifi_pwd, controller controllers/wifi/wifi.controller.js):
- The pool is seeded by admins (bulk Excel upload) as rows with cardno = null, roombookingid = null, status = 'active'.
- generateTempCode (GET /wifi/generate): [src: controllers/wifi/wifi.controller.js:26-93,329-364]
1. Eligibility: res_status must be MUMUKSHU or GUEST, else 403 "You are not eligible to generate a temporary WiFi code".
2. Must be checked in: fetchBookings(cardno) looks for a RoomBooking, FlatBooking, or UtsavBooking with status = 'checkedin' and checkout/end_date >= today; if none → 404 "user not checked in yet.". The claimed booking's bookingid becomes roombookingid.
3. Limit: counts existing inactive rows for this cardno + roombookingid; if >= MAX_WIFI_PASS_LIMIT (= 1, defined in wifi.controller.js:26, not constants.js) → 400 "Cannot generate more than 1 passwords".
4. Claim: UPDATE one status='active' pool row (ordered by pwd_id ASC, limit 1) setting cardno, roombookingid, status='inactive'. If nothing was updated (empty pool) → 404 "No available WiFi codes to assign. Please try again later.". Returns the claimed password (HTTP 200). Wrapped in a DB transaction.
- fetchTempCodes (GET /wifi): non-MUMUKSHU/GUEST get 200 with data: []; if not checked in, 200 "No active bookings found" + []; otherwise returns all wifi_pwd rows for this cardno + current bookingid (password, createdAt).
Permanent codes (permanent_wifi_codes):
- requestPermanentCode (POST /wifi/permanent, body deviceType): requires deviceType (400 if missing). For MUMUKSHU/GUEST only, blocks if an existing request is pending or approved (400 with a status-specific message). PR / Seva Kutir are not blocked, so they may hold multiple pending/approved rows (one per device — matches the app's per-device model). [src: controllers/wifi/wifi.controller.js:132-208] Username generation: strip a leading guest-; drop ignore-prefixes (rcof, rchk, cons, chak, divi, paon, guest); username = <firstName><lastName><last4ofCardno><deviceSuffix> where suffix map is mobile→ph, laptop→pc, tablet→tb, else→ot; a numeric counter is appended to de-dup against existing approved/reset/pending usernames. Creates the row status='pending', requested_at=now, then fires sendWifiRequestWhatsApp(...). Returns 201.
- fetchPermanentCodes (GET /wifi/permanent): returns all rows for the cardno except deleted, exposing id, username, code, ssid, status, requested_at, reviewed_at, admin_comments.
- resetPermanentCode (POST /wifi/permanent/reset, body id): must reference an approved row owned by the cardno (404 otherwise); flips it to status='reset', WhatsApp-notifies. Re-enters the admin queue.
Every route¶
| Method | Path | Auth realm | Purpose | Key params/body |
|---|---|---|---|---|
| GET | /api/v1/wifi |
Member (validateCard) |
List this stay's temporary codes | cardno (query) |
| GET | /api/v1/wifi/generate |
Member | Claim one temporary code from pool | cardno (query) |
| POST | /api/v1/wifi/permanent |
Member | Request a permanent code | cardno, deviceType |
| GET | /api/v1/wifi/permanent |
Member | Fetch permanent request status cards | cardno (query) |
| POST | /api/v1/wifi/permanent/reset |
Member | Request reset of an approved code | cardno, id |
| POST | /api/v1/admin/wifi/uploadcode |
Admin (superAdmin, wifiAdmin) |
Bulk-seed temporary-code pool from Excel | multipart file (col password) |
| GET | /api/v1/admin/wifi/wifirecords |
Admin | Temporary-code report (joined to card + room/flat) | query startDate,endDate,status,bookingType |
| GET | /api/v1/admin/wifi/permanent |
Admin | List permanent requests (filterable) | query status, requestType (pending-new|pending-reset) |
| PUT | /api/v1/admin/wifi/permanent/:requestId |
Admin | Approve/reject/reset/delete/re-pending a request | body action, permanent_code, admin_comments, ssid, username |
| POST | /api/v1/admin/wifi/uploadpercode |
Admin | Bulk-update permanent codes from Excel (supports ?dryRun=true) |
multipart file |
| POST | /api/v1/admin/wifi/insertpercode |
Admin | Bulk-insert permanent codes from Excel | ?allowInsert=true (required), ?dryRun=true |
| POST | /api/v1/admin/wifi/manual |
Admin | Manually add one approved permanent code | body mobno, cardno, ssid, deviceType, username, code |
| GET | /api/v1/admin/wifi/generate-username |
Admin | Preview a generated username | query cardno, issuedto, deviceType |
| GET | /api/v1/admin/wifi/permanent/portal-export |
Admin | Export approved codes as portal-account XLSX | query hours OR startDate+endDate |
Admin operations¶
Admin panel WiFi pages live under admin-main/admin/wifi/ (index.html, wifiReport.html/js, permanentCodeRequests.html/js, uploadCodes.js), gated to wifiAdmin / superAdmin.
- Temporary: uploadCodes.js → POST /wifi/uploadcode seeds the pool (dedupes on password, ignores duplicates). wifiReport.js → GET /wifi/wifirecords lists claimed codes with holder + room/flat check-in dates.
- Permanent: permanentCodeRequests.js drives the whole lifecycle. It lists via GET /wifi/permanent (status filter or requestType=pending-new/pending-reset), then per-row calls PUT /wifi/permanent/:requestId with action. Backend validations on update: action ∈ {approved,rejected,deleted,reset,pending}; can't delete an already-deleted row; only an approved row may move to reset; approving a code-less (pending-new) row requires permanent_code; a duplicate approved code across users is rejected with the conflicting cardno. Approving sets code; resetting nulls code; every update stamps reviewed_at/reviewed_by/admin_comments and WhatsApps the member. The page also does manual add (POST /wifi/manual, which looks up the card by cardno+mobno), username preview (GET /wifi/generate-username), bulk update/insert (uploadpercode/insertpercode, both with a dryRun diff preview), and portal export (portal-export, which derives First_name/Last_name/User_group from username + res_status — PR→Residents, Seva Kutir→1Yr1DeviceUnlimitedData, Mumukshu/Guest→NonPRpermanentCode).
Edge cases & branches¶
- Temporary eligibility is doubly gated:
res_status ∈ {MUMUKSHU, GUEST}and an active check-in (room/flat/utsav). PR/Seva Kutir never see the section; a checked-out member gets an empty list. - Permanent duplicate-request block applies only to Mumukshu/Guest; PR/Seva Kutir intentionally bypass it.
- Bulk
insertpercodeis safety-gated: without?allowInsert=trueit returns400 "Insert not allowed. Confirm by sending allowInsert=true". Both bulk endpoints support?dryRun=trueto preview changes without committing, and auto-generate a username when the Excel/DB username is blank. - WhatsApp sends are fire-and-forget (
setTimeout, 200ms throttle for bulk) — a WhatsApp failure never fails the request.
Discrepancies¶
- Validity windows are fiction at the backend. The app advertises "1 year" (permanent) and "2 weeks + data limit" (temporary), but no expiry, TTL, or data-cap column exists and nothing ever expires a code. Enforcement (if any) lives on the network appliance / portal export, not in Aashray.
[src: models/wifi.model.js; models/permanent_wifi_codes.model.js (no expiry/TTL/data columns)] deviceTypeis used but not modeled.requestPermanentCode,addPermanentCodeManually, and the bulk importers read/writedeviceType, but the Sequelize model (models/permanent_wifi_codes.model.js) does not declare adeviceTypecolumn. (InrequestPermanentCodedeviceTypeis only used to build the username/WhatsApp and is not passed to.create(), so no dropped write there; the manual/bulk paths may still write it against an unmodeled column.)[src: controllers/wifi/wifi.controller.js:132,243-251; models/permanent_wifi_codes.model.js]- The commented-out original
wifi.routes.jsreferenced adeletePermanentCodeclient endpoint; deletion is now admin-only (action:'deleted'). wifiRecordbuilds SQL by string concatenation with:replacements— parameterized, butbookingTypefilter joins bothroom_bookingandflat_bookingonroombookingid, so a flat-claimed temp code appears underbookingType=flatcorrectly but the join is wide.
2. Maintenance¶
What it is¶
Member-filed facility work requests (electrical / housekeeping / general maintenance) routed to a department, tracked through an open → in progress → closed lifecycle, and worked by department-scoped admins. A separate deep-cleaning tracker for flats rides on the same admin router.
Member-app behavior¶
Home → "Maintenance" tile → app route (home)/maintenanceRequestList ("Maintenance History"). A paginated, status-filterable (All/Open/Closed) list of the member's own requests as expandable cards, plus a floating "+" that opens a request form (Department single-select, multiline Detail of Work, single-line Place). Submit posts the request; list refetches. Department options shown are "Electrical", "House Keeping", "Maintenance" (the server key for "House Keeping" differs from the label).
Backend rules & logic¶
Controller controllers/client/maintenanceRequest.controller.js:
- CreateRequest (POST /maintenance/request): Eligibility — if res_status is not PR and not SEVA KUTIR, the member must have a RoomBooking with status='checkedin' (only RoomBooking — not flat or utsav), else 400 "You are not checked in". PR/Seva Kutir may file anytime. Creates a MaintenanceDb row with bookingid = uuidv4(), requested_by = cardno, department, work_detail, area_of_work (nullable), updatedBy='USER', default status='open'. Looks up the department's dept_email (400 "Department not found" if missing), emails the member (cc department) via the maintainanceRequest template, commits, then fires the maintenance_request_received WhatsApp template. Returns 201. [src: controllers/client/maintenanceRequest.controller.js:32-135]
- ViewRequest (GET /maintenance): paginated (page, page_size default 10) list of the member's own requests (requested_by = cardno), optional status filter (default all), ordered createdAt DESC, excluding updatedAt/updatedBy.
- FetchDepartments (GET /maintenance/departments): returns all Departments rows (used to populate the picker).
Every route¶
| Method | Path | Auth realm | Purpose | Key params/body |
|---|---|---|---|---|
| POST | /api/v1/maintenance/request |
Member (validateCard) |
File a maintenance request | cardno, department, work_detail, area_of_work? |
| GET | /api/v1/maintenance |
Member | List own requests (paginated, filtered) | cardno, page?, page_size?, status? |
| GET | /api/v1/maintenance/departments |
Member | List departments for the picker | cardno (query) |
| GET | /api/v1/admin/maintenance/fetch/:department |
Admin (superAdmin,maintenanceAdmin,housekeepingAdmin,electricalAdmin) |
Department work queue (prioritized) | :department path |
| PUT | /api/v1/admin/maintenance/update |
Admin | Update comments/status (close notifies member) | body bookingid, comments, status |
| GET | /api/v1/admin/maintenance/housekeeping/deep-cleaning/status |
Admin | Flat deep-cleaning status per flat | — |
| POST | /api/v1/admin/maintenance/housekeeping/deep-cleaning/done |
Admin | Mark deep cleaning done | body flatno|flatnos[], cleaningDate? |
| POST | /api/v1/admin/maintenance/housekeeping/deep-cleaning/interval |
Admin | Set a flat's cleaning interval | body flatno, interval |
Admin operations¶
Admin pages under admin-main/admin/maintenance/ and admin-main/admin/housekeeping/, gated to superAdmin + the three department admins.
- maintenance.js reads ?department=maintenance|housekeeping|electrical from the URL and calls GET /maintenance/fetch/:department. fetchMaintenanceReport returns rows for that department (statuses open/in progress/closed), joined to CardDb (issuedto,mobno), ordered by a status-priority literal (open=0, in progress=1, closed=2) then createdAt DESC, and computes a virtual closedAt (= updatedAt when closed).
- updateRequest.js → PUT /maintenance/update. updateMaintenanceRequest updates comments and status on the row (found by bookingid, included with CardDb as 'card'). When it transitions into closed (and wasn't already), it sends the maintenance_request_closed WhatsApp template to the member. Note: a department field in the body is accepted but ignored (reassignment is commented out).
- Deep-cleaning tracker (housekeeping/deepcleaning.js) is orthogonal to member requests — it operates on FlatDb (last_deep_cleaning, deep_cleaning_interval, deep_cleaning_history JSON), and only WhatsApps a hardcoded ~20-cardno whitelist (DEEP_CLEANING_WA_RECIPIENTS, starting '0002945690','0009076440','0002819369','0012247780',…). [src: config/constants.js:173+]
Data & relationships¶
maintenance_db(MaintenanceDb):bookingidPK (UUID string),requested_by(FK →card_db.cardno),department(FK →departments.dept_name),work_detail(not null),area_of_work/comments(nullable),statusENUM(open,closed,in progress) defaultopen,finished_atINTEGER (nullable, unused),updatedBy. Associations:MaintenanceDb.belongsTo(CardDb, {as:'card', fk:'requested_by'}),MaintenanceDb.belongsTo(Departments, {fk:'department'→'dept_name'});CardDb.hasMany(MaintenanceDb);Departments.hasMany(MaintenanceDb).departments(Departments):dept_namePK,dept_head,dept_email,updatedBy.
Edge cases & branches¶
- Non-resident eligibility checks only
RoomBookingcheck-in — not flat or utsav (unlike WiFi'sfetchBookings). A non-resident staying only via a flat or utsav booking is told "You are not checked in" and cannot file a request. - The status priority sort surfaces open work first;
closedAtis derived, not stored. - Email cc goes to the department mailbox; both email and WhatsApp are best-effort and log-and-continue on failure.
Discrepancies¶
- Flat/utsav guests are locked out of filing requests (room-only check-in gate) — likely unintended given the housekeeping tracker targets flats.
[src: controllers/client/maintenanceRequest.controller.js:32-44] finished_atcolumn exists but is never written.[src: models/maintenance_db.model.js (finished_at declared, no writes anywhere)]- Admin
updateMaintenanceRequestcannot reassign departments despite the request body acceptingdepartment(dead field).[src: controllers/admin/maintenanceManagement.controller.js:82,106]
3. Support¶
What it is¶
A minimal, write-only "general help" ticket: a member picks a service category and writes a free-text issue.
Member-app behavior¶
Home → "Support" tile → app route (home)/support ("Support Ticket"). Two fields — service single-select ("Booking Related Issues", "Payment Related Issues", "WiFi Related Issues", "Other") and a multiline description (client-side must be ≥ 10 characters). Submit posts the ticket, shows a success alert, resets, and navigates back. A back-discard guard warns if fields have content.
Backend rules & logic¶
Controller controllers/client/support.controller.js, single handler createTicket (POST /support): opens a transaction, creates a SupportTickets row (issued_by = cardno, service, issue), commits, returns 201 {success:true, message:'Ticket created successfully'}. No server-side validation of service/issue content or length (length is enforced only in the app). An email notification to tech@vitraagvigyaan.org exists in code but is commented out. [src: controllers/client/support.controller.js:13-38 (commented email :25-33); models/support_tickets.model.js:7-28 (no status/response columns)]
Every route¶
| Method | Path | Auth realm | Purpose | Key params/body |
|---|---|---|---|---|
| POST | /api/v1/support |
Member (validateCard) |
Create a support ticket | cardno, service, issue |
Admin operations¶
None found. There is no routes/admin/support* router, no admin support controller, and no admin panel page for support in admin-main. Tickets are currently readable only by direct database access.
Data & relationships¶
support_tickets(SupportTickets):idPK auto-increment,issued_by(not null FK →card_db.cardno),service(not null),issueTEXT (not null), timestamps.SupportTickets.belongsTo(CardDb, {fk:'issued_by'});CardDb.hasMany(SupportTickets).
Edge cases & branches¶
- No length/format validation server-side; a client bypassing the app could submit an empty/short issue.
- Transaction wraps a single insert (no partial-failure surface).
Discrepancies¶
- No admin surface exists for support tickets despite this being a "ticket" system — no list, no status, no assignee, no reply. The model has no
status/resolved/responsecolumns; a ticket is fire-and-forget. The disabled email means staff currently receive no notification either. This is the largest functional gap of the four services.[src: no routes/admin/support*, no admin support controller; models/support_tickets.model.js:7-28]
4. Gate¶
What it is¶
The physical entry/exit system. A member's QR (which encodes only their cardno) is scanned at the gate; the backend records an on-prem/off-prem event, toggles the member's live presence status on CardDb, and cascades check-in/check-out onto that member's room/flat bookings. It also powers the gate dashboard head-counts by resident class.
Member-app behavior¶
Members have no gate API — their only role is to present their identity QR. The app renders the cardno as a QR in two places (quick modal from the tab bar's floating QR button, and a full-screen ticket at profile/qr); both encode the raw cardno (the gate scanner also accepts a cardnumber=<cardno> prefixed payload and strips it). Staff at the gate scan it. So the "gate flow" is entirely an admin/staff operation triggered by the member's QR.
Backend rules & logic¶
Controller controllers/admin/gateManagement.controller.js, all inside /api/v1/admin/gate (roles gateAdmin, superAdmin):
- gateEntry (POST /gate/entry, body cardno): loads the CardDb (404 "User not found" if absent); if status == offprem sets status = onprem; inserts a GateRecord (status='onprem', updatedBy = admin username). On response finish (post-commit, best-effort), auto-checks-in both a FlatBooking and a RoomBooking whose status='pending checkin' and checkin = today → sets them checkedin. Returns {cardno, issuedto}.
- gateExit (POST /gate/exit, body cardno): loads CardDb, sets status='offprem', inserts a GateRecord (status='offprem'), and auto-checks-out a FlatBooking whose status='checkedin' and checkout <= today → checkedout. Returns {cardno, issuedto}.
- gateRecord (GET /gate/gaterecords): raw SQL, all gate records joined to card_db (issuedto,mobno), newest first.
- fetchGateHistoryByCard (GET /gate/history/:cardno): all GateRecord rows for one card, newest first.
- Head-count dashboards: fetchTotal (/gate/total, counts status=onprem grouped by res_status), and fetchPR/fetchGuest/fetchMumukshu/fetchSevaKutir (/gate/totalPR|totalGuest|totalMumukshu|totalSeva), each returning currently-on-prem cards of that res_status with correlated-subquery last_checkin (MAX createdAt where gate status onprem) and last_checkout (MAX where offprem).
Entry/exit asymmetry (the core defect): gateEntry auto-checks-in both a RoomBooking and a FlatBooking whose status is pending checkin and whose checkin date is today. gateExit only auto-checks-out a FlatBooking whose status is checkedin and whose checkout date has arrived — there is no equivalent room-booking branch in gateExit. The two handlers are not mirror images of each other, and a room-based stay is never automatically closed out at the gate. [src: controllers/admin/gateManagement.controller.js:200-224 (entry: flat + room); :262-273 (exit: flat only)]
Missing null guard (latent bug): gateEntry explicitly checks for a missing CardDb row and returns 404 "User not found". gateExit does not perform the same check — it calls .update(...) directly on the lookup result, so an unknown cardno throws when Sequelize tries to call .update on null, surfacing an unhandled 500 instead of a clean 404. [src: controllers/admin/gateManagement.controller.js:175-181 (entry guard); :242-249 (exit, no guard)]
Every route¶
| Method | Path | Auth realm | Purpose | Key params/body |
|---|---|---|---|---|
| GET | /api/v1/admin/gate/total |
Admin (gateAdmin,superAdmin) |
On-prem head-count grouped by res_status | — |
| GET | /api/v1/admin/gate/totalPR |
Admin | On-prem PRs + last in/out | — |
| GET | /api/v1/admin/gate/totalGuest |
Admin | On-prem Guests + last in/out | — |
| GET | /api/v1/admin/gate/totalMumukshu |
Admin | On-prem Mumukshus + last in/out | — |
| GET | /api/v1/admin/gate/totalSeva |
Admin | On-prem Seva Kutir + last in/out | — |
| POST | /api/v1/admin/gate/entry |
Admin | Record entry, set onprem, auto check-in room+flat | body cardno |
| POST | /api/v1/admin/gate/exit |
Admin | Record exit, set offprem, auto check-out flat | body cardno |
| GET | /api/v1/admin/gate/gaterecords |
Admin | All gate records (joined to card) | — |
| GET | /api/v1/admin/gate/history/:cardno |
Admin | Gate history for one card | :cardno path |
Admin operations¶
Gate pages under admin-main/admin/gate/, gated to gateAdmin/superAdmin. Two capture modes each for in and out:
- QR scan (gateIn.html/js, gateOut.html/js) — uses the Html5Qrcode library with the rear camera to scan the member's app QR; processScannedText strips a cardnumber= prefix if present, then POSTs {cardno} to /gate/entry or /gate/exit. On success it shows ✅ QR Code Scanned: {cardno} ({issuedto}).
- Manual / scanner-gun entry (gateInTap.html/js, gateOutTap.html/js) — a text input (keyboard or a USB barcode reader) submits a cardno; same POST. Plays an error sound on failure.
- gateReport.html/js → GET /gate/gaterecords renders the full event log; the dashboard cards call the total* endpoints.
Data & relationships¶
gate_record(GateRecord):idPK auto-increment,cardno(not null FK →card_db.cardno),statusENUM(onprem,offprem),updatedBy, timestamps.GateRecord.belongsTo(CardDb);CardDb.hasMany(GateRecord)(CASCADE). Every scan appends a new immutable row — presence history is the full event stream, andCardDb.statusis the denormalized "current" value.- Cross-domain writes: gate entry flips
RoomBooking/FlatBooking.statuspending checkin → checkedin; gate exit flipsFlatBooking.statuscheckedin → checkedout. These are the physical realization of the booking lifecycle (see the Bookings domain).
Edge cases & branches¶
- The auto check-in runs in a
res.on('finish')callback outside the transaction and swallows errors — a booking that fails to flip is logged only; the gate event still commits. - Presence status is idempotent-ish: entry only writes
onpremwhen currentlyoffprem, but always appends aGateRecord, so repeated scans stack duplicate events. - QR payload is trusted verbatim as a
cardno— there is no signature or expiry on the identity QR (it is explicitly an identity token, not a secure credential).
Discrepancies¶
- Entry/exit asymmetry.
gateEntryauto-checks-in bothRoomBookingandFlatBooking;gateExitonly auto-checks-outFlatBooking. The room-booking auto-checkout omission means room stays can linger incheckedinafter the guest physically leaves, unless a room admin closes them elsewhere.[src: controllers/admin/gateManagement.controller.js:200-224,262-273] gateExit's missing null guard is a latent 500: unlikegateEntry, it does not check whether theCardDblookup returned a row before calling.updateon it.[src: controllers/admin/gateManagement.controller.js:242-249]- The member app labels this an identity/QR system for "check-in, meals, and other services," but the only backend consumer of the gate scan is the gate controller; meal/other-service scanning is handled by their own controllers reading
cardno, not viagate_record.
How this connects to other domains (technical)¶
- Accounts & Identity (business-logic file 02): every service is keyed to
CardDb.cardno; the member realm's only credential is a validcardno, andCardDb.res_status(PR/SEVA KUTIR/MUMUKSHU/GUEST) gates WiFi-temporary and maintenance eligibility.CardDb.status(onprem/offprem) is owned and toggled by the gate flow and read by the WiFi/maintenance check-in gates. - Bookings & Check-in lifecycle: WiFi-temporary and maintenance both require an active
checkedinroom/flat/utsav booking; the gate flow is what creates thosecheckedin/checkedoutstates by cascading ontoRoomBooking/FlatBooking. Bugs here (room not auto-checked-out) ripple into occupancy reporting. - Admin Panel & Roles (11): each service maps to dedicated admin roles —
wifiAdmin,maintenanceAdmin/housekeepingAdmin/electricalAdmin,gateAdmin(all alsosuperAdmin) — enforced byauthorizeRoles. Support has no role because it has no admin surface. - Notifications: WiFi (request/approve/reject/reset), maintenance (received/closed), and the housekeeping tracker all emit WhatsApp templates (and maintenance also email); gate and support emit none. All notifications are best-effort and never block the primary write.
- Departments are a shared reference table also relevant to org/admin config; maintenance is their only functional consumer today.