Research & Evidence
This page is the audit trail behind the rest of the guide. Every feature, screen, permission, table, and automated job described elsewhere in this documentation was verified against the live module code under modules/Patrol/ before it was written up. Below is the inventory of the actual files that were read, with short notes so a reviewer, administrator, or auditor can trace any claim in the guide straight back to a real file — and confirm the guide does not describe anything the module does not actually do.
Why this page exists. Security documentation is only useful if it is truthful. Rather than ask you to take the guide on trust, this page names the source files behind each section. If you administer or audit this deployment, you can open the same files and confirm every statement. If something in another page ever seems to disagree with the running system, the code is the source of truth — verify against these files.
How this guide was verified
The guide was written after reading the module's real source, not from a specification or a wishlist. The evidence was gathered in this order:
- Read the manifest (
module.json) and the six configuration sidecars to establish the module's identity, menu, permissions, routes, and audit registrations. - Read all fifteen controllers to confirm each feature area's screens, actions, and permission gates.
- Read the models — especially the shared integrity base — to confirm reference numbering, versioning, the tamper-evident hash chain, and privacy behaviour.
- Read every migration to confirm the database tables actually exist, and cross-checked the count against
module.json'sdatabase.tableslist. - Read the six cron job classes to confirm each schedule and what each job actually does.
- Confirmed the one place Patrol touches ERPat core (a single notifications migration) and the helpers it relies on.
Result: 15 controllers, 37 models, 12 module migrations plus 1 core migration, 6 cron jobs, 6 config sidecars, and 2 helpers — all present and accounted for.
Module identity
The module announces itself through its manifest and module_config.php. These values decide how it appears in Settings → Manage Modules and which enable-toggle gates the whole feature.
| Fact | Value |
|---|---|
| Manifest slug | patrol |
| Version | 2.0.0 |
| Category | Security |
| Icon | fa-shield |
| Namespace | Modules\Patrol |
| Enable toggle (setting) | module_patrol |
| Declared tables | 39 patrol_* tables (matches the migrations) |
| Migration tracking table | migrations_patrol (run by php erpat migrate:modules) |
Controllers verified
Each Patrol screen is backed by one controller. All staff controllers extend App_Controller and gate themselves on the module toggle plus an area permission; the single public controller extends App\Core\GuestController and carries no session. The table below is the evidence for the feature areas documented across Daily Operations and Data & Screen Reference.
| File | Area permission | Covers |
|---|---|---|
Patrol.php | patrol_dashboard | Security dashboard landing page |
Patrol_logbook.php | patrol_log | Digital activity log / blotter (EVT-…) |
Patrol_visitors.php | patrol_visitor | Check-in/out, invites (QR), passes (VIS-…) |
Patrol_incidents.php | patrol_incident | Incident register, evidence, custody, people, tasks (INC-…) |
Patrol_shifts.php | patrol_shift | Shifts, roster, guard licenses, "On Duty Now" |
Patrol_handovers.php | patrol_handover | Shift endorsements (HND-…) |
Patrol_routes.php | patrol_route | Routes & checkpoints (scan codes + token hashes) |
Patrol_rounds.php | patrol_run | Patrol run execution & observations (RUN-…) |
Patrol_assets.php | patrol_asset | Equipment register, issue & return |
Patrol_reports.php | patrol_report_export | Activity CSV & incident-register PDF exports |
Patrol_settings.php | patrol_location | Locations admin: sites, zones, gates, posts |
Patrol_public_reports.php | patrol_public_report_triage | Staff triage of anonymous public reports (PSR-…) |
Patrol_governance.php | patrol_retention | Retention, legal holds, DSAR, disclosures |
Patrol_ai.php | patrol_ai | Human-in-the-loop AI assist (ships disabled) |
Patrol_report.php | public — no login | Anonymous public reporting & tracking form |
Models verified
Thirty-seven models sit under modules/Patrol/models/. The most important one is the shared integrity base every trustworthy record inherits from.
The integrity base
Patrol_base_model.php is where reference numbering, append-only versioning, the tamper-evident hash chain, and offline idempotency actually live. The activity-log, incident, and handover models extend it — so the guide's "you can't secretly edit a record" claims are grounded here, not asserted.
One model per record type
Visits, invites, passes, evidence, custody, incident people/actions/tasks/links, guards, licenses, shifts, assignments, routes, checkpoints, runs, scans, observations, assets, public reports, retention policies, legal holds, privacy requests, disclosures, and AI jobs/outputs each have a dedicated model.
No forked queries
The Reports controller and the AI feature reuse the activity and incident models rather than writing their own SQL, so exports and analysis always see the same records as the live screens.
Database tables & migrations
The module ships twelve idempotent, reversible migrations, tracked in migrations_patrol. Together they create the 39 patrol_* tables the manifest declares. This table is the evidence that every screen and data field mentioned in Data & Screen Reference is backed by a real table.
| Migration | Tables created |
|---|---|
…114114_create_patrol_location_tables | patrol_sites, patrol_zones, patrol_gates, patrol_posts (seeds "Main Site" / MAIN) |
…114121_create_patrol_activity_log_table | patrol_activity_logs |
…114128_create_patrol_visitor_tables | patrol_visits, patrol_visit_events, patrol_visitor_invites, patrol_visitor_passes |
…114135_create_patrol_incident_tables | patrol_incidents, patrol_incident_people, patrol_incident_actions, patrol_incident_links |
…114142_create_patrol_evidence_tables | patrol_evidence, patrol_evidence_custody |
…130811_create_patrol_shift_tables | patrol_guards, patrol_guard_licenses, patrol_shifts, patrol_shift_assignments, patrol_handovers, patrol_handover_items |
…133734_create_patrol_route_tables | patrol_routes, patrol_checkpoints, patrol_route_schedules, patrol_runs, patrol_scans, patrol_observations, patrol_devices |
…135651_create_patrol_asset_tables | patrol_assets, patrol_asset_assignments |
…140853_create_patrol_incident_task_table | patrol_incident_tasks |
…143355_create_patrol_public_report_tables | patrol_public_reports, patrol_public_report_messages |
…144453_create_patrol_governance_tables | patrol_retention_policies, patrol_legal_holds, patrol_privacy_requests, patrol_disclosures |
…145803_create_patrol_ai_tables | patrol_ai_jobs, patrol_ai_outputs |
Schema-present but no dedicated screen yet. Three tables ship in the schema but have no dedicated management screen in this version: patrol_route_schedules and patrol_devices (no UI controller), and patrol_incident_links (written by the escalate/link flow, not a standalone editor). The guide does not document screens for these — because there are none.
Automated jobs verified
Six background jobs live under modules/Patrol/jobs/ (namespace Modules\Patrol\Jobs). Each runs per-tenant, is enabled by default, and self-gates on module_patrol — so switching the module off silently stops all of them. These are the evidence for every "the system does this for you overnight / on a timer" claim in the guide.
| Job class | Runs | What it does |
|---|---|---|
EscalateIncidentsJob | every 5 min | Escalates unacknowledged critical, still-open incidents older than patrol_escalation_minutes (15); notifies via patrol_incident_escalated. |
PatrolMissedRoundsJob | every 15 min | Flags in-progress runs overdue past the route's estimate plus patrol_round_grace_minutes (15). |
PatrolAssetOverdueJob | hourly | Flags issued equipment past its expected return time. |
PatrolChainVerifyJob | daily 02:30 | Re-walks the hash chains of blotter, incidents, and handovers; raises a critical alert on any break. |
PatrolLicenseExpiryJob | daily 03:00 | Flags guard licenses expiring within patrol_license_expiry_days (30). |
PatrolRetentionSweepJob | daily 04:00 | Applies active retention policies (anonymise / soft-delete / purge) with legal-hold, evidence, and disclosure guards; writes a deletion certificate. |
Configuration sidecars verified
The module carries six configuration files under modules/Patrol/config/. These are what make it appear in the sidebar, the Roles editor, and the audit log without touching ERPat core.
| File | Role |
|---|---|
module_config.php | Manage-Modules registration; module_key = patrol |
menu.php | The single "Patrol" sidebar group and its 14 sub-items, each permission-gated |
default_menu.php | The module's slice of the default left menu |
permissions.php | All Patrol permission keys, injected into the Roles editor |
routes.php | Canonical and public (patrol-report/…) routes |
system_logs.php | Audit component/action registrations (category "Security") |
What the module deliberately does not ship. There is no dashboard-widget config, no CMS public-page sync config, no profile-tab config, and no REST / end-user API sidecar. The guide therefore documents none of those surfaces — because the module does not expose them. Patrol's only public surface is the anonymous reporting controller below.
Public & staff-triage surface
Patrol's only unauthenticated surface is the public reporting form, confirmed in config/routes.php and controllers/Patrol_report.php. It resolves the tenant from the URL the same way the Kiosk and Cron endpoints do.
| Route | Purpose |
|---|---|
patrol-report · patrol-report/{company_key} | Show the public report form |
patrol-report/submit | Submit a report; shows the one-time tracking code |
patrol-report/track/{company_key}[/{code}] | PII-free status lookup |
patrol-report/message/{company_key}/{code} | Reporter adds a message |
The staff side of public reporting (Patrol_public_reports.php) is a normal authenticated screen gated by patrol_public_report_triage; converting a report to an incident additionally requires patrol_incident_create. The tracking token is shown to the reporter once and only its SHA-256 is stored, so even staff cannot recover it.
Tamper-evidence primitives
The guide's strongest claims — reference numbers, no silent edits, a tamper-evident chain, and offline-safe writes — all trace to models/Patrol_base_model.php. Reading that one file confirms the behaviour rather than trusting the prose.
| Claim in the guide | Evidence |
|---|---|
| Every record gets a reference number | _generate_reference() mints PREFIX-YYYY-#### at version 1 |
| You can't secretly edit a submitted record | amend() flips the old row to superseded and inserts a new version; content and hash stay byte-intact |
| Records are tamper-evident | _compute_hash() = SHA-256 over key-sorted immutable content plus the previous hash, bucketed per site and day |
| Works offline without duplicating | get_by_client_id() makes a replayed write with a known client_generated_id a no-op |
| Nightly integrity check | PatrolChainVerifyJob re-walks those chains and raises a critical alert on any break |
Settings & toggles verified
These settings were confirmed by reading how the controllers and jobs call get_setting(). Note which are seeded and which simply fall back to a hard-coded default.
| Setting | Default | Effect |
|---|---|---|
module_patrol | — | Master on/off; gates the module and every cron job |
module_patrol_ai | Off (not seeded) | AI assist on/off; off shows the "disabled" page |
patrol_ai_dpa | Off (not seeded) | One-time data-privacy acknowledgement, admin-only |
patrol_escalation_minutes | 15 | How long a critical incident may sit unacknowledged |
patrol_license_expiry_days | 30 | Advance warning window for guard licenses |
patrol_round_grace_minutes | 15 | Grace before a patrol round is "missed" |
Core subsystems reused & the single core touch
Patrol is designed to reuse ERPat's existing platform rather than rebuild it. Verified reuse points:
- File storage — evidence and photos go through
general_files/ S3, not a bespoke store. - Audit — every mutation calls
set_system_logs(); components are registered inconfig/system_logs.php. - Attendance — "On Duty Now" reads the core
attendancetable (open clock-in records); Patrol never rebuilds clock-in. - Notifications — incident alerts ride the core notifications system.
The one file Patrol adds to ERPat core. Incident alerts require a single core migration, application/migrations/20260711121838_add_patrol_incident_id_to_notifications.php, run by php erpat migrate:latest. It adds a link column to notifications and seeds two notification settings (patrol_incident_raised, patrol_incident_escalated) with no default recipients. Until an administrator assigns recipients in Settings → Notifications, no one is notified — this is deliberate, to avoid alert spam. See Administration.
Documentation-state caveats
Honesty note for reviewers: the module's own bundled metadata is behind the code, and this guide follows the code.
The module's README, CHANGELOG, and its original single-page docs/index.html describe only "Phase 1" (blotter, visitors, incidents, evidence, and the escalation job) and list shifts & handover, rounds, public reporting, governance / DSAR, and AI assist as "planned." The shipped code implements all of those. Those bundled files are stale and understate the module. This multi-page guide reflects what the code actually does today, verified against the files listed on this page.
Feature → evidence map
A final cross-check: each documented feature area maps to a controller, an area permission, its tables, and (where relevant) a job. If you can open the controller and the tables, the feature is real.
| Feature area | Controller | Permission | Timed job |
|---|---|---|---|
| Blotter / activity log | Patrol_logbook | patrol_log | chain verify |
| Visitor management | Patrol_visitors | patrol_visitor | — |
| Incidents & evidence | Patrol_incidents | patrol_incident | escalate |
| Shifts & handover | Patrol_shifts, Patrol_handovers | patrol_shift, patrol_handover | license expiry |
| Routes & rounds | Patrol_routes, Patrol_rounds | patrol_route, patrol_run | missed rounds |
| Assets & equipment | Patrol_assets | patrol_asset | asset overdue |
| Reports & exports | Patrol_reports | patrol_report_export | — |
| Locations admin | Patrol_settings | patrol_location | — |
| Public reporting | Patrol_report, Patrol_public_reports | public / patrol_public_report_triage | — |
| Governance & retention | Patrol_governance | patrol_retention | retention sweep |
| AI assist | Patrol_ai | patrol_ai | — |
Where to go next
This page proves the module is real; the rest of the guide explains how to use it.
Field-by-field detail for every screen, status, and reference-number prefix confirmed above. ⚙ Administration
Grant the permissions listed here, wire up notification recipients, and configure retention. ? FAQ & Troubleshooting
Answers to common questions, including why the module's bundled README looks smaller than what you see.