Files

936 lines
52 KiB
Twig
Raw Permalink Normal View History

{% extends 'layout/base.twig' %}
2025-10-08 14:23:07 +03:00
{% set title = 'Edit Notification Group' %}
{% set pageTitle = 'Edit Notification Group' %}
{% set pageDescription = group.name %}
{% set pageIcon = 'fas fa-edit' %}
{% set channelIcons = { email: 'fa-envelope', telegram: 'fa-telegram', discord: 'fa-discord', slack: 'fa-slack', mattermost: 'fa-comments', pushover: 'fa-mobile-alt', webhook: 'fa-link' } %}
{% set channelIconPrefixes = { email: 'fas', telegram: 'fab', discord: 'fab', slack: 'fab', mattermost: 'fas', pushover: 'fas', webhook: 'fas' } %}
{% set channelColors = { email: 'blue', telegram: 'blue', discord: 'indigo', slack: 'teal', mattermost: 'green', pushover: 'red', webhook: 'purple' } %}
{% block content %}
2025-10-08 14:23:07 +03:00
<div class="max-w-7xl mx-auto space-y-4">
{# Group Details Form #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-slate-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
<i class="fas fa-info-circle text-gray-400 dark:text-slate-500 mr-2 text-sm"></i>
2025-10-08 14:23:07 +03:00
Group Details
</h2>
</div>
2025-10-08 14:23:07 +03:00
<div class="p-6">
<form method="POST" action="/groups/{{ group.id }}/update" class="space-y-5">
{{ csrf_field() }}
2025-10-08 14:23:07 +03:00
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
{# Group Name #}
2025-10-08 14:23:07 +03:00
<div>
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">
2025-10-08 14:23:07 +03:00
Group Name *
</label>
<input type="text"
id="name"
name="name"
class="w-full px-3 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
value="{{ group.name }}"
2025-10-08 14:23:07 +03:00
required>
</div>
{# Description #}
2025-10-08 14:23:07 +03:00
<div>
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">
2025-10-08 14:23:07 +03:00
Description (Optional)
</label>
<textarea id="description"
name="description"
class="w-full px-3 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
rows="3">{{ group.description|default('') }}</textarea>
2025-10-08 14:23:07 +03:00
</div>
</div>
<div class="flex gap-3">
<button type="submit"
2025-10-08 14:23:07 +03:00
class="inline-flex items-center px-5 py-2.5 bg-primary hover:bg-primary-dark text-white rounded-lg font-medium transition-colors text-sm">
<i class="fas fa-save mr-2"></i>
Update Group
</button>
</div>
</form>
</div>
</div>
{# Notification Channels #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-slate-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
<i class="fas fa-plug text-gray-400 dark:text-slate-500 mr-2 text-sm"></i>
2025-10-08 14:23:07 +03:00
Notification Channels
</h2>
</div>
2025-10-08 14:23:07 +03:00
<div class="p-6">
{% if group.channels is empty %}
2025-10-08 14:23:07 +03:00
<div class="text-center py-10">
<i class="fas fa-plug text-gray-300 dark:text-slate-600 text-5xl mb-3"></i>
<p class="text-gray-500 dark:text-slate-400">No channels configured yet</p>
<p class="text-sm text-gray-400 dark:text-slate-500 mt-1">Add your first channel below to start receiving notifications</p>
2025-10-08 14:23:07 +03:00
</div>
{% else %}
2025-10-08 14:23:07 +03:00
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
{% for channel in group.channels %}
{% set config = channel.channel_config|from_json %}
{% set icon = channelIcons[channel.channel_type]|default('fa-bell') %}
{% set iconPrefix = channelIconPrefixes[channel.channel_type]|default('fas') %}
{% set color = channelColors[channel.channel_type]|default('gray') %}
{% set formatLabels = { generic: 'Generic', google_chat: 'Google Chat', simple_text: 'Simple Text' } %}
<div class="bg-gray-50 dark:bg-slate-700 border border-gray-200 dark:border-slate-600 rounded-lg p-6 hover:shadow-md transition-shadow duration-200">
2025-10-08 14:23:07 +03:00
<div class="flex items-start justify-between mb-4">
<div class="w-12 h-12 bg-{{ color }}-100 dark:bg-{{ color }}-500/20 rounded-lg flex items-center justify-center">
{% if channel.channel_type == 'mattermost' %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 500 500" class="text-{{ color }}-600">
2025-10-25 13:13:56 +03:00
<path fill="currentColor" d="M 250.05,34.00 C 251.95,34.04 253.85,34.11 255.65,34.20 255.65,34.20 225.86,69.71 225.86,69.71 225.79,69.72 225.71,69.74 225.63,69.75 149.26,84.10 98.22,146.50 98.22,222.97 98.22,264.53 121.29,313.47 157.97,342.07 186.58,364.39 222.26,378.97 259.18,378.97 352.58,378.97 419.33,310.36 419.33,222.97 419.33,188.06 403.34,150.20 377.57,122.21 377.57,122.21 375.94,74.82 375.94,74.82 430.39,113.97 465.89,177.84 466.00,249.99 466.00,250.00 466.00,250.00 466.00,250.00 466.00,369.29 369.30,466.00 250.00,466.00 130.71,466.00 34.00,369.29 34.00,250.00 34.00,130.71 130.71,34.00 250.00,34.00 250.00,34.00 250.05,34.00 250.05,34.00 Z M 314.15,54.29 C 314.81,54.25 315.47,54.32 316.11,54.54 319.12,55.54 319.96,58.11 320.04,60.99 320.04,60.99 323.88,207.87 323.88,207.87 324.64,236.53 306.72,276.31 263.49,276.43 232.52,276.51 199.81,255.60 199.81,216.30 199.82,201.57 205.42,185.04 219.06,168.19 219.06,168.19 309.09,57.01 309.09,57.01 310.24,55.59 312.17,54.43 314.15,54.29 314.15,54.29 314.15,54.29 314.15,54.29 Z" />
</svg>
{% else %}
<i class="{{ iconPrefix }} {{ icon }} text-{{ color }}-600 text-xl"></i>
{% endif %}
2025-10-08 14:23:07 +03:00
</div>
<span class="px-3 py-1 rounded-full text-xs font-semibold {{ channel.is_active ? 'bg-green-100 dark:bg-green-500/20 text-green-800 dark:text-green-400' : 'bg-gray-200 dark:bg-slate-600 text-gray-600 dark:text-slate-400' }}">
{{ channel.is_active ? 'Active' : 'Disabled' }}
2025-10-08 14:23:07 +03:00
</span>
</div>
<h3 class="font-semibold text-gray-800 dark:text-slate-200 mb-2">{{ channel.channel_type|capitalize }}</h3>
<p class="text-sm text-gray-600 dark:text-slate-400 mb-4 truncate">
{% if channel.channel_type == 'email' %}
{{ config.email|default('No email') }}
{% elseif channel.channel_type == 'telegram' %}
Chat: {{ config.chat_id|default('N/A') }}
{% elseif channel.channel_type == 'pushover' %}
User: {{ (config.user_key|default('N/A'))|slice(0, 10) }}...
{% elseif channel.channel_type == 'webhook' %}
{% set format = config.format|default('generic') %}
Format: {{ formatLabels[format]|default(format|capitalize) }}
{% else %}
Webhook configured
{% endif %}
2025-10-08 14:23:07 +03:00
</p>
<div class="flex gap-2">
<button onclick="testChannel('{{ channel.channel_type }}', {{ config|json_encode|e('html_attr') }})"
class="flex-1 px-3 py-2 bg-blue-50 dark:bg-blue-500/10 text-blue-700 dark:text-blue-400 rounded text-center text-sm hover:bg-blue-100 dark:hover:bg-blue-500/20 transition-colors duration-150">
<i class="fas fa-paper-plane mr-1"></i>
Test
</button>
<form method="POST" action="/groups/{{ group.id }}/channels/{{ channel.id }}/toggle" class="flex-1">
{{ csrf_field() }}
<button type="submit"
class="w-full px-3 py-2 bg-yellow-50 dark:bg-yellow-500/10 text-yellow-700 dark:text-yellow-400 rounded text-center text-sm hover:bg-yellow-100 dark:hover:bg-yellow-500/20 transition-colors duration-150">
<i class="fas fa-{{ channel.is_active ? 'pause' : 'play' }} mr-1"></i>
{{ channel.is_active ? 'Disable' : 'Enable' }}
</button>
</form>
<form method="POST" action="/groups/{{ group.id }}/channels/{{ channel.id }}/delete" class="flex-1">
{{ csrf_field() }}
<button type="submit"
class="w-full px-3 py-2 bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-400 rounded text-center text-sm hover:bg-red-100 dark:hover:bg-red-500/20 transition-colors duration-150"
Enhance DNS discovery, validation & transfers Add comprehensive DNS management and input validation, plus safer transfer and logging behavior. - Add CronHelper utilities for cron scripts and unify logging/formatting. - Improve InputValidator: sanitizeDomainInput and validateRootDomain (handles multi-level TLDs) and use throughout domain import/create flows to reject subdomains. - DomainController: refactor DNS refresh to support quick/deep discovery (background deep scans), add endpoints to discover, add/delete/bulk-delete DNS records, import BIND zone files, enrich IP metadata via enrichIpDetails, and strengthen bulk import/reporting messages. - DnsRecord model: add source column handling (discovered/manual/imported), avoid auto-deleting manual/imported records, and add helpers for deleting, bulk deleting, manual adding and importing zone records. - Tag, NotificationGroup and Domain transfer logic: unlink groups when ownership changes, remove tags that belong to other users, add audit logging via Logger and improved bulk transfer reporting. TagController/View: show transferable users for admins and skip global tags on transfer. - Notification channels (Discord, Mattermost, etc.) and EmailHelper: allow explicit subjects and improve payload fields based on notification type. - Add new migration 029_add_dns_record_source.sql and wire it into the installer; update migrations detection. - Add new views/partials for confirm/import/transfer modals, update various domain/group/tag templates, and update cron scripts and routes for discovery. These changes preserve manual/imported DNS records, improve root-domain validation, enable background deep discovery, and add better logging/audit trails for transfers and imports.
2026-03-10 22:54:28 +02:00
onclick="return confirmClick(event, 'Delete this channel?')">
<i class="fas fa-trash mr-1"></i>
Delete
</button>
</form>
2025-10-08 14:23:07 +03:00
</div>
</div>
{% endfor %}
2025-10-08 14:23:07 +03:00
</div>
{% endif %}
2025-10-08 14:23:07 +03:00
{# Add Channel Form #}
<div class="bg-gray-50 dark:bg-slate-700 rounded-lg p-5 border border-gray-200 dark:border-slate-600">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
<i class="fas fa-plus-circle text-gray-400 dark:text-slate-500 mr-2 text-sm"></i>
2025-10-08 14:23:07 +03:00
Add New Channel
</h3>
<form method="POST" action="/groups/{{ group.id }}/channels" id="channelForm" class="space-y-5">
{{ csrf_field() }}
<input type="hidden" name="group_id" value="{{ group.id }}">
2025-10-08 14:23:07 +03:00
{# Channel Type #}
2025-10-08 14:23:07 +03:00
<div>
<label for="channel_type" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">Channel Type</label>
<select id="channel_type"
name="channel_type"
class="w-full px-3 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
2025-10-08 14:23:07 +03:00
onchange="toggleChannelFields()">
<option value="">-- Select Channel Type --</option>
<option value="email">Email</option>
<option value="telegram">Telegram</option>
<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>
2025-10-08 14:23:07 +03:00
</select>
</div>
{# Email Fields #}
2025-10-08 14:23:07 +03:00
<div id="email_fields" class="hidden space-y-4">
<div>
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">
2025-10-08 14:23:07 +03:00
Email Address
</label>
<input type="email"
id="email"
name="email"
class="w-full px-3 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
2025-10-08 14:23:07 +03:00
placeholder="user@example.com">
</div>
</div>
{# Telegram Fields #}
2025-10-08 14:23:07 +03:00
<div id="telegram_fields" class="hidden space-y-4">
<div>
<label for="bot_token" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">
2025-10-08 14:23:07 +03:00
Bot Token
</label>
<input type="text"
id="bot_token"
name="bot_token"
class="w-full px-3 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
2025-10-08 14:23:07 +03:00
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11">
<p class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
2025-10-08 14:23:07 +03:00
Get from @BotFather on Telegram
</p>
</div>
<div>
<label for="chat_id" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">
2025-10-08 14:23:07 +03:00
Chat ID
</label>
<input type="text"
id="chat_id"
name="chat_id"
class="w-full px-3 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
2025-10-08 14:23:07 +03:00
placeholder="123456789">
<p class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
2025-10-08 14:23:07 +03:00
Use @userinfobot to get your chat ID
</p>
</div>
</div>
{# Discord Fields #}
2025-10-08 14:23:07 +03:00
<div id="discord_fields" class="hidden space-y-4">
<div>
<label for="discord_webhook" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">
Webhook URL <span class="text-red-500">*</span>
2025-10-08 14:23:07 +03:00
</label>
<input type="text"
id="discord_webhook"
name="discord_webhook_url"
class="w-full px-3 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm font-mono bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
placeholder="https://discord.com/api/webhooks/1234567890/abcdefg..."
autocomplete="off">
<p class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
<i class="fas fa-info-circle text-blue-500 mr-1"></i>
Paste the complete webhook URL from Discord Server Settings &rarr; Integrations &rarr; Webhooks
2025-10-08 14:23:07 +03:00
</p>
</div>
</div>
{# Slack Fields #}
2025-10-08 14:23:07 +03:00
<div id="slack_fields" class="hidden space-y-4">
<div>
<label for="slack_webhook" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">
2025-10-08 14:23:07 +03:00
Webhook URL
</label>
<input type="text"
id="slack_webhook"
name="slack_webhook_url"
class="w-full px-3 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm font-mono bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
placeholder="https://hooks.slack.com/services/..."
autocomplete="off">
<p class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
Create in Slack App Settings &rarr; Incoming Webhooks
2025-10-08 14:23:07 +03:00
</p>
</div>
</div>
{# Mattermost Fields #}
<div id="mattermost_fields" class="hidden space-y-4">
<div>
<label for="mattermost_webhook" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">
Webhook URL
</label>
<input type="text"
id="mattermost_webhook"
name="mattermost_webhook_url"
class="w-full px-3 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm font-mono bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
placeholder="https://your-mattermost.com/hooks/..."
autocomplete="off">
<p class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
Create in Mattermost &rarr; Integrations &rarr; Incoming Webhooks
</p>
</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 dark:text-slate-300 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 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm font-mono bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
placeholder="azGDORePK8gMaC0QOYAMyEEuzJnyUi"
autocomplete="off">
<p class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
<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 dark:text-slate-300 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 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm font-mono bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
placeholder="uQiRzpo4DXghDmr9QzzfQu27cmVRsG"
autocomplete="off">
<p class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
<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 dark:text-slate-300 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 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
placeholder="Leave empty for all devices"
autocomplete="off">
<p class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
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 dark:text-slate-300 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 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm bg-white dark:bg-slate-700 text-gray-900 dark:text-white">
<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 dark:text-slate-400">
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>
<label for="webhook_format" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">
Webhook Format
</label>
<select id="webhook_format"
name="webhook_format"
class="w-full px-3 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
onchange="updateWebhookPlaceholder()">
<option value="generic">Generic (n8n/Zapier/Make)</option>
<option value="google_chat">Google Chat</option>
<option value="simple_text">Simple Text ({"text":"..."})</option>
</select>
<p id="webhook_format_help" class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
Choose the payload format for your webhook endpoint.
</p>
</div>
<div>
<label for="generic_webhook_url" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">
Webhook URL <span class="text-red-500">*</span>
</label>
<input type="text"
id="generic_webhook_url"
name="webhook_url"
class="w-full px-3 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm font-mono bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
placeholder="https://example.com/webhook-endpoint"
autocomplete="off">
<p id="webhook_url_help" class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
Will receive JSON payload compatible with n8n/Zapier/Make.
</p>
</div>
{# Google Chat specific help #}
<div id="google_chat_help" class="hidden bg-green-50 dark:bg-green-500/10 border border-green-200 dark:border-green-500/30 rounded-lg p-4">
<h4 class="text-sm font-medium text-green-800 dark:text-green-400 flex items-center mb-2">
<i class="fas fa-info-circle mr-2"></i>
Google Chat Setup Instructions
</h4>
<ol class="text-xs text-green-700 dark:text-green-400 space-y-1 list-decimal list-inside">
<li>Open your Google Chat space</li>
<li>Click the space name &rarr; <strong>Apps & integrations</strong></li>
<li>Click <strong>+ Add webhooks</strong></li>
<li>Enter a name (e.g., "Domain Monitor") and optionally add an avatar</li>
<li>Click <strong>Save</strong> and copy the webhook URL</li>
<li>Paste the URL above (starts with <code class="bg-white dark:bg-slate-700 px-1 rounded">https://chat.googleapis.com/v1/spaces/...</code>)</li>
</ol>
</div>
</div>
<div class="flex gap-3">
<button type="submit"
class="inline-flex items-center px-5 py-2.5 bg-primary hover:bg-primary-dark text-white rounded-lg font-medium transition-colors text-sm">
<i class="fas fa-plus mr-2"></i>
Add Channel
</button>
<button type="button"
id="testChannelBtn"
onclick="testChannel()"
class="inline-flex items-center px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors text-sm hidden">
<i class="fas fa-paper-plane mr-2"></i>
Test Channel
</button>
</div>
2025-10-08 14:23:07 +03:00
</form>
</div>
</div>
</div>
{# Assigned Domains #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-slate-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
<i class="fas fa-globe text-gray-400 dark:text-slate-500 mr-2 text-sm"></i>
Assigned Domains ({{ group.domains|length }})
2025-10-08 14:23:07 +03:00
</h2>
</div>
2025-10-08 14:23:07 +03:00
<div class="p-6">
{% if group.domains is empty %}
2025-10-08 14:23:07 +03:00
<div class="text-center py-10">
<i class="fas fa-globe text-gray-300 dark:text-slate-600 text-5xl mb-3"></i>
<p class="text-gray-500 dark:text-slate-400">No domains assigned to this group yet</p>
2025-10-08 14:23:07 +03:00
<a href="/domains/create" class="mt-3 inline-flex items-center px-5 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-plus mr-2"></i>
Add a Domain
</a>
</div>
{% else %}
2025-10-08 14:23:07 +03:00
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for domain in group.domains %}
{% set statusClass = domain.status == 'active' ? 'bg-green-100 dark:bg-green-500/20 text-green-800 dark:text-green-400' : 'bg-gray-200 dark:bg-slate-600 text-gray-600 dark:text-slate-400' %}
<a href="/domains/{{ domain.id }}" class="block bg-gray-50 dark:bg-slate-700 border border-gray-200 dark:border-slate-600 rounded-lg p-6 hover:shadow-md hover:border-primary transition-all duration-200">
2025-10-08 14:23:07 +03:00
<div class="flex items-start justify-between mb-3">
<div class="w-12 h-12 bg-primary bg-opacity-10 rounded-lg flex items-center justify-center">
<i class="fas fa-globe text-primary text-xl"></i>
</div>
<span class="px-3 py-1 rounded-full text-xs font-semibold {{ statusClass }}">
{{ domain.status|capitalize }}
2025-10-08 14:23:07 +03:00
</span>
</div>
<h3 class="font-semibold text-gray-800 dark:text-slate-200 mb-2 truncate">{{ domain.domain_name }}</h3>
<p class="text-sm text-gray-600 dark:text-slate-400 flex items-center">
2025-10-08 14:23:07 +03:00
<i class="far fa-calendar mr-2"></i>
Expires: {{ domain.expiration_date ? domain.expiration_date|date('M j, Y') : 'Unknown' }}
2025-10-08 14:23:07 +03:00
</p>
</a>
{% endfor %}
2025-10-08 14:23:07 +03:00
</div>
{% endif %}
2025-10-08 14:23:07 +03:00
</div>
</div>
</div>
<script>
function toggleChannelFields() {
const channelType = document.getElementById('channel_type').value;
const testBtn = document.getElementById('testChannelBtn');
const emailField = document.getElementById('email');
const botTokenField = document.getElementById('bot_token');
const chatIdField = document.getElementById('chat_id');
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');
emailField.removeAttribute('required');
botTokenField.removeAttribute('required');
chatIdField.removeAttribute('required');
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');
2025-10-08 14:23:07 +03:00
document.getElementById('email_fields').classList.add('hidden');
document.getElementById('telegram_fields').classList.add('hidden');
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');
testBtn.classList.add('hidden');
2025-10-08 14:23:07 +03:00
if (channelType) {
document.getElementById(channelType + '_fields').classList.remove('hidden');
switch(channelType) {
case 'email':
emailField.setAttribute('required', 'required');
break;
case 'telegram':
botTokenField.setAttribute('required', 'required');
chatIdField.setAttribute('required', 'required');
break;
case 'discord':
discordWebhook.setAttribute('required', 'required');
discordWebhook.focus();
break;
case 'slack':
slackWebhook.setAttribute('required', 'required');
slackWebhook.focus();
break;
case 'mattermost':
if (mattermostWebhook) {
mattermostWebhook.setAttribute('required', 'required');
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');
genericWebhook.focus();
}
break;
}
testBtn.classList.remove('hidden');
2025-10-08 14:23:07 +03:00
}
}
const addChannelForm = document.querySelector('form[action="/groups/{{ group.id }}/channels"]');
if (addChannelForm) {
addChannelForm.addEventListener('submit', function(e) {
const channelType = document.getElementById('channel_type').value;
if (!channelType) {
e.preventDefault();
alert('Please select a channel type');
return false;
}
if (channelType === 'discord') {
const webhookField = document.getElementById('discord_webhook');
const webhookUrl = webhookField.value.trim();
if (!webhookUrl) {
e.preventDefault();
alert('Please enter the Discord webhook URL');
webhookField.focus();
return false;
}
if (!webhookUrl.includes('discord.com/api/webhooks/')) {
e.preventDefault();
alert('Invalid Discord webhook URL. It should start with:\nhttps://discord.com/api/webhooks/');
webhookField.focus();
return false;
}
}
if (channelType === 'slack') {
const webhookUrl = document.getElementById('slack_webhook').value.trim();
if (!webhookUrl) {
e.preventDefault();
alert('Please enter the Slack webhook URL');
document.getElementById('slack_webhook').focus();
return false;
}
}
if (channelType === 'mattermost') {
const webhookUrl = document.getElementById('mattermost_webhook').value.trim();
if (!webhookUrl) {
e.preventDefault();
alert('Please enter the Mattermost webhook URL');
document.getElementById('mattermost_webhook').focus();
return false;
}
}
if (channelType === 'webhook') {
const webhookUrl = document.getElementById('generic_webhook_url').value.trim();
if (!webhookUrl) {
e.preventDefault();
alert('Please enter the Webhook URL');
document.getElementById('generic_webhook_url').focus();
return false;
}
}
return true;
});
}
function testChannel(channelType, existingConfig = null) {
const isExistingChannel = existingConfig !== null;
if (!isExistingChannel) {
channelType = document.getElementById('channel_type').value;
const testBtn = document.getElementById('testChannelBtn');
if (!channelType) {
alert('Please select a channel type first');
return;
}
let isValid = true;
let errorMessage = '';
switch(channelType) {
case 'email':
const email = document.getElementById('email').value.trim();
if (!email) {
isValid = false;
errorMessage = 'Please enter an email address';
} else if (!email.includes('@') || !email.includes('.')) {
isValid = false;
errorMessage = 'Please enter a valid email address';
}
break;
case 'telegram':
const botToken = document.getElementById('bot_token').value.trim();
const chatId = document.getElementById('chat_id').value.trim();
if (!botToken) {
isValid = false;
errorMessage = 'Please enter a bot token';
} else if (!/^\d+:[A-Za-z0-9_-]+$/.test(botToken)) {
isValid = false;
errorMessage = 'Please enter a valid bot token format (e.g., 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11)';
} else if (!chatId) {
isValid = false;
errorMessage = 'Please enter a chat ID';
} else if (!/^-?\d+$/.test(chatId)) {
isValid = false;
errorMessage = 'Please enter a valid chat ID (numeric value)';
}
break;
case 'discord':
const discordWebhook = document.getElementById('discord_webhook').value.trim();
if (!discordWebhook) {
isValid = false;
errorMessage = 'Please enter a Discord webhook URL';
} else if (!discordWebhook.includes('discord.com/api/webhooks/')) {
isValid = false;
errorMessage = 'Invalid Discord webhook URL';
}
break;
case 'slack':
const slackWebhook = document.getElementById('slack_webhook').value.trim();
if (!slackWebhook) {
isValid = false;
errorMessage = 'Please enter a Slack webhook URL';
}
break;
case 'mattermost':
const mattermostWebhook = document.getElementById('mattermost_webhook').value.trim();
if (!mattermostWebhook) {
isValid = false;
errorMessage = 'Please enter a Mattermost webhook URL';
}
break;
case 'webhook':
const genericWebhook = document.getElementById('generic_webhook_url').value.trim();
if (!genericWebhook) {
isValid = false;
errorMessage = 'Please enter a Webhook URL';
}
break;
case 'pushover':
const pushoverApiToken = document.getElementById('pushover_api_token').value.trim();
const pushoverUserKey = document.getElementById('pushover_user_key').value.trim();
if (!pushoverApiToken) {
isValid = false;
errorMessage = 'Please enter a Pushover API token';
} else if (!/^[a-zA-Z0-9]{30}$/.test(pushoverApiToken)) {
isValid = false;
errorMessage = 'Please enter a valid Pushover API token (30 alphanumeric characters)';
} else if (!pushoverUserKey) {
isValid = false;
errorMessage = 'Please enter a Pushover user key';
} else if (!/^[a-zA-Z0-9]{30}$/.test(pushoverUserKey)) {
isValid = false;
errorMessage = 'Please enter a valid Pushover user key (30 alphanumeric characters)';
}
break;
}
if (!isValid) {
alert(errorMessage);
return;
}
testBtn.disabled = true;
testBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Testing...';
}
const formData = new FormData();
formData.append('channel_type', channelType);
let groupId = document.querySelector('input[name="group_id"]')?.value;
if (!groupId) {
const urlParts = window.location.pathname.split('/');
groupId = urlParts[urlParts.indexOf('groups') + 1];
}
formData.append('group_id', groupId);
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
formData.append('csrf_token', csrfToken);
if (isExistingChannel) {
switch(channelType) {
case 'email':
formData.append('email', existingConfig.email);
break;
case 'telegram':
formData.append('bot_token', existingConfig.bot_token);
formData.append('chat_id', existingConfig.chat_id);
break;
case 'discord':
formData.append('discord_webhook_url', existingConfig.webhook_url);
break;
case 'slack':
formData.append('slack_webhook_url', existingConfig.webhook_url);
break;
case 'mattermost':
formData.append('mattermost_webhook_url', existingConfig.webhook_url);
break;
case 'webhook':
formData.append('webhook_url', existingConfig.webhook_url);
if (existingConfig.format) {
formData.append('webhook_format', existingConfig.format);
}
break;
case 'pushover':
formData.append('pushover_api_token', existingConfig.api_token);
formData.append('pushover_user_key', existingConfig.user_key);
if (existingConfig.device) {
formData.append('pushover_device', existingConfig.device);
}
if (existingConfig.sound) {
formData.append('pushover_sound', existingConfig.sound);
}
break;
}
} else {
switch(channelType) {
case 'email':
formData.append('email', document.getElementById('email').value);
break;
case 'telegram':
formData.append('bot_token', document.getElementById('bot_token').value);
formData.append('chat_id', document.getElementById('chat_id').value);
break;
case 'discord':
formData.append('discord_webhook_url', document.getElementById('discord_webhook').value);
break;
case 'slack':
formData.append('slack_webhook_url', document.getElementById('slack_webhook').value);
break;
case 'mattermost':
formData.append('mattermost_webhook_url', document.getElementById('mattermost_webhook').value);
break;
case 'webhook':
formData.append('webhook_url', document.getElementById('generic_webhook_url').value);
const webhookFormat = document.getElementById('webhook_format');
if (webhookFormat && webhookFormat.value) {
formData.append('webhook_format', webhookFormat.value);
}
break;
case 'pushover':
formData.append('pushover_api_token', document.getElementById('pushover_api_token').value);
formData.append('pushover_user_key', document.getElementById('pushover_user_key').value);
const pushoverDevice = document.getElementById('pushover_device');
if (pushoverDevice && pushoverDevice.value) {
formData.append('pushover_device', pushoverDevice.value);
}
const pushoverSound = document.getElementById('pushover_sound');
if (pushoverSound && pushoverSound.value) {
formData.append('pushover_sound', pushoverSound.value);
}
break;
}
}
fetch('/channels/test', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
})
.then(response => response.json())
.then(data => {
if (!isExistingChannel) {
const testBtn = document.getElementById('testChannelBtn');
testBtn.disabled = false;
testBtn.innerHTML = '<i class="fas fa-paper-plane mr-2"></i>Test Channel';
}
if (data.success) {
showToast(data.message, 'success');
} else {
showToast(data.message, 'error');
}
})
.catch(error => {
if (!isExistingChannel) {
const testBtn = document.getElementById('testChannelBtn');
testBtn.disabled = false;
testBtn.innerHTML = '<i class="fas fa-paper-plane mr-2"></i>Test Channel';
}
showToast('Test failed: ' + error.message + ' Please check your configuration and try again.', 'error');
});
}
function showToast(message, type = 'info') {
const toastContainer = document.getElementById('toast-container');
if (!toastContainer) return;
const typeConfig = {
success: {
icon: 'fa-check',
iconColor: 'text-green-600',
bgColor: 'bg-green-100',
borderColor: 'border-green-500',
title: 'Success'
},
error: {
icon: 'fa-times',
iconColor: 'text-red-600',
bgColor: 'bg-red-100',
borderColor: 'border-red-500',
title: 'Error'
},
warning: {
icon: 'fa-exclamation-triangle',
iconColor: 'text-orange-600',
bgColor: 'bg-orange-100',
borderColor: 'border-orange-500',
title: 'Warning'
},
info: {
icon: 'fa-info',
iconColor: 'text-blue-600',
bgColor: 'bg-blue-100',
borderColor: 'border-blue-500',
title: 'Info'
}
};
const config = typeConfig[type] || typeConfig.info;
const toast = document.createElement('div');
toast.className = `toast bg-white dark:bg-slate-800 border-l-4 ${config.borderColor} rounded-lg shadow-lg p-4 flex items-start animate-slide-in`;
toast.innerHTML = `
<div class="flex-shrink-0">
<div class="w-8 h-8 ${config.bgColor} rounded-full flex items-center justify-center">
<i class="fas ${config.icon} ${config.iconColor} text-sm"></i>
</div>
</div>
<div class="ml-3 flex-1">
<p class="text-sm font-medium text-gray-900 dark:text-white">${config.title}</p>
<p class="text-sm text-gray-600 dark:text-slate-400 mt-0.5">${message}</p>
</div>
<button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 dark:text-slate-500 hover:text-gray-600 dark:hover:text-slate-300 transition-colors">
<i class="fas fa-times text-sm"></i>
</button>
`;
toastContainer.appendChild(toast);
setTimeout(() => {
toast.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out';
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
setTimeout(() => {
toast.remove();
}, 300);
}, 5000);
}
function updateWebhookPlaceholder() {
const format = document.getElementById('webhook_format').value;
const urlInput = document.getElementById('generic_webhook_url');
const urlHelp = document.getElementById('webhook_url_help');
const formatHelp = document.getElementById('webhook_format_help');
const googleChatHelp = document.getElementById('google_chat_help');
switch(format) {
case 'google_chat':
urlInput.placeholder = 'https://chat.googleapis.com/v1/spaces/XXXXX/messages?key=...';
urlHelp.innerHTML = '<i class="fas fa-info-circle text-green-500 mr-1"></i>Paste your Google Chat webhook URL from space settings.';
formatHelp.textContent = 'Sends messages in Google Chat format with rich cards for domain alerts.';
googleChatHelp.classList.remove('hidden');
break;
case 'simple_text':
urlInput.placeholder = 'https://example.com/webhook-endpoint';
urlHelp.innerHTML = 'Sends simple JSON payload: <code class="bg-gray-100 dark:bg-slate-700 px-1 rounded">{"text":"message"}</code>';
formatHelp.textContent = 'Compatible with services expecting simple text payloads.';
googleChatHelp.classList.add('hidden');
break;
default:
urlInput.placeholder = 'https://example.com/webhook-endpoint';
urlHelp.textContent = 'Will receive JSON payload compatible with n8n/Zapier/Make.';
formatHelp.textContent = 'Sends structured JSON with event type, message, data, and timestamp.';
googleChatHelp.classList.add('hidden');
break;
}
}
2025-10-08 14:23:07 +03:00
</script>
{% endblock %}