Home›Inspectora›Manual
Inspectora · Manual

Inspectora Manual

Inspectora — User Manual

Inspections, calibration and logbooks: every asset you inspect, test or calibrate, the checks it is subject to, when each was last done and when it next falls due. Release 4.1.3 [src: app/src/Domain.php:22].

About this manual

Every statement here was written by reading Inspectora'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.

Nothing is described that the code does not do. Where something is deliberately limited, or currently wrong, that is said plainly rather than left out.

What you are keeping

Six things, and the source names each one in a sentence [src: app/src/Domain.php:61]:

Assetseverything you inspect, test or calibrate — extinguishers, outlets, meters, vehicles [src: app/src/Domain.php:64]
Inspection typesthe checks an asset is subject to, and how long each one stays good for [src: app/src/Domain.php:69]
Logbookwhat was checked, when, and when it is next due [src: app/src/Domain.php:74]
Schedulesa planned round of inspections; marking one done writes the asset's record [src: app/src/Domain.php:79]
Resultswhich assets were on a round and which were actually done [src: app/src/Domain.php:85]
Evidencethe certificate, test sheet or photo behind a record [src: app/src/Domain.php:89]

Note the distinction the last two draw. A schedule says what was planned; results say what was actually done. A round with ten assets on it and six results is not six-tenths of a record — it is six records and four assets still due.

What the version number means

Inspectora reports 3.1.0 on every surface, from a single constant [src: app/src/Domain.php:22], so the ping, the OpenAPI document, the health check, the agent endpoint and the stamp inside a backup cannot drift apart.

The source is careful about what that number claims: it is the platform release the product ships on — the same number every product in the catalog carries, so a buyer comparing two of them is comparing the same platform [src: app/src/Domain.php:18]. It is not a claim that Inspectora had a 1.x; this is its first release.

⭐ The grid, and the five states

Every cell is one asset against one inspection type, and it holds one of five states [src: app/src/Matrix.php:31]: valid, expiring, expired, none — meaning that asset has never had that check recorded at all — or failed, meaning the latest record says the check failed [src: app/src/Matrix.php:38].

They come back in the order a person should read them, worst first [src: app/src/Matrix.php:155]. Executed:

failed , expired , none , expiring , valid

A failed check heads the list because it is known to be wrong today; an expired one may simply not have been redone yet. "Never recorded" ranking above "expiring" is the right priority for a logbook: an extinguisher with no service history at all is a bigger problem than one whose certificate lapses next month.

⭐ The day a certificate runs out, it is still valid

The engine is emphatic about this. A calibration certificate that says "valid to 31 December" is valid on 31 December, and treating that day as expired is the kind of off-by-one that makes an operator distrust the whole grid [src: app/src/Matrix.php:109].

Executed, with today at 2026-09-05 and a warning window of thirty days:

next due 2026-12-31  ->  valid      valid, 117d left
next due 2026-10-05  ->  expiring   in 30d
next due 2026-10-04  ->  expiring   in 29d
next due 2026-09-05  ->  expiring   expires today      ← the due day itself
next due 2026-09-04  ->  expired    1d overdue
next due 2026-08-01  ->  expired    35d overdue

Three further behaviors, all executed:

A record with no expiry is valid forever [src: app/src/Matrix.php:109] — an inspection type that does not lapse comes back valid rather than looking like missing data.

An asset with no record at all is none, phrased "never recorded" [src: app/src/Matrix.php:130]. That is a different fact from an overdue one, and it is sorted and colored differently.

A warning window of zero narrows "expiring" to the due day alone: 2026-09-05 is expiring, 2026-09-06 is valid.

The wording never invents a number it was not given — where no day count is available you get a bare "expired" or "expiring" rather than a fabricated figure [src: app/src/Matrix.php:141].

The engine does not know what it is inspecting

Matrix.php knows that a subject holds a requirement, that holding it has a date and may expire, that rounds are planned events producing outcomes, and that evidence attaches to a record. It does not know whether a subject is a person or a fire extinguisher [src: app/src/Matrix.php:14].

Every user-facing word and every table name arrives from Domain [src: app/src/Matrix.php:15] — here, assets, inspection types, schedules and results [src: app/src/Domain.php:31]. Even the URL segments come from there, so /assets reads like the domain while the router itself stays generic, and each segment is whitelisted before it reaches a route pattern [src: app/src/Domain.php:41].

If you are customizing: add the word to Domain and read it from the view. A hardcoded "inspection" in a template is exactly how the vocabulary drifts [src: app/src/Domain.php:13].

The engine is shared with another product, and the suite enforces it

The source declares a canonical copy at tools/matrix-engine/Matrix.php, and says both products' suites md5-compare their copy against it so that editing one turns both suites red [src: app/src/Matrix.php:5]. Verified independently:

7cdb042d4b4bffb17328d182ee31b57b  tools/matrix-engine/Matrix.php
7cdb042d4b4bffb17328d182ee31b57b  44-inspectora/app/src/Matrix.php

The reason given is worth having: two codebases for one engine is an expensive mistake, and the hash check is the tripwire against making it by accident [src: app/src/Matrix.php:7].

SQL safety

Table names are the only identifiers interpolated into a query; everything else is a bound parameter [src: app/src/Matrix.php:17]. Those names come from a hardcoded map, never from input, and are whitelisted again before they reach a query, so a bad value fails in one place rather than producing a query nobody reads [src: app/src/Matrix.php:56].

API keys are shaped so a mistake fails cleanly

Every key carries a per-product prefix — ink here [src: app/src/Domain.php:95]. The reasoning is stated: a key pasted into the wrong installation is rejected by its shape rather than by a lookup that leaks whether it exists [src: app/src/Domain.php:93].

Users and roles

Three roles [src: app/controllers/v3.php:24]. A viewer reads the grid and the reports; a member can also record an inspection and run a scheduled round [src: app/controllers/v3.php:25]; admin holds everything [src: app/controllers/v3.php:26].

Backups are for administrators

Both backup downloads ask for the administrator role — the JSON export and the SQLite file [src: app/controllers/api.php, backup_json() and backup_sqlite()] — and so do the restore page and the scheduled-backup token. A viewer or member who opens either address is refused, and the Security page shows the backup buttons to administrators only [src: app/views/security.php].

A backup contains every table: the full inspection history of every asset, including which checks were overdue and for how long. The JSON export redacts credential hashes and the SMTP password; the SQLite file is the database itself, byte for byte. Before 3.1.3 a sign-in alone was enough to download either.

Webhooks: one event, fired from every door

The one event offered for subscription is record.created [src: app/src/Webhook.php], and it fires from every path that writes an inspection record: the board, a round being marked, the REST API and the agent tool. Before 3.1.3 the settings screen offered three leave-management events this product never fires, and record.created could not be ticked.

Reminders and the digest

Two switches under Settings → Email, both off on a fresh install, both sent only through the SMTP server you enter there:

  • "email me a digest of what is expiring" — one message a day, to your notification address, listing everything failed, expired or expiring within the warning window.
  • "tell them when something of theirs is expiring" — a note to the address on each asset (usually the responsible team) with something expired or expiring, never more often than the "remind … at most every N days" setting.

They go out when cron/reminders.php runs. Schedule it once a day, for example 15 7 * * * php /path/to/app/cron/reminders.php, and try --dry-run first: it lists who would be written to and sends nothing. On a host without cron, Settings → Run reminders now runs the same pass. Running it twice in a day sends nothing the second time: every note is claimed in a ledger before the mail server is contacted, and a send the server refuses gives its claim back so the next run retries [src: app/controllers/app.php, reminder_run()]. Before 3.1.3 the README named this script but it was not shipped, and the two switches did nothing.

Settings shows when the pass last ran and what it did. When a switch is on and nothing has run the pass for more than two days, the dashboard says so.

The API and agent access

Five tools [src: app/controllers/v3.php:64]: four read — the whole grid, what needs attention, one asset in detail and the inspection-type list — and exactly one write, recording a completed inspection [src: app/controllers/v3.php:155].

The instructions given to an agent carry a guarantee worth knowing yourself [src: app/controllers/v3.php:60]:

"Read the matrix before you write to it. Recording a completion is the only write here; it goes through the same engine call the screens use, so the refusals are identical."

There is no separate API validation to drift out of step with what the screens enforce.

⭐ Checklist inspections, the report of one inspection, labels and assigned schedules (3.2)

  • A checklist per inspection type. Write the steps once, one per line, on the inspection type. Inspect (on the asset page, on a schedule, or in My inspections) opens a form built for a phone: each step asks pass, fail or not applicable, with a note and a photo from the camera. Every step needs an answer, and one failed step records the inspection as a fail [src: app/controllers/inspectora.php:48]. The record goes through the same engine call as every other door, and the answers are kept against the record, so changing the checklist later does not rewrite past inspections [src: app/controllers/inspectora.php:69].
  • Photos are evidence. Each photo is filed on the record like any evidence: the bytes decide the type (PDF, PNG, JPEG or WebP), up to 10 MB each and 12 on one inspection [src: app/controllers/inspectora.php:105]. The answers are kept on the phone while the form is filled in (see the 3.3 section below); photos are chosen again after a reload.
  • From a schedule, the asset's row on the schedule is marked done with the record, once.
  • The report of one inspection. Report PDF on the record page prints the result, the checklist and the list of files [src: app/controllers/inspectora.php:240]. Make a read-only link gives a link that opens that one inspection, with its photos, without a login and kept out of search engines; it can be emailed from the same form through your mail server. Only its SHA-256 is stored, it is shown once, a new link stops the old one, and Withdraw the link stops it outright [src: app/controllers/inspectora.php:318]. The link's photos are served only for that inspection [src: app/controllers/inspectora.php:396]. The JSON backup leaves the link's hash out.
  • Certificates. Certificate PDF prints a certificate of inspection, numbered INSP-000123 from the record, for a pass or a pass with remarks; a failed inspection has none [src: app/controllers/inspectora.php:277]. It states what was recorded here and says it is not issued by a regulator.
  • QR labels. Assets → QR labels prints one label per active asset (or one site or class), each QR opening the asset's page for someone signed in [src: app/controllers/inspectora.php:415].
  • Assigned schedules. A schedule can be assigned to an active member or administrator when it is made or on its page [src: app/controllers/inspectora.php:442]. My inspections lists your planned schedules, soonest first, with what is still to inspect on each [src: app/controllers/inspectora.php:473].

An inspection survives a dropped connection (3.3)

Phones on a site flip between Wi-Fi and mobile data, and a started inspection should not be lost when they do.

  • The answers are kept on the phone as you go. Every answer, note and date on the inspection form is kept in the phone's browser storage for that asset and inspection type, as it is typed [src: app/views/inspect.php:51]. If the page reloads or the phone drops the connection, opening the form again puts the answers back and says so, with Start again to throw them away. Photos are not kept this way: choose them again.
  • Pressing the button with no connection keeps the page as it is and says "No connection right now. Nothing is lost". Press it again once you are back online.
  • An inspection sent twice is recorded once. Each form carries a one-off send reference, kept with the answers [src: app/controllers/inspectora.php:146]. Say the first send arrived but its answer was lost in a dropped connection, and you press again. The second send is told "already recorded" and opens the first record; nothing is added [src: app/controllers/inspectora.php:162]. - The reference is claimed before anything is written (a UNIQUE row, [src: app/src/Database.php:239]), so two sends cannot both record. - A send the form refuses (a step not answered) gives its reference back, so the corrected form can go [src: app/controllers/inspectora.php:180]. - A claim left by a request that died mid-way is taken over after two minutes.
  • Once it has arrived, the record page clears the phone's copy of that send, and only that one [src: app/views/_insp_record.php:46].
  • What this is not: Inspectora is not an offline app. Nothing is recorded until the form reaches your server, and the due board, schedules and reports need a connection. The kept answers live in that phone's browser only; another phone, or a cleared browser, does not have them.

What Inspectora does not do

It does not treat the due day as overdue [src: app/src/Matrix.php:109].

It does not confuse "never inspected" with "overdue" [src: app/src/Matrix.php:130].

It does not invent a day count it was not given [src: app/src/Matrix.php:141].

It does not let a template decide the vocabulary [src: app/src/Domain.php:13].

It does not accept a key from another installation on a lookup [src: app/src/Domain.php:93] — the shape is rejected first.

It does not let a fail start a clock. Every record carries a result: pass, pass with remarks, or fail [src: app/src/Matrix.php:46]. A fail is written to the logbook like any record, but with no next-due date [src: app/src/Matrix.php:371], and its cell reads "failed" until a later pass supersedes it; a pass dated before the fail does not clear it. A pass with remarks is a pass, with its next-due date; the remarks go in the note. Before 3.1.3 a record could only say that the inspection took place, so a check that failed showed as in date.

Looking after your install (from 4.0)

Updates from inside Inspectora

Updates in the menu opens the Updates page (/updates) [src: app/views/updates.php]. Paste your license key once and press Save key: either key works, the one in your Lemon Squeezy receipt or the one on your order page (it starts with OWX-). Check for updates asks the store for the newest release your license covers; Inspectora never checks by itself.

When a new version is ready, Back up and update to that version backs up your data and the current version, installs the new one and checks it. If the update is interrupted, or the new version's database step or that check fails, the previous version is put back by itself. Inspectora is unavailable for about a minute while this runs. The update replaces Inspectora's files, including any you edited, except .htaccess, .user.ini and robots.txt: an edited copy of those is kept, with the new version's copy beside it as .new to compare and merge. The Health page lists any file that differs from the version installed, so you can see that before you update. A copy of the previous version (data/updates/code-<version>/) and a backup of your data from just before the update stay in data/updates/ [src: app/src/Updater.php]. If Inspectora shows an error page after an update anyway, that is your way back: copy the files from code-<version>/ over the app's files, then restore the backup on the backup page. Updating from the page needs PHP's sodium and Phar extensions; if one is missing, the Updates and Health pages say so and updates stay by hand. Where Inspectora is hosted for you, the host updates it and the page says so.

Reaching 4.0 from a 3.x version is the last update done by hand: replace the files as before (config.php and data/ stay, and the database migrates itself on the next page load). From 4.0 on, the Updates page does it.

Starting with sample data

The installer offers Start with sample data: a two-site operation's assets, inspection types and records to try things on. You can remove it in one click. [src: app/install/index.php:118]. It is ticked by default; untick it to start empty. The sample is added when you open the first page after the installer.

When you are done with it, press the remove button in the sample banner. It asks "Remove the sample assets, inspection types and records? Your own records stay." Only the sample goes: your own records stay, and a sample record that one of your own records uses is kept and named in the report [src: app/src/SampleData.php].

Backups by themselves, and a copy somewhere else

On the backup page (/restore, linked from Settings), Back up once a day by itself is on by default; untick it to stop [src: app/views/restore.php:74]. The first visit at least 24 hours after the last backup starts the next one, after the page has loaded, so no cron job is needed. The newest 14 stay in data/backups/, with credentials left out of them (a restore keeps the live values for those). Back up now takes one straight away.

To keep a copy somewhere else as well, fill in an S3-compatible bucket (Backblaze B2, Wasabi, AWS S3 or your own MinIO): the bucket address (an https endpoint), the region, the bucket name, an optional folder, and the access key ID and secret key. Leave the address empty to keep backups on this server only. The copy goes only to the bucket you set; nothing is sent to Ownware. Once a week Inspectora reads the newest backup back through the restore's own dry run and keeps the result for the Health page [src: app/src/BackupTargets.php].

On Nginx: keep data/ private

On Apache, the .htaccess that ships with Inspectora keeps data/ (your backups included), src/, config.php and the other private files off the web. Nginx does not read .htaccess, so add these three blocks to your server block, above the block that passes .php files to PHP (Nginx uses the first one that matches):

location ~ ^/(src|data|bin|tests|controllers|deploy|cron)/ { deny all; }
location ~ ^/views/.+\.php$ { deny all; }
location ~ (^|/)(config\.php|\.demo-mode)$|\.sqlite$ { deny all; }

Then open /config.php in a browser: it should answer 403. Only the template files under views/ are closed, not the folder: the app's own saved-view addresses start with /views/ too, and they must reach it.

A complete server block for a new site (PHP-FPM 8.3; change the name, the folder and the socket to yours):

server {
    listen 80;
    server_name example.com;
    root /var/www/inspectora/app;
    index index.php;

    location ~ ^/(src|data|bin|tests|controllers|deploy|cron)/ { deny all; }
    location ~ ^/views/.+\.php$ { deny all; }
    location ~ (^|/)(config\.php|\.demo-mode)$|\.sqlite$ { deny all; }

    location = /install/ { rewrite ^ /install/index.php last; }
    location / { try_files $uri /index.php?$query_string; }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

The Health page

Health in the menu (/health) shows, one line each, whether the install is looking after itself: the PHP version and extensions, disk space, whether data/ is writable, email, the age of the last backup, the offsite copy, the weekly check, whether an update is waiting, and any file that differs from the version installed. Each line says what to do when it is not fine. The page only reads: it never changes anything and never contacts anything [src: app/src/Health.php].

Connect it to other tools (Zapier, Make, n8n)

Inspectora sends a webhook the moment something happens and accepts calls to its API, so Zapier, Make and n8n can connect to it without a plugin. The steps, the events and example recipes are in API.md, in the section "Connect it to Zapier, Make or n8n"; the API page of Inspectora on the store shows the same file [src: app/API.md].

New in 4.1.2: sign-in limits

New in 4.1.2 (security update): Two-step codes are now limited (10 wrong per person or per address in 10 minutes), and wrong passwords count per account as well as per address. An installed app whose database does not answer keeps its web installer closed instead of showing the install form. On Ownware Cloud, opening the app from the console starts a fresh session.

Wrong passwords are counted per address and, since 4.1.2, per account too (the email typed, in any case, stored only as a hash). After 10 wrong passwords in 10 minutes the sign-in at /login answers "Too many attempts — wait a few minutes and try again." for that address, and for that account from any address. A right password clears both counts [src: app/src/LoginRate.php:40].

Wrong two-step codes (for people who turned two-step sign-in on under /security) are counted per person and per address. After 10 wrong codes in 10 minutes the code step answers "Too many wrong codes. Wait ten minutes, then sign in again. A recovery code works in place of the code." even for the right code, and the half-finished sign-in is dropped, so the password is asked again [src: app/src/LoginRate.php:91]. The right code clears that person's count; an address's count runs out ten minutes after its wrong codes. Before 4.1.2 the code step had no limit.

If you are locked out: wait ten minutes, then sign in again with your password and a current code, or one of your recovery codes (each works once). If you lost both the authenticator and the recovery codes, whoever runs the server can turn two-step sign-in off for you in Inspectora's database (by default the SQLite file in data/, or your MySQL database): UPDATE users SET totp_secret = '' WHERE email = 'you@example.com'; Then sign in with your password and turn it on again under /security.

On Ownware Cloud, opening Inspectora from the console starts a fresh session [src: app/index.php:54].

The web installer stays closed when the database does not answer: with config.php in place, /install now answers 503 "This app is already installed (config.php exists), but its database is not answering, so the installer stays closed." and writes nothing until the database answers again. Before 4.1.2 a database outage (or a missing SQLite file) showed the install form there, and whoever found it could set up a new administrator. To install again from scratch, remove config.php first [src: app/install/index.php:21].

New in 4.1.3: security update

New in 4.1.3 (security update): no other site can put Inspectora's pages in a frame; the /mcp address refuses requests from other sites' pages; company sign-in is stricter; and the audit CSV treats one more first character as text. Nothing else changed: no feature, screen, setting or database table, and your data, config.php and data/ are not touched.

No other site can frame these pages. Every page now answers Content-Security-Policy: frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN, so a page on another site cannot show Inspectora in a hidden frame and trick a signed-in person into pressing a button [src: app/index.php:4]. The installer answers frame-ancestors 'none' and X-Frame-Options: DENY [src: app/install/index.php:4]. If you had Inspectora framed inside another site of your own, that frame now stays empty; link to it instead.

/mcp refuses other sites. A request whose Origin header names another site gets 403 before anything is read. A desktop or server client sends no Origin and is served as before, and so is a page at Inspectora's own address [src: app/src/Mcp.php:272].

Single sign-on. The sign-in now uses PKCE (S256), so an intercepted sign-in code is of no use on its own; a sign-in that comes back to a different browser session is asked to start again [src: app/src/Oidc.php:228]. With Google as the provider, a company address needs a Google Workspace account of that same domain (the token's hd claim), and Google must report the address as verified, so a personal Google account made with a work address no longer gets in [src: app/src/Oidc.php:215]. Gmail addresses work as before, other providers are unchanged, and password sign-in is not affected.

The audit CSV. A cell that starts with a carriage return now opens in a spreadsheet as text, as cells starting with = + - @ or TAB already did [src: app/src/AuditLog.php:138].

Docker. The Dockerfile now installs PHP's zip extension, so a Docker install can make the backup with files [src: app/Dockerfile:7].

Also changed, with no effect on a running install: the corner card on a public demo offers only the purchase, and API.md no longer shows the config example of a product we retired.

← Back to Inspectora · Quickstart · API · Test run