A token-protected JSON endpoint that exposes information about a Craft CMS installation (versions, plugins, environment, health, available updates) so an external dashboard can keep an overview of all your sites.
It is the agent side: each site runs this plugin and exposes a read-only report; a central dashboard polls every site and aggregates the results.
- Requires Craft CMS 5.8+ and PHP 8.2+
- Env-only configuration — no control-panel settings, no secrets in the database
composer require zeixcom/craftmonitoring
./craft plugin/install craft-monitoringCopy config.example.php (shipped with the plugin) to your project's
config/craft-monitoring.php:
<?php
use craft\helpers\App;
return [
'clientSecret' => App::env('MONITORING_CLIENT_SECRET'),
];The plugin rejects any secret shorter than 32 characters (it returns 404 as
if the endpoint did not exist). Generate a strong secret:
openssl rand -hex 32That gives you 64 hex characters (~256 bits of entropy). Put it in .env:
MONITORING_CLIENT_SECRET="<paste-output-here>"Treat this secret like an admin password — rotate it if it leaks, and never commit it. The matching value lives in the monitoring dashboard's config.
POST <cpTrigger>/monitoring/api/system-report
The endpoint is mounted on the CP URL manager, so it sits behind any IP
allowlist, basic-auth, or WAF rule scoped to the CP host. If your CP trigger is
admin, the URL is https://<site>/admin/monitoring/api/system-report. If you
ever IP-restrict the CP at the webserver layer, the dashboard's egress IP must
be on that allowlist.
The endpoint accepts the secret in any of these places (first match wins):
| Method | Example |
|---|---|
| HTTP header | X-Client-Secret: <secret> |
| Authorization bearer | Authorization: Bearer <secret> |
| POST form parameter | clientSecret=<secret> |
| JSON body field | {"clientSecret": "<secret>"} |
The URL query string is not accepted — secrets in URLs leak into webserver logs, CDN logs, and browser history.
# Header (recommended)
curl -X POST \
-H "X-Client-Secret: $MONITORING_CLIENT_SECRET" \
https://<site>/admin/monitoring/api/system-reportA successful call returns 200 with a JSON body. The top-level reportVersion
(currently 1) lets the dashboard evolve independently of the agent — bump it
on any breaking change to the payload shape. The body contains reportVersion,
generatedAt, craft, php, database, environment, system, updates,
health, plugins, modules.
Reads Craft's cached update info only — the monitoring request never makes an outbound call to the update server.
| Field | Meaning |
|---|---|
infoCached |
false ⇒ Craft hasn't checked recently; the counts below are stale (0/false) |
available |
Total available updates (Craft + plugins) |
criticalAvailable |
A critical/security update is available |
migrationsPending |
Code was updated but migrations haven't run |
| Field | Meaning |
|---|---|
queueTotalJobs |
Pending queue jobs (incl. failed); steady growth ⇒ stuck |
deprecationWarnings |
Count of logged deprecation warnings |
pendingProjectConfigChanges |
Project config YAML is out of sync with the database |
Any health/update field is null if its probe threw.
| Field | Meaning |
|---|---|
lastSystemOrPluginUpdateAt |
Last Craft CMS core or plugin update (use this in the dashboard) |
systemInfoUpdatedAt |
Last craft_info write — includes project config applies |
lastSystemOrPluginUpdateAt is derived from migration history (craft and
plugin:* tracks) plus a version fingerprint stored in
storage/runtime/monitoring/last-system-update.json. Project config applies
that only bump systemInfoUpdatedAt are ignored.
- Timing-safe comparison via
hash_equals(). - Opaque failures: missing secret, invalid secret, wrong method, and
unknown path all return the same
404to unauthenticated callers — the endpoint is indistinguishable from a non-existent route. Failure reasons are still logged server-side atwarninglevel. - CSRF disabled (this is a token-based API, not a session endpoint).
- No caching: response sets
Cache-Control: no-store. - Minimum secret length enforced at 32 chars.
- No sensitive identifiers in the report: internal hostname, kernel version, and the Craft system UID are intentionally not emitted.
Dev-mode note: with
CRAFT_DEV_MODE=true, Craft's debug error page may render slightly different bodies for different 404 sources. With dev mode off (production), all 404s render the same template and the distinction disappears. Monitor production, not dev.
If you previously ran this as a copied Yii module:
composer require zeixcom/craftmonitoringand./craft plugin/install craft-monitoring.- Remove the module from
config/app.php(modules+bootstrapentries) and deletemodules/craftmonitoring/. - Rename
config/monitoring.php→config/craft-monitoring.php(the plugin reads its config by handle). TheMONITORING_CLIENT_SECRETenv var is unchanged.
The route, payload, and storage/runtime/monitoring/ state file are identical,
so the dashboard needs no changes.
MIT — see LICENSE.md.