Switch PHP views to Twig and add 2FA/UI enhancements
Migrate many view templates from raw PHP to Twig and modernize UI/UX for 2FA and settings. Controllers updated to provide avatar data and two-factor info (ProfileController, UserController) and SettingsController now includes timezone lists, notification preset selection, cron path, cached update state and rollback availability. ErrorHandler now attempts to render error pages via a new Core\TwigService with a safe fallback to raw PHP views. TwoFactorService generation silences deprecated warnings during QR code creation. Numerous .php view files were removed and replaced with .twig equivalents (2fa setup/verify/backup-codes and many auth, dashboard, domains, errors, layout, users, tags, tld-registry, etc.), and core/TwigService was added. These changes move the app toward a Twig-based templating system, improve 2FA flows, surface avatar images in lists/profiles, and make error rendering more robust.
This commit is contained in:
935
app/Views/groups/edit.twig
Normal file
935
app/Views/groups/edit.twig
Normal file
@@ -0,0 +1,935 @@
|
||||
{% extends 'layout/base.twig' %}
|
||||
|
||||
{% 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 %}
|
||||
<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>
|
||||
Group Details
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<form method="POST" action="/groups/{{ group.id }}/update" class="space-y-5">
|
||||
{{ csrf_field() }}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
{# Group Name #}
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">
|
||||
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 }}"
|
||||
required>
|
||||
</div>
|
||||
|
||||
{# Description #}
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">
|
||||
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>
|
||||
</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-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>
|
||||
Notification Channels
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
{% if group.channels is empty %}
|
||||
<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>
|
||||
</div>
|
||||
{% else %}
|
||||
<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">
|
||||
<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">
|
||||
<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 %}
|
||||
</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' }}
|
||||
</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 %}
|
||||
</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"
|
||||
onclick="return confirm('Delete this channel?')">
|
||||
<i class="fas fa-trash mr-1"></i>
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# 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>
|
||||
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 }}">
|
||||
|
||||
{# Channel Type #}
|
||||
<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"
|
||||
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>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# Email Fields #}
|
||||
<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">
|
||||
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"
|
||||
placeholder="user@example.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Telegram Fields #}
|
||||
<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">
|
||||
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"
|
||||
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11">
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
|
||||
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">
|
||||
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"
|
||||
placeholder="123456789">
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
|
||||
Use @userinfobot to get your chat ID
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Discord Fields #}
|
||||
<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>
|
||||
</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 → Integrations → Webhooks
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Slack Fields #}
|
||||
<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">
|
||||
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 → Incoming Webhooks
|
||||
</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 → Integrations → 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 → <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>
|
||||
</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 }})
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
{% if group.domains is empty %}
|
||||
<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>
|
||||
<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 %}
|
||||
<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">
|
||||
<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 }}
|
||||
</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">
|
||||
<i class="far fa-calendar mr-2"></i>
|
||||
Expires: {{ domain.expiration_date ? domain.expiration_date|date('M j, Y') : 'Unknown' }}
|
||||
</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</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');
|
||||
|
||||
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');
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user