Permora Manual, as shipped in the download
Permora — User Manual
Construction permits and the inspections they require, tracked on your own server. Version 3.1.4 [src: app/controllers/api.php:17].
About this manual
Every statement here was written by reading Permora's own source, and each one carries the file it came from in a bracketed src marker. Paths are relative to the folder this docs directory sits in. If a sentence disagrees with the application, the application is right and this manual has a bug — the markers exist so you can check rather than take our word for it.
Nothing is described that the code does not do. Where something is deliberately limited, that is said plainly rather than left out.
What Permora is, and what it is not
It tracks permits the contractor has already pulled and the inspections those permits require.
The source states the boundary in one sentence, and it is worth quoting because it removes four separate misunderstandings at once: Permora does not file permits with any authority, collect or move money, sign anything, or give legal advice. It is a record-keeping and reminder tool only [src: app/src/Permits.php:16].
Nothing here is legal advice, and nothing in the product substitutes for your authority having jurisdiction.
The date maths, and why it is the core value
The source calls portable date arithmetic the core value of the product [src: app/src/Permits.php:8], and two decisions make it trustworthy.
Both endpoints are pinned to midnight UTC, so the difference is always a whole number of 24-hour days. A daylight-saving transition in your business timezone cannot make a "days until" figure off by one, and there is no fractional-day rounding to get wrong [src: app/src/Permits.php:126].
The database is never asked for "now" — today flows in from the application in your business's timezone [src: app/src/Permits.php:10].
Together those mean a permit that expires in seven days says seven, in March and in October, on MySQL and on SQLite.
Expiry states
A permit's expiry is classified against three configurable windows — notice, warning and critical, defaulting to 60, 30 and 7 days [src: app/src/Permits.php:151]:
| State | Meaning |
|---|---|
none | No expiry date set, or the stored value is not a date |
expired | Expiry is before today |
critical | Zero to critical days away — "expires today" is critical |
warning | Between critical and warning |
notice | Between warning and notice |
ok | Further out than notice |
A permit expiring today is still valid through the day [src: app/src/Permits.php:141] — zero days remaining, not expired.
A mis-set settings row cannot invert the buckets
The three thresholds are clamped into a sane non-negative descending order before they are used [src: app/src/Permits.php:167], so entering a warning window smaller than the critical one cannot produce nonsense: the values are pulled back into order rather than obeyed literally.
Inspections
An inspection is overdue only when it is still pending and its scheduled date is strictly before today [src: app/src/Permits.php:195].
Two consequences follow, and both are deliberate:
- An inspection scheduled for today is "due today", not overdue [src: app/src/Permits.php:196].
- An inspection that has a result — pass, fail or partial — is never overdue, regardless of when it was scheduled [src: app/src/Permits.php:197].
That second one matters: a failed inspection from last week is not an outstanding task, it is a completed inspection with a bad outcome. Conflating the two would fill an overdue list with work that has already happened.
An inspection is "upcoming" when it is pending with a scheduled date on or after today [src: app/src/Permits.php:208], which is what "this job has a next inspection scheduled" means.
Money
Permit fees are integer cents, summed in PHP with a final clamp and never in SQL — because a SQL SUM() over many integer rows can overflow under MySQL's strict mode [src: app/src/Permits.php:11].
Values are clamped to a bound that fits a MySQL integer with headroom [src: app/src/Permits.php:24].
Exporting
The CSV export carries a numeric-aware formula-injection guard [src: app/src/Permits.php:13], so a cell that would execute in a spreadsheet is defused while real numbers stay numbers.
Users and roles
Three roles, and note that the middle one is named for the job rather than generically: viewer, coordinator, admin [src: app/src/Auth.php:57].
A coordinator runs the work — schedules inspections, records results, attaches permit documents and inspection photographs — and cannot manage users or change settings [src: app/src/Auth.php:45].
| Permission | viewer | coordinator | admin |
|---|---|---|---|
.view permissions, report.view | yes | yes | yes |
audit.view | yes | yes | yes |
job.*, permit.*, inspection.*, attachment.* | — | yes | yes |
view.save | — | yes | yes |
| users, settings | — | — | yes |
Why settings are admin-only
The source gives a specific reason rather than a generic one, and it is the best justification for a settings lock in the catalogue [src: app/src/Auth.php:50]:
The notice, warning and critical day windows live there, and those three numbers decide which permits the dashboard calls "expiring". Moving them changes what every screen in the product claims is urgent, which is an owner's decision rather than a scheduling one.
The audit trail is granted to a viewer explicitly — audit.view is named in the viewer's list rather than arriving through the wildcard [src: app/src/Auth.php:58]. A deliberate decision, not an accident of pattern matching.
The API and agent access
Seven tools are exposed to an agent, of which three write [src: app/controllers/v3.php:258]: listing permits, permit detail, upcoming inspections and an expiry report on the read side; creating a permit, scheduling an inspection and recording an inspection result on the write side.
⚠️ The agent endpoint reports an older version than the product
The REST interface, its OpenAPI document and the health check all report 3.1.1 [src: app/controllers/api.php:17], [src: app/controllers/api.php:223], [src: app/controllers/api.php:354].
The agent endpoint reports 3.0.0 [src: app/controllers/v3.php:257], and so does its transport probe [src: app/controllers/v3.php:517].
Nothing behaves differently because of it — the tools are the current ones — but an agent or a monitoring system reading the server version will be told the wrong number. If you are matching on it, match on the REST value. This has been raised against the product.
A bug the source records, worth reading if you build integrations
The specification key is app, not name, and the source explains what spelling it name cost: an undefined-array-key warning on every initialise, which with display errors on flushed output before the core could set its headers, and shipped a nameless server to the client [src: app/controllers/v3.php:251].
The lesson recorded alongside it is the useful part: the unit suite missed it because it built its own correct specification by hand instead of calling this function [src: app/controllers/v3.php:254].
A test that constructs its own input tests the code it wrote, not the code that ships.
What Permora does not do
It does not file a permit with anybody [src: app/src/Permits.php:16], collect or move money, sign anything, or give legal advice.
It does not treat a completed inspection as outstanding [src: app/src/Permits.php:197].
It does not lose or gain a day across a clock change [src: app/src/Permits.php:126].
It does not obey an inverted settings row [src: app/src/Permits.php:167].
It does not sum money in SQL [src: app/src/Permits.php:11].