Add Pushover notification channel and improve status detection

Introduces Pushover as a new notification channel with priority-based alerts, device targeting, and custom sounds. Enhances domain status detection for .nl and .eu domains, ensuring accurate handling when expiration dates or explicit status flags are missing. Fixes PHP 8.x compatibility issues with null parameters in date functions and improves error handling and logging by replacing error_log() with a centralized Logger service. Updates documentation and migrations for version 1.1.1.
This commit is contained in:
Hosteroid
2025-11-18 13:22:49 +02:00
parent 5b932aa565
commit 2b4035dd29
31 changed files with 684 additions and 97 deletions

View File

@@ -245,7 +245,7 @@ ob_start();
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 truncate"><?= htmlspecialchars($domain['domain_name']) ?></p>
<p class="text-xs text-gray-500 mt-0.5">
<?= date('M d, Y', strtotime($domain['expiration_date'])) ?>
<?= $domain['expiration_date'] ? date('M d, Y', strtotime($domain['expiration_date'])) : 'Unknown' ?>
<span class="<?= $urgencyClass ?> font-semibold ml-2">
<?= $daysLeft ?> days
</span>

View File

@@ -128,7 +128,7 @@ ob_start();
<?php if ($domain['expiration_date']): ?>
<p class="mt-1 text-xs text-green-600">
<i class="fas fa-check-circle mr-1"></i>
Current expiration date: <?= date('M j, Y', strtotime($domain['expiration_date'])) ?>
Current expiration date: <?= $domain['expiration_date'] ? date('M j, Y', strtotime($domain['expiration_date'])) : 'Unknown' ?>
</p>
<?php else: ?>
<p class="mt-1 text-xs text-amber-600">

View File

@@ -367,7 +367,7 @@ $currentFilters = $filters ?? ['search' => '', 'status' => '', 'group' => '', 's
<?php if (!empty($domain['expiration_date'])): ?>
<div class="text-sm">
<div class="font-medium text-gray-900 flex items-center">
<?= date('M d, Y', strtotime($domain['expiration_date'])) ?>
<?= $domain['expiration_date'] ? date('M d, Y', strtotime($domain['expiration_date'])) : 'Unknown' ?>
<?php if ($domain['isManualExpiration']): ?>
<span class="ml-1 inline-flex items-center px-1 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800" title="Manual expiration date">
<i class="fas fa-edit" style="font-size: 8px;"></i>

View File

@@ -164,7 +164,7 @@ ob_start();
</span>
<?php endif; ?>
</p>
<p class="text-xs font-semibold text-gray-900"><?= date('M j, Y', strtotime($domain['expiration_date'])) ?></p>
<p class="text-xs font-semibold text-gray-900"><?= $domain['expiration_date'] ? date('M j, Y', strtotime($domain['expiration_date'])) : 'Unknown' ?></p>
</div>
</div>
<span class="px-2 py-1 bg-<?= $expiryColor ?>-100 text-<?= $expiryColor ?>-800 rounded text-xs font-bold">

View File

@@ -81,7 +81,7 @@ ob_start();
<ul class="text-xs text-gray-600 space-y-1">
<li class="flex items-center">
<i class="fas fa-circle text-blue-500" style="font-size: 6px;"></i>
<span class="ml-2">After creating the group, you'll be able to add notification channels (Email, Telegram, Discord, Slack)</span>
<span class="ml-2">After creating the group, you'll be able to add notification channels (Email, Telegram, Discord, Slack, Mattermost, Pushover, Webhook)</span>
</li>
<li class="flex items-center">
<i class="fas fa-circle text-blue-500" style="font-size: 6px;"></i>

View File

@@ -77,9 +77,9 @@ ob_start();
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
<?php foreach ($group['channels'] as $channel):
$config = json_decode($channel['channel_config'], true);
$icons = ['email' => 'fa-envelope', 'telegram' => 'fa-telegram', 'discord' => 'fa-discord', 'slack' => 'fa-slack', 'mattermost' => 'fa-comments', 'webhook' => 'fa-link'];
$iconClasses = ['email' => 'fas', 'telegram' => 'fab', 'discord' => 'fab', 'slack' => 'fab', 'mattermost' => 'fas', 'webhook' => 'fas'];
$colors = ['email' => 'blue', 'telegram' => 'blue', 'discord' => 'indigo', 'slack' => 'teal', 'mattermost' => 'green', 'webhook' => 'purple'];
$icons = ['email' => 'fa-envelope', 'telegram' => 'fa-telegram', 'discord' => 'fa-discord', 'slack' => 'fa-slack', 'mattermost' => 'fa-comments', 'pushover' => 'fa-mobile-alt', 'webhook' => 'fa-link'];
$iconClasses = ['email' => 'fas', 'telegram' => 'fab', 'discord' => 'fab', 'slack' => 'fab', 'mattermost' => 'fas', 'pushover' => 'fas', 'webhook' => 'fas'];
$colors = ['email' => 'blue', 'telegram' => 'blue', 'discord' => 'indigo', 'slack' => 'teal', 'mattermost' => 'green', 'pushover' => 'red', 'webhook' => 'purple'];
$icon = $icons[$channel['channel_type']] ?? 'fa-bell';
$iconClass = $iconClasses[$channel['channel_type']] ?? 'fas';
$color = $colors[$channel['channel_type']] ?? 'gray';
@@ -106,6 +106,8 @@ ob_start();
echo htmlspecialchars($config['email'] ?? 'No email');
} elseif ($channel['channel_type'] === 'telegram') {
echo "Chat: " . htmlspecialchars($config['chat_id'] ?? 'N/A');
} elseif ($channel['channel_type'] === 'pushover') {
echo "User: " . substr(htmlspecialchars($config['user_key'] ?? 'N/A'), 0, 10) . "...";
} else {
echo "Webhook configured";
}
@@ -164,6 +166,7 @@ ob_start();
<option value="discord">Discord</option>
<option value="slack">Slack</option>
<option value="mattermost">Mattermost</option>
<option value="pushover">Pushover</option>
<option value="webhook">Webhook (Custom)</option>
</select>
</div>
@@ -267,6 +270,90 @@ ob_start();
</div>
</div>
<!-- Pushover Fields -->
<div id="pushover_fields" class="hidden space-y-4">
<div>
<label for="pushover_api_token" class="block text-sm font-medium text-gray-700 mb-1.5">
API Token (Application Key) <span class="text-red-500">*</span>
</label>
<input type="text"
id="pushover_api_token"
name="pushover_api_token"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm font-mono"
placeholder="azGDORePK8gMaC0QOYAMyEEuzJnyUi"
autocomplete="off">
<p class="mt-1.5 text-xs text-gray-500">
<i class="fas fa-info-circle text-blue-500 mr-1"></i>
Create an application at <a href="https://pushover.net/apps/build" target="_blank" class="text-blue-600 hover:underline">pushover.net/apps/build</a>
</p>
</div>
<div>
<label for="pushover_user_key" class="block text-sm font-medium text-gray-700 mb-1.5">
User Key <span class="text-red-500">*</span>
</label>
<input type="text"
id="pushover_user_key"
name="pushover_user_key"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm font-mono"
placeholder="uQiRzpo4DXghDmr9QzzfQu27cmVRsG"
autocomplete="off">
<p class="mt-1.5 text-xs text-gray-500">
<i class="fas fa-info-circle text-blue-500 mr-1"></i>
Find your user key on your <a href="https://pushover.net/" target="_blank" class="text-blue-600 hover:underline">Pushover dashboard</a>
</p>
</div>
<div>
<label for="pushover_device" class="block text-sm font-medium text-gray-700 mb-1.5">
Device Name (Optional)
</label>
<input type="text"
id="pushover_device"
name="pushover_device"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
placeholder="Leave empty for all devices"
autocomplete="off">
<p class="mt-1.5 text-xs text-gray-500">
Specify a device name to send to specific device only (e.g., "iPhone", "Desktop")
</p>
</div>
<div>
<label for="pushover_sound" class="block text-sm font-medium text-gray-700 mb-1.5">
Notification Sound (Optional)
</label>
<select id="pushover_sound"
name="pushover_sound"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm">
<option value="">Default (based on priority)</option>
<option value="pushover">Pushover (default)</option>
<option value="bike">Bike</option>
<option value="bugle">Bugle</option>
<option value="cashregister">Cash Register</option>
<option value="classical">Classical</option>
<option value="cosmic">Cosmic</option>
<option value="falling">Falling</option>
<option value="gamelan">Gamelan</option>
<option value="incoming">Incoming</option>
<option value="intermission">Intermission</option>
<option value="magic">Magic</option>
<option value="mechanical">Mechanical</option>
<option value="pianobar">Piano Bar</option>
<option value="siren">Siren</option>
<option value="spacealarm">Space Alarm</option>
<option value="tugboat">Tugboat</option>
<option value="alien">Alien Alarm (long)</option>
<option value="climb">Climb (long)</option>
<option value="persistent">Persistent (long)</option>
<option value="echo">Pushover Echo (long)</option>
<option value="updown">Up Down (long)</option>
<option value="vibrate">Vibrate Only</option>
<option value="none">None (silent)</option>
</select>
<p class="mt-1.5 text-xs text-gray-500">
Custom sound for notifications. If not set, sound will be chosen based on urgency.
</p>
</div>
</div>
<!-- Generic Webhook Fields -->
<div id="webhook_fields" class="hidden space-y-4">
<div>
@@ -342,7 +429,7 @@ ob_start();
<h3 class="font-semibold text-gray-800 mb-2 truncate"><?= htmlspecialchars($domain['domain_name']) ?></h3>
<p class="text-sm text-gray-600 flex items-center">
<i class="far fa-calendar mr-2"></i>
Expires: <?= date('M j, Y', strtotime($domain['expiration_date'])) ?>
Expires: <?= $domain['expiration_date'] ? date('M j, Y', strtotime($domain['expiration_date'])) : 'Unknown' ?>
</p>
</a>
<?php endforeach; ?>
@@ -364,6 +451,8 @@ function toggleChannelFields() {
const discordWebhook = document.getElementById('discord_webhook');
const slackWebhook = document.getElementById('slack_webhook');
const mattermostWebhook = document.getElementById('mattermost_webhook');
const pushoverApiToken = document.getElementById('pushover_api_token');
const pushoverUserKey = document.getElementById('pushover_user_key');
const genericWebhook = document.getElementById('generic_webhook_url');
// Remove required from all
@@ -373,6 +462,8 @@ function toggleChannelFields() {
discordWebhook.removeAttribute('required');
slackWebhook.removeAttribute('required');
if (mattermostWebhook) mattermostWebhook.removeAttribute('required');
if (pushoverApiToken) pushoverApiToken.removeAttribute('required');
if (pushoverUserKey) pushoverUserKey.removeAttribute('required');
if (genericWebhook) genericWebhook.removeAttribute('required');
// Hide all fields
@@ -381,6 +472,7 @@ function toggleChannelFields() {
document.getElementById('discord_fields').classList.add('hidden');
document.getElementById('slack_fields').classList.add('hidden');
document.getElementById('mattermost_fields').classList.add('hidden');
document.getElementById('pushover_fields').classList.add('hidden');
document.getElementById('webhook_fields').classList.add('hidden');
// Hide test button by default
@@ -413,6 +505,11 @@ function toggleChannelFields() {
mattermostWebhook.focus();
}
break;
case 'pushover':
if (pushoverApiToken) pushoverApiToken.setAttribute('required', 'required');
if (pushoverUserKey) pushoverUserKey.setAttribute('required', 'required');
if (pushoverApiToken) pushoverApiToken.focus();
break;
case 'webhook':
if (genericWebhook) {
genericWebhook.setAttribute('required', 'required');

View File

@@ -24,7 +24,7 @@ ob_start();
<h3 class="text-sm font-semibold text-gray-900 mb-1">About Notification Groups</h3>
<p class="text-xs text-gray-600 leading-relaxed">
Notification groups allow you to organize your notification channels. You can create multiple channels
(Email, Telegram, Discord, Slack) within each group, then assign domains to the group. When a domain
(Email, Telegram, Discord, Slack, Mattermost, Pushover, Webhook) within each group, then assign domains to the group. When a domain
is about to expire, all active channels in its group will receive notifications.
</p>
</div>

View File

@@ -90,7 +90,7 @@ ob_start();
<td class="px-6 py-4">
<?php if (!empty($domain['expiration_date'])): ?>
<div class="text-sm">
<div class="font-medium text-gray-900"><?= date('M d, Y', strtotime($domain['expiration_date'])) ?></div>
<div class="font-medium text-gray-900"><?= $domain['expiration_date'] ? date('M d, Y', strtotime($domain['expiration_date'])) : 'Unknown' ?></div>
<div class="text-xs <?= $expiryClass ?>">
<?= $daysLeft ?> days
</div>

View File

@@ -203,7 +203,7 @@ $currentFilters = $filters ?? ['search' => '', 'status' => '', 'registrar' => ''
<?php if (!empty($domain['expiration_date'])): ?>
<div class="text-sm">
<div class="font-medium text-gray-900 flex items-center">
<?= date('M d, Y', strtotime($domain['expiration_date'])) ?>
<?= $domain['expiration_date'] ? date('M d, Y', strtotime($domain['expiration_date'])) : 'Unknown' ?>
</div>
<div class="text-xs <?= $expiryClass ?>">
<?= $daysLeft ?> days
@@ -282,7 +282,7 @@ $currentFilters = $filters ?? ['search' => '', 'status' => '', 'registrar' => ''
<?php if (!empty($domain['expiration_date'])): ?>
<div class="flex items-center">
<i class="fas fa-calendar-alt text-gray-400 mr-2 w-4"></i>
<span>Expires: <?= date('M d, Y', strtotime($domain['expiration_date'])) ?> (<?= $daysLeft ?> days)</span>
<span>Expires: <?= $domain['expiration_date'] ? date('M d, Y', strtotime($domain['expiration_date'])) : 'Unknown' ?> (<?= $daysLeft ?> days)</span>
</div>
<?php endif; ?>