Version 5.4.1
October 15, 2026
Fixed
- Fresh installs now create the alert rules and webhooks tables. They were defined only in the
alerts_v1migration, which Craft marks as applied on install without running it. Every site installed on 5.x was missingcontroltower_alert_rules,controltower_webhooks, thealertRuleIdcolumn on alerts and the five default rules, so anything touching alert rules or webhooks failed with "Base table or view not found" (#4). The install migration now creates them. A new migration adds whatever is missing on sites that are already installed, and it skips sites that have the tables. It only seeds the default rules when it creates the rules table, so rules you deleted stay deleted.
Version 5.4.0
October 9, 2026
Added
- Health endpoint for uptime monitors.
/control-tower/healthreturns a JSON report of firing alerts and key gauges for Oh Dear, Better Stack, Uptime Kuma and the like: HTTP 200 when healthy, 503 while an alert is firing or the alert checks have stalled. It is off until a Health Token of at least 32 characters is set, ideally as an environment variable ($CONTROL_TOWER_HEALTH_TOKEN). Monitors send it asAuthorization: Bearer …,X-Control-Tower-Tokenor?key=, and it is compared exactly and in constant time. The report carries no alert messages, hostnames, paths, versions or personal data. Seedocs/health.md. php craft control-tower/status. The same report on the command line, exiting 0 (OK), 1 (warning), 2 (critical) or 3 (unknown) in the Nagios convention.--fail-on=criticalfails only on critical rules,--jsonprints the endpoint's JSON, and--refreshruns the alert checks before reporting.- A Stalled Check Threshold setting (30 minutes). Every alert sweep now records when it ran, and the health report fails when the last one is older than this, since alerts can't fire while
RunAlertChecksJobisn't running.
Version 5.3.0
October 8, 2026
Added
- Custom alert metrics. Plugins and modules can now add metrics for alert rules through
MetricRegistryService::EVENT_REGISTER_METRICS. The README has always called the metric registry extensible; until now it was a fixed list. A registered metric appears everywhere the built-ins do: the rule editor, alert checks, System Pulse, the dashboard widget, the JSON API, and alert emails and webhooks. Seedocs/metrics.md, which includes a complete example module. - A metric contract,
MetricInterface: handle, label, unit, description, the operators that make sense for it, an optional look-back window, andgetValue(MetricPeriod). ExtendMetric, or register aCallbackMetricwith anevaluator. The built-ins are defined the same way, so there is one path for both. - Three built-in metrics:
db_response_ms(database round trip),failed_logins(accounts with a failed login in the last hour), andcritical_updates(Craft and plugins with a critical release waiting, read from Craft's cached update info; Control Tower never calls out for it). - An Alert Metrics panel on System Pulse showing every registered metric, its value right now, and how many rules use it.
- The widget can show any alert metrics as stat cards, and keeps them current on refresh.
- Generic webhook payloads carry
metricLabel,metricUnit,metricWindowandmetricAvailable. Slack, Teams and email notifications show the metric's label instead of its handle.
Changed
MetricRegistryService::all()andget()returnMetricInterfaceobjects instead of arrays. Twig that readsmeta.labelormeta.unitkeeps working.- A rule can only be saved with an operator its metric accepts. The percentage and millisecond gauges take
>,>=,<and<=; counts take all five. Existing rules keep evaluating as before. One using=on a gauge only has to change when it is next saved. - A metric that can't be measured now has no value, and rules on it are skipped for that check. Before, an unreadable CPU, memory or disk figure counted as
0, so a<rule could fire on it. - An exception or a non-finite result from any metric is logged and skipped, and the rest of the alert sweep carries on.
Fixed
- A rule whose metric is no longer registered is listed as Unavailable and skipped by alert checks. Opening it shows a warning and keeps its stored metric selected; before, the editor silently swapped in the first metric in the list.
- The README listed 5xx error rate and 404 spike thresholds under settings. Those settings stopped doing anything in 5.1.0, when alert rules replaced them, so they're gone from the docs.
Version 5.2.0
September 29, 2026
Security
- Webhook URLs can no longer reach internal addresses. Every webhook POST — the "Send test" button and real alerts alike — now requires an
http/httpsURL whose host resolves only to public addresses, pins the connection to those addresses (so DNS cannot be rebound between the check and the connect), refuses URLs carrying credentials, and does not follow redirects. Before, anyone allowed to manage webhooks could make the server POST to the cloud metadata service or the private network. Webhooks pointed at a private address are now refused when saved. - "Send test" reports the HTTP status only. It used to echo the first 500 characters of the response body (or the connection error) back to the browser, which let the test button read internal endpoints. The body is still logged.
- Visitor IP hashes are keyed. IPs were stored as a plain SHA-256, which can be reversed by hashing all 2^32 IPv4 addresses. They are now an HMAC keyed on the site's security key and the day. A migration clears the hashes stored by earlier versions.
Added
allowPrivateWebhookHostsconfig setting for sites that genuinely alert an internal endpoint. Set it inconfig/control-tower.php; likedisableLicenseEnforcement, it is not exposed in the CP. Redirects are still not followed.- Integration checks (
tests/integration/checks.php,http.php) and ECS.
Fixed
- Webhook alerts sent from the queue no longer fail. Building the alert link read the request's host, which a console queue runner doesn't have, so every Slack/Teams/generic webhook sent from a CLI queue worker threw.
Changed
- Dashboard, rule and webhook screens use plugin CSS classes on Craft's spacing scale instead of inline styles. The alert email keeps its inline styles, which email clients need.
- Visitors active when you upgrade are counted once more, because session hashes are now keyed too.
Version 5.1.4
August 19, 2026
Added
- Craftnet license validation. Control Tower now checks its license key status through Craft's built-in Craftnet integration and gates itself when the install isn't licensed. Enforcement is strict: only
validandtrialunlock the plugin. - License screen at Control Tower → License, showing the current status, Craft's license issues, and a key field (admins only). This page is never gated — it's how a locked install gets unlocked.
disableLicenseEnforcementconfig setting for CI and unrecognised staging domains. Set it inconfig/control-tower.php; it is intentionally not exposed in the CP.- Refresh status button on the license screen, and an automatic re-check when a key is saved, so a valid key unlocks the install immediately instead of waiting for Craft's next scheduled update check.
Notes on expired licenses
- An expired license does not lock the plugin. Licenses are perpetual for versions released before they expire, so an install that lapses its renewal and stays on its current version keeps reporting
validand keeps working. Onlyastray— installed version newer than the license covers — locks the plugin, and Craftnet determines that itself. - From
astray, the license screen presents both remedies as equally valid: renew to cover the newer version, or downgrade to the last covered version and continue without renewing.
Changed
- A locked install collapses its CP nav to the License item, redirects all Control Tower pages there, returns
402from the JSON API, and renders a locked notice in the dashboard widget. - A locked install stops collecting visitor, editor, content, and metrics data, and stops running alert checks and notifications. Retention cleanup still runs, and no existing data is deleted.
- Installs on domains Craft considers testable (local/dev) are never gated. The verdict is mirrored into the cache so console and queue requests agree with web requests.
Version 5.1.3
August 19, 2026
Fixed
- Plugin settings are saved again. Every field in the settings screen was named
settings[…], but both screens that render those fields already apply thesettingsnamespace themselves — so the values posted assettings[settings][…]and Craft'splugins/save-plugin-settingsaction ignored them. The screen reported "Plugin settings saved" and discarded every edit. Fields are now emitted bare, and Control Tower's own Settings tab wraps them in{% namespace 'settings' %}to match the Settings → Plugins screen, which Craft namespaces for it.
Note on 5.1.2
- The
5.1.2tag was originally published against the 5.1.1 commit, so the fixes listed under 5.1.2 below never actually reached anyone. The tag has been corrected, and 5.1.3 contains that work regardless — if you are coming from 5.1.1 or earlier, upgrading to 5.1.3 picks up both releases.
Version 5.1.2
July 19, 2026
Fixed
- Alert
contextand rulewebhookIdsare no longer stored as the literalfalsewhenjson_encode()fails; they now fall back tonull. - Server metric collection no longer passes a
falsefile-read result intopreg_match()/substr_count()when/proc/meminfoor/proc/cpuinfocan't be read, so CPU/memory sampling degrades gracefully instead of misreporting. - Content events now cast the current user id to an integer before storing, avoiding a type mismatch on the
userIdcolumn.
Added
- PHPStan static analysis is wired up for the plugin (
craftcms/phpstandev dependency,phpstan.neonat level 5, and acomposer phpstanscript). Thesrc/tree passes cleanly.
Version 5.1.1
May 17, 2026
Fixed
- Republish 5.1.0 under a corrected tag so Packagist accepts it. The 5.1.0 tag was placed on a commit whose
composer.jsonstill read5.0.2, causing Packagist to skip it.
Version 5.1.0
May 14, 2026
Added
- User-configurable alert rules. New Alerts → Rules section lets users define alert conditions in the CMS without editing config. Each rule has a metric, operator, threshold, severity, and enable toggle.
- Webhook notifications for Slack, Microsoft Teams (Power Automate / Adaptive Cards), and generic JSON receivers like Zapier. Defined once globally and referenced per-rule.
- Per-rule email recipients. “Notify admins” toggle plus a freeform list of additional email addresses.
- Send test button on the webhook edit page — fires a sample payload and reports delivery status inline.
- Flap throttle —
minNotifyInterval(minutes) per rule suppresses repeat notifications when an alert flaps. Alert still appears in the CP regardless. - Notify on resolve option per rule.
- User permissions —
controltower:viewDashboard,controltower:manageAlerts, andcontroltower:manageSettings. CP navigation respects them so non-admin users can be granted scoped access. - Metric registry — extensible list of metrics that rules can target: queue failed/pending, CPU/memory/disk %, editor collisions, active editor count, stale content count.
- Alerts now record which rule fired them via a new
alertRuleIdforeign key; the alerts UI shows the rule name instead of the internal type slug. - Email template at
templates/_cp/_emails/alert.twig, overrideable by site builders. SendAlertNotificationJobqueue job — notifications dispatch asynchronously so SMTP / webhook latency doesn't block the request that triggers the alert.
Changed
- The three hardcoded checks (queue failure, editor collisions, server resources) are now data — seeded as default rules on install. Existing installs are migrated automatically by the install of the new schema. The corresponding settings (
queueFailureAlertThreshold,enableCollisionDetection) are now ignored; configure the seeded rules instead. Plugin::PERMISSION_VIEWreplaces the implicitaccessPlugin-control-towerrequirement in controllers.
Migration
- New tables:
controltower_alert_rules,controltower_webhooks. - New column:
alertRuleIdoncontroltower_alerts(FK to alert rules, SET NULL on delete). - Schema version bumped to 1.1.0.
Version 5.0.2
May 14, 2026
Fixed
- Install migration now skips table creation if the table already exists, so installing on a load-balanced environment with a shared database no longer fails on the second node.
Version 5.0.1
May 14, 2026
Fixed
- Plugin store install failing with “Table
controltower_editor_sessionsdoesn't exist” because activity tracking ran before the install migration. Tracking now waits until the plugin is fully installed. - Correct documentation URL in composer.json to point to https://craft-controltower.com/docs instead of the placeholder URL. (Thanks @brandonkelly!)
Version 5.0.0
May 2, 2026
Added
- Initial release of Control Tower for Craft CMS 5.
- Live operational monitoring dashboard with site activity, editor tracking, content health, queue watch, and system metrics.