Using the Safety app — module by module
One application serves two roles. The Safety Officer works under /safety and decides whether work may proceed; the Inspector works under /inspector and checks what is actually happening on site. Same app, same login screen, different halves.
As with the Contractor guide, this describes what is on dev today, and says so inline wherever a module is switched off or has never had its precondition met in production. For why each role may do what it does, see What each role does.
Updated for CR round 4, re-checked 2026-09-12 against what has actually landed since
Areas are gone — the three area screens are deleted, and safety now names facility plans and places pins on them instead. The risk map switches between active plans, colours pins by status with a legend, refreshes itself visibly and now carries attention markers for a gas-overdue, critical-note or overlap permit. The review screen is tabbed (ten tabs, a Report tab among them), with anything urgent pinned above the tabs. Closure is now the safety officer's, and the screen finally says so — see Closing a permit; the old "backstop" wording is gone. A visit can now start from history, not only from a fresh scan. The inspector's visit is an action menu, not a fixed sequence, with a proper entrant table and "not available". The menu itself has shrunk too: see Getting around below — nothing in this app still describes the old ten/six-item lists.
The modules
Safety Officer — /safety
| Module | Path | What it is for |
|---|---|---|
| Review queue | /safety/review | Permits waiting for a decision |
| Review detail | /safety/review/:id | The decision screen for one permit |
| All permits | /safety/permits | Every permit, any status, any contractor |
| Permit detail | /safety/permits/:id | The same screen as review detail, reached from the full list |
| Risk map | /safety/risk-map | Live permits plotted on a facility plan, with attention markers |
| Facility plan | /safety/risk-map/plan | Name and upload plans, activate or deactivate them, and place pins |
| Audit log | /safety/audit-log | The append-only record — route unchanged, reached from Dashboard, not the nav list |
| Dashboard | /safety/dashboard | Site-wide overview, plus the Audit log action |
| Users | /safety/users, /safety/contractors, /safety/safety-officers, /safety/inspectors | Contractors · Safety officers · Inspectors, as three tabs (SafetyUsersPage.vue) over the same three routes |
| Getting started | /safety/getting-started | A static walkthrough for the officer — reached from the app bar, not the nav list |
Opening /safety with no path sends you to the review queue.
Since wayfinder 110 (safety 14d52ae1, 2026-09-12)
The officer menu shrank from ten items to five. Audit log is no longer a nav entry — it is now a button on the Dashboard. Contractors, Safety officers and Inspectors collapsed into one Users entry, three tabs over the same three routes (SafetyUsersPage.vue), so those routes still exist, just not as separate nav rows. Getting started moved to the app bar for both roles. Checked against src/composables/useNavItems.ts.
Removed in round 4: Areas (/safety/areas), Area visibility (/safety/areas/visibility) and Area drawing (/safety/areas/:id/drawing). Contractors no longer propose areas, so there is nothing to approve, and there are no per-contractor grants to manage — every contractor sees every pin. A place on the site is now a pin you place on a facility plan.
Two of those rows are the same screen twice. /safety/review/:id and /safety/permits/:id render the identical review-detail component; the only difference is where the back button returns you. There is no second permit screen to go looking for.
The three account modules are also one screen each, shared. Contractors, safety officers and inspectors all use the same list page and the same form page — the route says which role the account being created or edited will have. Since wayfinder 110 they are also one nav entry, Users, tabbed.
Inspector — /inspector
| Module | Path | What it is for |
|---|---|---|
| Scan | /inspector/scan | Scan a permit QR and see its live status |
| Visit | /inspector/visit | The site visit a scan just started |
| Entrant register | /inspector/entrant-register | Scan workers in and out of a permit you just scanned — route stays, no longer in the nav |
| Gas log | /inspector/gas-log | Readings taken during the work — route stays, no longer in the nav |
| Offline queue | /inspector/offline-queue | What has not reached the server yet |
| History | /inspector/history | Permits this inspector has scanned, and — since wayfinder 101 — can restart a visit on within the scan's window |
| Getting started | /inspector/getting-started | A static walkthrough for the inspector — reached from the app bar, not the nav list |
Opening /inspector with no path sends you to the scanner.
Getting around
Both halves share one navigation list, filtered by what your account may do — there is no role switch to find and no separate menu per role. An officer sees the officer entries, an inspector sees the inspector ones. On a screen narrower than 900px it renders as a bottom bar; wider, as a side drawer. Both show the identical set.
Today's lists, in the app's own labels — checked against src/composables/useNavItems.ts's NAV_ITEMS (wayfinder 110, safety 14d52ae1):
| Role | Menu |
|---|---|
| Safety officer | Review · Permits · Risk Map · Dashboard · Users |
| Inspector | Scan · Offline · History |
Review carries a live badge — the count of permits pending the officer's decision, fed by the socket service (wayfinder 109) and corrected by its /badges poll if the socket is down; the nav never knows or cares which transport delivered the number. Offline still carries its own pending count, unchanged since before round 4. Getting started (both roles) is in the app bar, not this list — DefaultLayout.vue.
Planned: a real notification bell
There is no persistent notification menu today — new items surface only as a transient toast, plus a one-off banner on the review queue for new submissions. A topbar bell/dropdown that lists notifications by type (distinguishing, for example, a close-request from an emergency/incident note) is planned but not yet built; the Notification record itself has no type field yet either.
Entrant register and Gas log lost their inspector nav entries, but their routes and pages are untouched: a visit's action menu still links out to both (InspectorGasLogPage is also the only producer of GAS_LOG offline-queue items, wayfinder 111, and deleting the page would have ended gas offline coverage — it was deliberately kept).
Two of the screens above are not navigation entries, because you reach them from their parent: the facility plan page (from the Risk map's Manage facility plan link) and the visit (only ever from a scan — see below).
Safety Officer — approving and rejecting
This is the module the system exists for.
Step by step
Open
/safety/review. The queue holds everything inPENDING. A badge on the page shows how many new-submission notifications are genuinely unread; tapping it marks them read and filters/highlights the queue down to just those — distinct from the plainPENDINGfilter, since aPENDINGpermit you already opened once still counts asPENDINGbut is no longer unread. (Ondevas of this writing, not yet merged/deployed — before this, the badge showed a count but did nothing when tapped.)Open a permit. At the top is the status banner. Directly below it, anything alarming right now is pinned above the tabs, so it needs no click: an
EMERGENCYorINCIDENTnote an inspector logged, a live overlap with another permit on the same pin, or the server's verdict that a Confined Space gas reading is overdue. When none of those holds, nothing is shown there.Everything else is in eleven tabs: Overview, Where & When (the pin by name and the plan it sits on, drawn on the plan image; the work window; the contractor's schedule note), Safety Readings, Workers & PPE (each worker's role on this permit and certificate, the health check on a Confined Space permit, and the PPE declared by the contractor), Job Safety Analysis, Closure & Fire Watch (just Closure on a permit type that has no Fire Watch), Overlapping Permits, Inspector Visits, Audit Trail, Report (since wayfinder 112 — a printable visits view and closure report, mirroring the contractor's own Report tab; full note and photo content, since safety already sees everything) and — appended last, read-only — Pre-Work Checklist: the contractor's own Step-3 Safety Checks answers (Yes/No/N-A, 13-17 items depending on permit type). Nobody in this app edits it; it is a different, separate thing from the Closure & Fire Watch tab's checklist, which the officer fills in. A tab with no data still shows an explicit empty state rather than disappearing. The QR sits beside the tabs on a wide screen.
Planned: a page-level Fire Watch/closure alert card
Today, Hot Work fire-watch and closure status is visible only inside the Closure & Fire Watch tab. A page-level alert card surfacing that status above the tabs — alongside the other urgent items — is planned but not yet built, so an officer does not have to open the tab to know a fire watch is running or a closure is pending.
A Print / Export PDF button sits above the tabs, not inside any of them — the document it produces covers every tab at once, formatted for A4 with an e-safework header/footer repeating on every printed page. Browser print (
window.print()), not a downloaded file from the server. (Ondevas of this writing, not yet merged/deployed, same as the Pre-Work Checklist tab.)Read the safety readings, where & when, and the overlapping-permits tab before deciding. Overlapping permits are the case the readings alone cannot tell you about: another crew already booked on the same pin, and the warning names the other permit, the colliding dates and daily hours, and the pin. This warning is advisory only — it never blocks Approve. Approving over an overlap is a decision you are making, not a rule the system enforces for you. Two limits worth knowing: a permit with no pin is shown as not checked (there was nothing to compare), and crews on two adjacent pins do not warn — a pin is a point, not a zone.
Press ✓ Approve & Issue QR or ✕ Reject Permit at the bottom.
- Approve asks for your signature, and nothing else.
- Reject asks for a reason and your signature. The reason goes back to the contractor.
Confirm. The server revalidates every safety threshold at this moment — your approval does not override a reading that is out of limits. The server's verdict is the one that counts.
The contractor never sees a rejection as a mystery: the rejecting officer's name is recoverable from the audit trail, and the reason you typed is shown to them.
Safety Officer — All permits
/safety/permits is the whole picture rather than only the queue: every permit, in any status, from any contractor. Use it to find a permit you are not being asked to decide on — one that is already active, or closed last month. Since wayfinder 098's safety half, it also carries a Close requested column and a server-side filter — the closest thing this app has to a request queue.
Opening one takes you to the same detail screen described above.
Safety Officer — Risk map
/safety/risk-map plots every live permit — PENDING, ACTIVE and FIRE_MONITOR — on a facility plan. The map itself is capped at 70% of the viewport height (max-h-[70vh]) so it fits the screen instead of pushing the legend and lists below it out of view — it used to size purely off the image's aspect ratio with no ceiling at all. (On dev as of this writing, not yet merged/deployed; so is the legend's new position and the pulse below.)
- Plan — when more than one plan is active, choose which one to draw. The choice is kept in the address (
?plan=), so a link opens the same plan. With no choice, the first active plan is drawn. - Status, not type. Each pin is coloured by its permit's status, with its own icon, and a legend card above the map (moved up from below it) names each status with a count. Colour is never the only signal. A pin already carrying an attention badge (below) now pulses gently — no other pin does, to keep a busy plant from turning into visual noise.
- Tap a pin for the permit's location, foreman and dates, and View to open it.
- Nothing live disappears. Beneath the map, every live permit that cannot be drawn on this plan is listed by name, in three groups: permits on another active plan (named — tap to switch to it), permits whose pin or plan has been deactivated or cannot be resolved, and permits not on any plan because no pin was recorded. A map that silently under-reports live work is its own hazard.
- It refreshes itself, visibly. The page re-fetches about once a minute and shows Updated Ns ago; Refresh fetches now. If an update arrives while you have a pin's summary open, the map is not redrawn under you — a note says new data is waiting, and Show now applies it. If a background refresh fails, what is on screen stays and an error above the map says the refresh failed.
- Attention markers (wayfinder 108 step 3, safety
65cb9060) — a pin whose permit has an overdue gas reading, a liveEMERGENCY/INCIDENTnote, or an overlap gets a corner badge (a danger-triangle icon, a distinct shape and position from the status-coloured circle — never colour alone, since status already spent the pin's one colour slot) plus an aria-label suffix, and the legend gains a count of how many pins are flagged. A marker never gates the completeness rule above: an unplaced or stale-pin permit carryingattentionflags still appears named in its list. Checked againstSafetyRiskMapPage.vue'shasAttention().
Not in the live flow yet
No facility plan is active in production, and when round 4 landed every existing plan was deactivated on purpose. Until you name a plan, activate it and place pins on it, the risk map has nothing to draw and every live permit is listed as unplaced.
Safety Officer — Facility plans and pins
/safety/risk-map/plan, reached from Manage facility plan on the risk map. A facility plan is now a named place — "Floor 1", "Tank Farm" — with an image that never changes. There is no version chain any more: a new scan of a place is a new plan, and you deactivate the old one yourself.
Adding a plan
- Upload new version, and give the plan a name that tells it apart from every other.
- Choose the image. Only PNG, JPEG and WebP are accepted — PDF and HEIC are refused on purpose, because the map renders the image directly and a badly converted plan becomes a permanent fixture of the safety map.
- If it is a photo of a printed plan taken at an angle, drag the four corners onto the plan's real corners to straighten it, or Skip — upload as-is if it is already square.
- Activate it. Several plans may be active at once; activating one never deactivates another.
Activating is the first of three steps that switch on a submit rule
Once there is an active pin on an active plan, every contractor must select a pin before they can submit (PERMIT_POSITION_REQUIRED). Activating a plan with no pins changes nothing for them. The activate dialog still asks you to confirm "the contractor app's position picker has shipped" — that picker has shipped (it is now a pin picker), so the question is out of date rather than a real precondition.
Placing and managing pins
Open a plan (View) to see it at a readable size, with its uploader, how long it has been active, and how many live permits sit on it.
- Place a pin by clicking the plan where it belongs, or with Add pin by typing its X and Y position (0–100, percentages of the image). Every pin needs a name — it is what the contractor picks from and what the overlap warning names.
- Rename a pin at any time. A name is a label.
- A pin cannot be moved. Its position is a claim about where work happened. To move one, place a new pin and deactivate the old one.
- Deactivate, never delete. A deactivated pin leaves the contractor's picker, but every permit that already references it keeps resolving it.
- Deactivating a plan tells you how many live permits sit on it first. Nothing is deleted; its pins and permits keep resolving by id, and they appear on the risk map under "not shown on this plan".
Contractors do not upload plans or place pins. Per-contractor drawings would put two crews on the same physical roof into unrelated coordinate spaces, and the overlap warning could then not see the collision. A contractor's own sketch attaches to the permit as a document with no coordinate system.
On the review screen, a permit's pin is always drawn on its own plan, and it is never hidden when that pin or plan has since been deactivated — it is flagged instead, so a caveated pin is information you can act on, not an error that swallows the position.
Safety Officer — Audit log
/safety/audit-log is the record of every decision, in order, hash-chained. There is no edit and no delete — not on this screen, not on any screen, not through the API. That is what makes it evidence.
A field action shows how it reached the permit: by scan, by manual entry, or — new in round 4 — by the system, which is what an entrant checked out automatically at closure reads as. That is kept distinct from a row with no recorded source at all. Rows for areas approved or rejected before Areas were removed still render with their labels; the log is append-only, so they are there for good.
Safety Officer — Accounts
/safety/users — one page, three tabs (Contractors, Safety officers, Inspectors), each its own route (/safety/contractors, /safety/safety-officers, /safety/inspectors still work directly, and keep the right tab highlighted — wayfinder 110, SafetyUsersPage.vue) over the same shared list-and-form pair as before:
- Open the Users entry, or a tab directly.
- Press new, or open an existing account.
- Fill the form and save.
Accounts are deactivated, never deleted. An account that has signed a permit has to keep existing for that signature to mean anything.
Safety Officer — closing a permit
Since 2026-09-11, closing a permit is yours. Only a safety officer can close one; the contractor and the inspector can only request closure. That reverses the earlier rule that closure was the foreman's act, with the officer's close as a backstop. As of wayfinder 098's safety half (9529d490, 2026-09-11) the screen says so — the "backstop" wording is gone.
What the screen does today:
- On an
ACTIVEorFIRE_MONITORpermit, the review screen offers a plain Close permit button — or, when a request is standing, Approve close request, prefilled with the request's reason (editable). Both ask for a reason (always required) and your signature; both are the samePOST /:id/close. Checked againstofficerCloseButton/approveCloseRequestButtoninsrc/locales/en.ts. - A Hot Work permit can close only from
FIRE_MONITOR, after the 30-minute Fire Watch has elapsed. No override. - Closing a Confined Space permit with people still checked in is allowed. Entrants still inside are counted and you are told they will be checked out automatically, with system provenance, before you confirm. You no longer have to get the register to zero first — but your reason is on the record, so say why.
Closure requests are now visible, not just notified. A CloseRequestBanner.vue sits above the tabs beside the urgent strip — who requested, their role, when, and why. Beyond the notification and the audit trail's Close Requested label, a "Close requested" chip appears on PermitCard (so it shows in the review queue and anywhere else permits are cards) and All permits (/safety/permits) has both a column and a server-side filter, Close requested: any / Close requested / Not requested (?closeRequested=, never client-filtered). Checked against CloseRequestBanner.vue, SafetyAllPermitsPage.vue and PermitCard.vue.
Safety Officer — Dashboard
/safety/dashboard — the site-wide overview.
Inspector — the scan-first flow
The inspector's app hangs off the scanner. You do not navigate to a permit; you scan it and the app takes you where you need to be. A scan is what proves you are physically at the permit.
Step by step
- Open
/inspector/scan— it is where the app lands. - Scan the permit's QR code. If the camera cannot read it, type the permit id in by hand; the app treats a typed id and a scanned one identically, and records which it was.
- The result shows the permit's live status and a single Start Visit action — nothing else. (Older versions of this page also showed Entrant Register → and Gas Log → shortcuts here unconditionally, regardless of the permit's type or whether a visit had even started; those were removed — every type-relevant action now lives only on the visit page below, where it is correctly gated. On
devas of this writing, not yet merged/deployed.) If the permit's status isn'tACTIVE/FIRE_MONITOR, Start Visit is disabled with a short explanation instead of failing only after you tap it. - Start Visit creates the visit record — one visit per trip to the permit, not one for the permit's whole life. The permit shows a timeline of every past visit on its review screen.
- Work the action menu — see below — in whatever order the site demands.
- Submit Visit once. A submitted visit cannot be edited; if you got something wrong, file another visit.
Only ACTIVE and FIRE_MONITOR permits can be inspected. A CLOSED or EXPIRED permit still scans — the lookup exists to tell you it is closed — but no visit, check-in, gas reading or closure request is accepted on it. The server enforces this, not the app.
The visit menu
Round 4 replaced the fixed sequence of steps with a menu: four items, or five on a Confined Space permit. Each shows its own state — Not done, Done this visit or Nothing pending — each with its own distinct color (warning/success/neutral), not the uniform grey every item used to render regardless of state (on dev as of this writing, not yet merged/deployed) — and only one blocks submitting.
| Item | What it is | Blocks submit? |
|---|---|---|
| Check-in / Check-out | The permit's worker table: each worker's status (Inside, Outside, Not available) with Check In, Check Out and Not Available buttons | Yes — but opening the table is enough, even with no change |
| PPE Check | The PPE the contractor declared, each marked Worn or Not worn; plus flagging anything they did not declare as missing | No |
| Gas Reading | Confined Space only. Whether a reading is due — the server's verdict — with Log a Reading → to the gas log | No, even when overdue |
| Note | Typed notes and photos | No |
| Request Close | Ask safety to close the permit, with an optional reason | No |
Why only one item blocks, and why that one is satisfied by looking: the gas clock is now the server's — the 2-hour sweep notifies every inspector and the officer, and escalates to the officer and the foreman, whatever any visit records — so a skipped gas item no longer means a missed alarm. The entrant check exists so an inspector actually looks at who is inside; making it require a change would teach people to check workers out early just to finish.
There is no fire-watch step any more. On a Hot Work permit in FIRE_MONITOR the server's Fire Watch countdown is shown above the menu, for information only.
Check-in / Check-out, and "not available"
The table lists every worker on the permit. Check In and Check Out record an entrant event; the certificate is re-checked, and a worker whose certificate has lapsed is refused with no field override (and a CERT_BLOCKED row on the audit log). With no connection, a check-in or check-out is queued and syncs later (see Offline queue); Not Available cannot be.
Not Available is for a worker who is on the permit but not on site — didn't show, sent home, unfit after the health check, reassigned. It asks why, and records it against that worker on this permit — not as an entry or exit, because the register answers "who is inside" and an absence is not an entry. Someone who turns up late can still be checked in, which clears it.
PPE Check
You check only what the contractor declared on the permit — each item Worn or Not worn, never pre-ticked. Below that you can flag an undeclared item as missing: something the job needed that the contractor did not declare at all. That flag is the point of the check. A flagged item needs a Corrective action, Emergency or Incident note on the same visit before you can submit, so it reaches the contractor. Visits recorded before round 4 show their old checklist as an earlier format, unchanged.
Notes — and who reads them
Every note carries a type: General, Warning, Corrective action (the record of "I told them to fix X"), Emergency and Incident. Who is told depends on the type:
| Type | Notifies |
|---|---|
| Emergency, Incident | every safety officer and the contractor who owns the permit; also pinned above the tabs on the officer's review screen |
| Corrective action | the contractor who owns the permit |
| General, Warning | nobody — they are read in the visit timeline |
None of them changes the permit's status. Your notes are readable by the permit's contractor. Since round 4 the contractor may read the full visit record — notes and photos — on their own permits. Write accordingly.
Request Close
If the work looks finished, Request Close tells safety, with an optional reason. It does not close anything — only a safety officer can. If a request already stands, the item shows Nothing pending and says closure is already awaiting safety.
Entrant register
/inspector/entrant-register — scan a worker's badge, choose Entering or Exiting, or pick the worker from the permit's list when the camera will not cooperate. It works on the permit you last scanned. A lapsed certificate is an unmistakable ENTRY DENIED, with no override.
The count no longer has to reach zero before a permit can close. Closing a Confined Space permit with people inside now succeeds and checks them out automatically, on the record; the old ENTRANTS_STILL_INSIDE refusal is retired.
Gas log
Readings taken during the work, against the same thresholds the permit was approved under. The colour on each row is an on-device guide only; the "overdue" verdict is the server's own answer from this same log, never recomputed on the phone.
Offline queue
Site work happens where the signal does not. A check-in or check-out — from the Entrant register screen or from a visit's worker table — and a gas reading from the Gas log screen are held on the phone when the connection drops, marked queued, never saved, until they are sent. A queued check-in shows a pending badge on that worker's row.
Three things cannot be queued, because the server has no way to replay them: Not Available, the PPE Check, and Submit Visit itself. If the connection drops on one of those, the app says plainly that it was not saved — do it again once you have signal. (Ticket 111, 2026-09-11.)
- Open
/inspector/offline-queue— or watch its badge from anywhere in the app. The nav entry carries a live count of what is still pending. - Everything listed, with its type, permit, time and status, has not reached the server yet.
- The queue replays by itself when the connection comes back while the app is open; Sync Now sends it immediately.
- Read the result: how many synced, and how many conflicted. A conflict means the server had already moved on — the permit closed, say, while your phone held an entry against it — and the entry shows the server's reason in plain language. Conflicted entries stay listed until you dismiss them; dismissing removes the entry, it does not send it.
- An empty queue with a zero badge means everything you recorded has landed.
Sync before leaving site. A reading still sitting in the queue exists only on that phone. Two limits the screen itself now states:
- It covers a blip, not a shift. There is no offline app shell, so the app will not load at all with no signal.
- A queued entry is recorded at the time it syncs, not the time it happened. An exit taken at 10:00 and synced at 10:40 reads as 10:40 on the register — and a late-synced gas reading moves the overdue clock. This is a known defect (ticket 126, open), so sync as soon as the signal is back.
History
/inspector/history — the permits you have scanned recently, so you can reopen one without hunting down the QR code again. An entry clears after 7 days regardless of status — it used to also drop the moment a permit went EXPIRED/CLOSED/REJECTED (a deliberate ticket-016 rule), which is what got that changed: an inspector wants to look back at a permit they scanned even after it closed, not just while it is still open. (On dev as of this writing, not yet merged/deployed.) View live status re-fetches the permit's status; it does not replay the status as it was when you scanned it.
Since wayfinder 101's safety half (488676ad, 2026-09-11), a history entry can start a visit.Start Visit appears on an entry, enabled, whenever the server says canStartFromHistory === true — which as of 2026-09-13 (owner decision, on dev, not yet merged/deployed) means simply "you have scanned this permit before, ever" and the permit's status still allows a visit (ACTIVE/FIRE_MONITOR). The original 12-hour/work-window time bound is gone — it kept forcing a re-scan on permits that were still perfectly active, which is exactly what got this changed. Starting one seeds the same in-memory visit session a fresh scan does, recorded with source: 'history', distinct from a camera or manual scan. The button is disabled, with an explanation, only when the permit's status itself rules a visit out — never any more because of how long ago you scanned it. History retention itself also changed the same day: it used to drop EXPIRED/CLOSED/REJECTED permits from the list entirely (a deliberate ticket-016 rule); every status now stays, still capped at 7 days of age. Checked against ScanHistoryList.vue.
Signing in
/auth/login, and /auth/reset-password.
Signing into this app currently signs you out of the Contractor app in the same browser. The server-side fix has shipped; the infrastructure change that completes it has not. See Open items.