oliverthiele / ot-alerts
OT Alerts - Alert management for TYPO3 extensions — Pushover notifications with rate limiting
Package info
github.com/oliverthiele/ot-alerts
Type:typo3-cms-extension
pkg:composer/oliverthiele/ot-alerts
Requires
- php: >=8.3
- guzzlehttp/guzzle: *
- typo3/cms-core: ^13.4 || ^14.3
README
Centralised alert proxy for TYPO3 extensions — sends Pushover push notifications
with built-in
rate limiting and deduplication. Other extensions simply call
AlertManager::notify() and
AlertManager::resolve(); ot_alerts handles throttling, state tracking, and
channel dispatch.
Features
- Pushover push notifications via REST API with HTML formatting
- Event key and occurrence count visible in every notification
- Optional tappable link button in Pushover (via
context['url']) - Rate limiting: first occurrence triggers immediately, then once per configurable reminder interval
- State machine: NEW → NOTIFIED → RESOLVED — resolved errors trigger fresh notifications when they reappear
- DB-backed event log (
tx_otalerts_events) for audit trail and future backend module - Optional integration: inject
?AlertManagervia constructor — ot_alerts is not a hard dependency, the service is simplynullwhen not installed - Configurable reminder interval via TYPO3 Extension Configuration, with optional per-alert override
- Transactional notifications:
throttle: falsedelivers every single time, bypassing rate limiting completely
Requirements
| Requirement | Version |
|---|---|
| TYPO3 | ^13.4 || ^14.0 |
| PHP | ^8.4 |
| guzzlehttp/guzzle | ^7.0 |
Installation
composer require oliverthiele/ot-alerts
After installation, run database schema update:
vendor/bin/typo3 database:updateschema
Configuration
Environment Variables
Add the following to your .env file:
PUSHOVER_APP_TOKEN=your_app_token_here PUSHOVER_USER_KEY=your_user_key_here
Both values are available in your Pushover dashboard.
Extension Configuration
Configure in the TYPO3 backend under Admin Tools → Settings → Extension Configuration → ot_alerts:
| Key | Type | Default | Description |
|---|---|---|---|
reminderInterval |
int | 3600 |
Seconds between reminder notifications (1 hour) |
pushoverEmergencyRetry |
int | 60 |
Seconds between retries for CRITICAL alerts (min 30) |
pushoverEmergencyExpire |
int | 3600 |
Seconds until Pushover stops retrying (max 10800) |
Usage
Optional dependency via constructor injection
The recommended integration pattern uses TYPO3's Symfony DI container. Declare
?AlertManager
as a nullable constructor parameter — when ot_alerts is not installed, the
container injects
null and all calls are silently skipped via the null-safe operator:
use OliverThiele\OtAlerts\Alert\Alert; use OliverThiele\OtAlerts\Alert\AlertSeverity; use OliverThiele\OtAlerts\Service\AlertManager; class MyService { public function __construct( private readonly ?AlertManager $alertManager = null, ) {} public function doSomething(): void { // ... your logic ... // notify — only fires when ot_alerts is installed $this->alertManager?->notify(new Alert( source: 'my_extension', eventKey: 'api.connection.failed', message: 'Could not connect to external API', severity: AlertSeverity::ERROR, )); } }
No class_exists() guard or GeneralUtility::makeInstance() needed — the
container resolves
the optional service automatically.
Sending an alert
$this->alertManager?->notify(new Alert( source: 'my_extension', eventKey: 'api.connection.failed', message: 'Could not connect to external API', severity: AlertSeverity::ERROR, ));
Sending an alert with a URL
Pass context['url'] to add a tappable link button to the Pushover
notification.
This is useful to open the affected page directly from the push:
$this->alertManager?->notify(new Alert( source: 'my_extension', eventKey: 'api.connection.failed', message: 'Could not connect to external API', severity: AlertSeverity::ERROR, context: ['url' => 'https://example.com/affected-page/'], ));
Sending an alert with a per-alert reminder interval
Pass reminderInterval to override the global extension configuration for this
specific alert. Useful when certain events need a shorter or longer throttle than
the global default:
$this->alertManager?->notify(new Alert( source: 'my_extension', eventKey: 'quota.warning', message: 'API quota at 90 %', severity: AlertSeverity::WARNING, reminderInterval: 300, // remind every 5 minutes instead of the global default ));
Sending a transactional notification
Not everything worth a push is an error. A submitted form, a completed import, an incoming order — these are events that carry their own occasion and have to be delivered every single time. Rate limiting would silently swallow the second one within the reminder interval.
Pass throttle: false for those. AlertManager then skips the rate limit and the
event status entirely, and the occurrence counter is left out of the message,
because it describes a condition that keeps repeating:
$this->alertManager?->notify(new Alert( source: 'my_extension', eventKey: 'order.received', message: 'New order #4711 — Jane Doe, 249.00 EUR', severity: AlertSeverity::NOTICE, throttle: false, ));
notify() reports these dispatches as reason: notification. The event row is
still written, so the log keeps last_message, last_occurrence and the total
count — you just do not get them pushed to your phone.
NOTICE is the matching severity: audible like WARNING, but the title reads
[NOTICE] my_extension and does not claim that something is wrong.
Resolving an alert
Call resolve() once the error condition is no longer present. This resets the
state so the
next occurrence will trigger a fresh notification immediately.
$this->alertManager?->resolve('my_extension', 'api.connection.failed');
Notification format
Pushover messages use HTML formatting for readability:
Title: [ERROR] my_extension
Message: api.connection.failed ← bold event key
Could not connect to
external API
occurrence #3 ← italic, from the 2nd occurrence onwards,
omitted when throttle: false
[Open page →] ← tappable link button, only shown when context['url'] is set
Severity levels and Pushover priorities
| Severity | Pushover priority | Behaviour |
|---|---|---|
INFO |
Low (-1) | Quiet notification, no sound |
NOTICE |
Normal (0) | Default sound and vibration — nothing is wrong, just worth knowing |
WARNING |
Normal (0) | Default sound and vibration |
ERROR |
High (1) | Bypasses quiet hours |
CRITICAL |
Emergency (2) | Repeated every pushoverEmergencyRetry seconds until acknowledged |
Emergency notifications (CRITICAL) require acknowledgement in the Pushover app.
Notification behaviour
First error → Push sent immediately → status: NOTIFIED
Still broken → Push after reminderInterval → status: NOTIFIED
Error fixed → resolve() called → status: RESOLVED
New error → Push sent immediately → status: NOTIFIED
With throttle: false none of this applies — every notify() pushes.
CLI
Test notification
Verify that Pushover credentials are configured and a push is delivered:
vendor/bin/typo3 ot_alerts:test
Send with a specific severity:
vendor/bin/typo3 ot_alerts:test --severity=error
Reset the rate limit after the test so the next real error triggers immediately:
vendor/bin/typo3 ot_alerts:test --resolve
Dispatch as a transactional notification, the way throttle: false does — no rate
limiting, and the rate limit of the real events stays untouched:
vendor/bin/typo3 ot_alerts:test --no-throttle --severity=notice
Show per-channel dispatch result including HTTP status:
vendor/bin/typo3 ot_alerts:test -v
Show the raw Pushover API response body (full debug output):
vendor/bin/typo3 ot_alerts:test -vvv
If the alert was previously sent and the reminder interval has not yet elapsed,
the command shows [WARNING] Rate limit active. Use --resolve to bypass:
the command pre-resolves the event before sending (so the rate limit is skipped)
and post-resolves after (so the next test also sends immediately).
The command shows which ENV variables are present, dispatches the alert, and prints the result.
License
GPL-2.0-or-later — © 2025 Oliver Thiele