Ownware
Home›Permora›Manual
Permora · Manual

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]:

StateMeaning
noneNo expiry date set, or the stored value is not a date
expiredExpiry is before today
criticalZero to critical days away — "expires today" is critical
warningBetween critical and warning
noticeBetween warning and notice
okFurther 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].

Permissionviewercoordinatoradmin
.view permissions, report.viewyesyesyes
audit.viewyesyesyes
job.*, permit.*, inspection.*, attachment.*—yesyes
view.save—yesyes
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].

← Back to Permora · Quickstart · API · Test run

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →