Add SSL monitoring (Svc, model, cron, UI)

Introduce SSL certificate monitoring: add SslService for fetching/parsing certs and parsing monitor targets, SslCertificate model for storing snapshots and managing monitored targets, and cron/check_ssl.php for scheduled checks. Extend DomainController with many SSL endpoints and helpers (add/refresh/bulk refresh/delete/bulk delete, snapshot handling, formatting, stats, safety checks) and surface SSL data in domain views. Add NotificationService helpers to create/send SSL alerts, update Installer to include new migration, add migration 028 to create ssl_certificates table, bump app version default to 1.1.5, update changelog, and modify routes and templates to include SSL tab and related UI. Logs and basic validation/error handling are included to surface SSL issues and protect default root-target behavior.
This commit is contained in:
Hosteroid
2026-03-08 21:12:09 +02:00
parent 8559e903b9
commit 5916daa293
17 changed files with 2460 additions and 349 deletions

View File

@@ -1,379 +1,507 @@
<!-- SSL TAB CONTENT -->
{% set sslStats = sslStats|default({
total: 0,
valid: 0,
expiring: 0,
expired: 0,
invalid: 0,
issues: 0
}) %}
{% set sslCertificates = sslCertificates|default([]) %}
{% set sslMonitoringEnabled = domain.ssl_monitoring_enabled|default(0) %}
{% set rootCertificate = sslCertificates|filter(cert => cert.is_root)|first %}
<!-- Preview Banner -->
<div class="mb-3 bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-800 rounded-lg p-3">
<div class="flex items-center">
<i class="fas fa-flask text-amber-600 dark:text-amber-400 mr-2" style="font-size: 12px;"></i>
<div>
<p class="text-xs font-semibold text-amber-900 dark:text-amber-300">Preview</p>
<p class="text-xs text-amber-800 dark:text-amber-400 mt-0.5">SSL certificate monitoring is coming soon. This is a design preview with sample data.</p>
</div>
</div>
</div>
<!-- Filters & Actions Bar -->
<div class="mb-3 flex flex-wrap gap-3 justify-between items-center">
<div class="flex-1 max-w-md">
<div class="relative">
<input type="text" id="ssl-search" placeholder="Search certificates..." class="w-full pl-9 pr-3 py-2 border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-900 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm">
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-slate-500 text-xs"></i>
</div>
</div>
<div class="flex gap-2">
<select id="ssl-filter" class="px-3 py-2 border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-900 text-gray-900 dark:text-white rounded-lg text-sm">
<option value="all">All Certificates</option>
<option value="valid">Valid Only</option>
<option value="expiring">Expiring Soon</option>
<option value="expired">Expired</option>
<option value="invalid">Invalid</option>
</select>
<button onclick="checkAllCertificates()" class="inline-flex items-center px-3 py-2 bg-green-600 text-white text-xs rounded-lg hover:bg-green-700 transition-colors font-medium">
<i class="fas fa-sync-alt mr-1.5" style="font-size: 10px;"></i>
Check All
</button>
<button class="inline-flex items-center px-3 py-2 bg-primary text-white text-xs rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-plus mr-1.5" style="font-size: 10px;"></i>
Add Subdomain
</button>
</div>
</div>
<!-- Bulk Actions Toolbar (Hidden by default) -->
<div id="ssl-bulk-actions" class="hidden mb-3 bg-blue-50 dark:bg-blue-500/10 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span id="ssl-selected-count" class="text-xs font-medium text-blue-900 dark:text-blue-300"></span>
<button type="button" onclick="bulkCheckSSL()" class="inline-flex items-center px-3 py-1.5 bg-green-600 text-white text-xs rounded-lg hover:bg-green-700 transition-colors font-medium">
<i class="fas fa-sync-alt mr-1.5" style="font-size: 9px;"></i>
Check Selected
</button>
<button type="button" onclick="bulkDeleteSSL()" class="inline-flex items-center px-3 py-1.5 bg-red-600 text-white text-xs rounded-lg hover:bg-red-700 transition-colors font-medium">
<i class="fas fa-trash mr-1.5" style="font-size: 9px;"></i>
Delete Selected
</button>
<button type="button" onclick="clearSSLSelection()" class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-gray-700 dark:text-slate-300 text-xs rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors font-medium">
<i class="fas fa-times mr-1.5" style="font-size: 9px;"></i>
Clear Selection
</button>
</div>
</div>
</div>
<!-- SSL Statistics -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-3 mb-3">
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-3">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase">Total</p>
<p class="text-lg font-semibold text-gray-900 dark:text-white mt-0.5">3</p>
</div>
<i class="fas fa-lock text-gray-400 dark:text-slate-500" style="font-size: 18px;"></i>
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-3">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase">Valid</p>
<p class="text-lg font-semibold text-green-600 dark:text-green-400 mt-0.5">2</p>
</div>
<i class="fas fa-check-circle text-green-500 dark:text-green-400" style="font-size: 18px;"></i>
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-3">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase">Expiring Soon</p>
<p class="text-lg font-semibold text-orange-600 dark:text-orange-400 mt-0.5">1</p>
</div>
<i class="fas fa-exclamation-triangle text-orange-500 dark:text-orange-400" style="font-size: 18px;"></i>
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-3">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase">Invalid</p>
<p class="text-lg font-semibold text-red-600 dark:text-red-400 mt-0.5">1</p>
</div>
<i class="fas fa-times-circle text-red-500 dark:text-red-400" style="font-size: 18px;"></i>
</div>
</div>
</div>
<!-- Pagination Info -->
<div class="mb-3 flex justify-between items-center">
<div class="text-xs text-gray-600 dark:text-slate-400">
Showing <span class="font-semibold text-gray-900 dark:text-white">1</span> to <span class="font-semibold text-gray-900 dark:text-white">3</span> of <span class="font-semibold text-gray-900 dark:text-white">3</span> certificate(s)
</div>
<div class="flex items-center gap-2">
<label class="text-xs text-gray-600 dark:text-slate-400">Show:</label>
<select class="px-2 py-1 border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-900 text-gray-900 dark:text-white rounded text-xs">
<option>10</option>
<option selected>25</option>
<option>50</option>
</select>
</div>
</div>
<!-- SSL Certificates List -->
<div class="space-y-3">
<!-- Cert 1 (root) -->
<div class="bg-white dark:bg-slate-800 rounded-lg border-2 border-green-200 dark:border-green-800 overflow-hidden ssl-cert-item" data-cert-id="1" data-status="valid">
<div class="px-4 py-2 bg-green-50 dark:bg-green-500/10 border-b border-green-200 dark:border-green-800">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<i class="fas fa-lock text-green-600 dark:text-green-400 mr-2" style="font-size: 14px;"></i>
<div>
<h3 class="text-xs font-semibold text-gray-900 dark:text-white">example.com <span class="ml-2 px-1.5 py-0.5 bg-primary text-white text-xs font-semibold rounded">Root</span></h3>
<p class="text-xs text-gray-500 dark:text-slate-400 mt-0.5">Certificate monitoring active</p>
</div>
</div>
<span class="inline-flex items-center px-2 py-1 bg-green-100 dark:bg-green-500/10 text-green-800 dark:text-green-400 text-xs font-semibold rounded border border-green-200 dark:border-green-800">
<i class="fas fa-check-circle mr-1" style="font-size: 9px;"></i>
Valid & Trusted
</span>
{% if not sslMonitoringEnabled %}
<div class="bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/30 rounded-lg p-4">
<div class="flex items-start">
<i class="fas fa-pause-circle text-amber-500 dark:text-amber-400 mt-0.5 mr-3" style="font-size: 18px;"></i>
<div>
<h3 class="text-sm font-semibold text-amber-800 dark:text-amber-300">SSL monitoring is disabled</h3>
<p class="text-xs text-amber-700 dark:text-amber-400 mt-1">This domain is not checked by the SSL cron. Enable it in Edit to monitor the root certificate and tracked SSL endpoints.</p>
<a href="/domains/{{ domain.id }}/edit#ssl-monitoring" class="inline-flex items-center mt-2 text-xs font-medium text-amber-700 dark:text-amber-300 hover:text-amber-900 dark:hover:text-amber-100">
<i class="fas fa-edit mr-1"></i>Enable SSL monitoring in Edit
</a>
</div>
</div>
<div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-3">
<div>
<label class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Validity Period</label>
<div class="mt-1.5 space-y-1.5">
<div class="flex justify-between items-center"><span class="text-xs text-gray-600 dark:text-slate-400">Issued:</span><span class="text-xs font-medium text-gray-900 dark:text-white">Oct 05, 2025</span></div>
<div class="flex justify-between items-center"><span class="text-xs text-gray-600 dark:text-slate-400">Expires:</span><span class="text-xs font-semibold text-green-700 dark:text-green-400">Jan 08, 2026</span></div>
<div class="flex justify-between items-center"><span class="text-xs text-gray-600 dark:text-slate-400">Valid for:</span><span class="text-xs font-bold text-green-600 dark:text-green-400">65 days</span></div>
</div>
</div>
<div>
<label class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Certificate Authority</label>
<div class="mt-1.5">
<p class="text-xs text-gray-900 dark:text-white font-medium">Let's Encrypt Authority X3</p>
<p class="text-xs text-gray-500 dark:text-slate-400 mt-0.5">✓ Trusted CA</p>
</div>
</div>
</div>
<div class="space-y-3">
<div>
<label class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Covered Domains (SANs)</label>
<div class="mt-1.5 space-y-1">
<div class="flex items-center text-xs"><i class="fas fa-check text-green-500 dark:text-green-400 mr-1.5" style="font-size: 9px;"></i><span class="text-gray-900 dark:text-white font-mono">example.com</span></div>
<div class="flex items-center text-xs"><i class="fas fa-check text-green-500 dark:text-green-400 mr-1.5" style="font-size: 9px;"></i><span class="text-gray-900 dark:text-white font-mono">www.example.com</span></div>
</div>
</div>
<div>
<label class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Security Details</label>
<div class="mt-1.5 space-y-1.5">
<div class="flex justify-between"><span class="text-xs text-gray-600 dark:text-slate-400">Signature:</span><span class="text-xs font-medium text-gray-900 dark:text-white">SHA256-RSA</span></div>
<div class="flex justify-between"><span class="text-xs text-gray-600 dark:text-slate-400">Key Size:</span><span class="text-xs font-medium text-gray-900 dark:text-white">2048 bits</span></div>
<div class="flex justify-between"><span class="text-xs text-gray-600 dark:text-slate-400">Version:</span><span class="text-xs font-medium text-gray-900 dark:text-white">v3</span></div>
</div>
</div>
</div>
</div>
{% else %}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-4">
<div class="flex flex-col xl:flex-row xl:items-center xl:justify-between gap-3">
<div>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">SSL certificate monitoring</h3>
<p class="text-xs text-gray-500 dark:text-slate-400 mt-1">
Track the default root certificate and any monitored HTTPS endpoints, including custom ports.
</p>
</div>
<div class="flex items-center justify-between mt-4 pt-4 border-t border-gray-200 dark:border-slate-700">
<div class="text-xs text-gray-500 dark:text-slate-400"><i class="far fa-clock mr-1"></i>Last checked: Today 10:00</div>
<div class="flex gap-2">
<button class="inline-flex items-center px-2 py-1 bg-green-600 text-white text-xs rounded hover:bg-green-700 transition-colors font-medium"><i class="fas fa-sync-alt mr-1" style="font-size: 9px;"></i>Check Now</button>
</div>
<div class="flex flex-wrap gap-2">
<form method="POST" action="/domains/{{ domain.id }}/ssl/add" class="inline">
{{ csrf_field()|raw }}
<input type="hidden" name="hostname" value="@">
<button type="submit" class="inline-flex items-center px-3 py-2 bg-green-600 text-white text-xs rounded-lg hover:bg-green-700 transition-colors font-medium">
<i class="fas fa-shield-alt mr-1.5" style="font-size: 10px;"></i>
{{ rootCertificate ? 'Check Root SSL (443)' : 'Start Root Monitoring (443)' }}
</button>
</form>
<form method="POST" action="/domains/{{ domain.id }}/ssl/refresh-all" class="inline">
{{ csrf_field()|raw }}
<button type="submit" class="inline-flex items-center px-3 py-2 bg-green-600 text-white text-xs rounded-lg hover:bg-green-700 transition-colors font-medium">
<i class="fas fa-sync-alt mr-1.5" style="font-size: 10px;"></i>
Check All
</button>
</form>
<button type="button" onclick="openAddSslEndpointModal()" class="inline-flex items-center px-3 py-2 bg-primary text-white text-xs rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-plus mr-1.5" style="font-size: 10px;"></i>
Add Endpoint
</button>
</div>
</div>
</div>
<!-- Cert 2 (mail subdomain) -->
<div class="bg-white dark:bg-slate-800 rounded-lg border-2 border-green-200 dark:border-green-800 overflow-hidden ssl-cert-item" data-cert-id="2" data-status="valid">
<div class="px-4 py-2 bg-green-50 dark:bg-green-500/10 border-b border-green-200 dark:border-green-800">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<input type="checkbox" class="ssl-checkbox rounded border-gray-300 dark:border-slate-600 text-primary focus:ring-primary" value="2" onchange="updateSSLBulkActions()">
<i class="fas fa-lock text-green-600 dark:text-green-400 mr-2" style="font-size: 14px;"></i>
<div>
<h3 class="text-xs font-semibold text-gray-900 dark:text-white">mail.example.com</h3>
<p class="text-xs text-gray-500 dark:text-slate-400 mt-0.5">Certificate monitoring active</p>
</div>
</div>
<span class="inline-flex items-center px-2 py-1 bg-green-100 dark:bg-green-500/10 text-green-800 dark:text-green-400 text-xs font-semibold rounded border border-green-200 dark:border-green-800">
<i class="fas fa-check-circle mr-1" style="font-size: 9px;"></i>
Valid & Trusted
</span>
</div>
{% if sslStats.total > 0 %}
<div class="grid grid-cols-2 xl:grid-cols-5 gap-3">
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-3">
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase">Total</p>
<p class="text-lg font-semibold text-gray-900 dark:text-white mt-1">{{ sslStats.total }}</p>
</div>
<div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-3">
<div>
<label class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Validity Period</label>
<div class="mt-1.5 space-y-1.5">
<div class="flex justify-between items-center"><span class="text-xs text-gray-600 dark:text-slate-400">Issued:</span><span class="text-xs font-medium text-gray-900 dark:text-white">Aug 01, 2025</span></div>
<div class="flex justify-between items-center"><span class="text-xs text-gray-600 dark:text-slate-400">Expires:</span><span class="text-xs font-semibold text-green-700 dark:text-green-400">Jul 28, 2026</span></div>
<div class="flex justify-between items-center"><span class="text-xs text-gray-600 dark:text-slate-400">Valid for:</span><span class="text-xs font-bold text-green-600 dark:text-green-400">270 days</span></div>
</div>
</div>
<div>
<label class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Certificate Authority</label>
<div class="mt-1.5">
<p class="text-xs text-gray-900 dark:text-white font-medium">DigiCert Inc.</p>
<p class="text-xs text-gray-500 dark:text-slate-400 mt-0.5">✓ Trusted CA</p>
</div>
</div>
</div>
<div class="space-y-3">
<div>
<label class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Covered Domains (SANs)</label>
<div class="mt-1.5 space-y-1">
<div class="flex items-center text-xs"><i class="fas fa-check text-green-500 dark:text-green-400 mr-1.5" style="font-size: 9px;"></i><span class="text-gray-900 dark:text-white font-mono">mail.example.com</span></div>
<div class="flex items-center text-xs"><i class="fas fa-check text-green-500 dark:text-green-400 mr-1.5" style="font-size: 9px;"></i><span class="text-gray-900 dark:text-white font-mono">smtp.example.com</span></div>
<div class="flex items-center text-xs"><i class="fas fa-check text-green-500 dark:text-green-400 mr-1.5" style="font-size: 9px;"></i><span class="text-gray-900 dark:text-white font-mono">imap.example.com</span></div>
</div>
</div>
<div>
<label class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Security Details</label>
<div class="mt-1.5 space-y-1.5">
<div class="flex justify-between"><span class="text-xs text-gray-600 dark:text-slate-400">Signature:</span><span class="text-xs font-medium text-gray-900 dark:text-white">SHA256-RSA</span></div>
<div class="flex justify-between"><span class="text-xs text-gray-600 dark:text-slate-400">Key Size:</span><span class="text-xs font-medium text-gray-900 dark:text-white">2048 bits</span></div>
<div class="flex justify-between"><span class="text-xs text-gray-600 dark:text-slate-400">Version:</span><span class="text-xs font-medium text-gray-900 dark:text-white">v3</span></div>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-between mt-4 pt-4 border-t border-gray-200 dark:border-slate-700">
<div class="text-xs text-gray-500 dark:text-slate-400"><i class="far fa-clock mr-1"></i>Last checked: Today 10:00</div>
<div class="flex gap-2">
<button class="inline-flex items-center px-2 py-1 bg-green-600 text-white text-xs rounded hover:bg-green-700 transition-colors font-medium"><i class="fas fa-sync-alt mr-1" style="font-size: 9px;"></i>Check Now</button>
<button class="inline-flex items-center px-2 py-1 bg-red-600 text-white text-xs rounded hover:bg-red-700 transition-colors font-medium"><i class="fas fa-trash mr-1" style="font-size: 9px;"></i>Remove</button>
</div>
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-3">
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase">Valid</p>
<p class="text-lg font-semibold text-green-600 dark:text-green-400 mt-1">{{ sslStats.valid }}</p>
</div>
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-3">
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase">Expiring</p>
<p class="text-lg font-semibold text-amber-600 dark:text-amber-400 mt-1">{{ sslStats.expiring }}</p>
</div>
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-3">
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase">Expired</p>
<p class="text-lg font-semibold text-red-600 dark:text-red-400 mt-1">{{ sslStats.expired }}</p>
</div>
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-3">
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase">Invalid</p>
<p class="text-lg font-semibold text-red-600 dark:text-red-400 mt-1">{{ sslStats.invalid }}</p>
</div>
</div>
<div class="flex items-center justify-between text-xs text-gray-500 dark:text-slate-400">
<span>
<i class="far fa-clock mr-1"></i>
Last checked: {{ domain.ssl_last_checked ? domain.ssl_last_checked|date('M d, Y H:i') : 'Never' }}
</span>
</div>
<div class="flex flex-col lg:flex-row gap-3 lg:items-center lg:justify-between">
<div class="relative flex-1 max-w-md">
<input
type="text"
id="ssl-search"
placeholder="Search monitored endpoints..."
class="w-full pl-9 pr-3 py-2 border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-900 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm"
>
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-slate-500 text-xs"></i>
</div>
<div class="flex flex-wrap gap-2">
<select id="ssl-filter" class="px-3 py-2 border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-900 text-gray-900 dark:text-white rounded-lg text-sm">
<option value="all">All certificates</option>
<option value="valid">Valid only</option>
<option value="expiring">Expiring soon</option>
<option value="expired">Expired</option>
<option value="invalid">Invalid</option>
</select>
</div>
</div>
<div id="ssl-bulk-actions" class="hidden bg-blue-50 dark:bg-blue-500/10 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<span id="ssl-selected-count" class="text-xs font-medium text-blue-900 dark:text-blue-300"></span>
<div class="flex flex-wrap gap-2">
<button type="button" onclick="submitBulkSslAction('refresh')" class="inline-flex items-center px-3 py-1.5 bg-green-600 text-white text-xs rounded-lg hover:bg-green-700 transition-colors font-medium">
<i class="fas fa-sync-alt mr-1.5" style="font-size: 9px;"></i>
Check Selected
</button>
<button type="button" onclick="submitBulkSslAction('delete')" class="inline-flex items-center px-3 py-1.5 bg-red-600 text-white text-xs rounded-lg hover:bg-red-700 transition-colors font-medium">
<i class="fas fa-trash mr-1.5" style="font-size: 9px;"></i>
Delete Selected
</button>
<button type="button" onclick="clearSSLSelection()" class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-gray-700 dark:text-slate-300 text-xs rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors font-medium">
<i class="fas fa-times mr-1.5" style="font-size: 9px;"></i>
Clear Selection
</button>
</div>
</div>
</div>
<!-- Cert 3 (api - expired) -->
<div class="bg-white dark:bg-slate-800 rounded-lg border-2 border-red-200 dark:border-red-800 overflow-hidden ssl-cert-item" data-cert-id="3" data-status="expired">
<div class="px-4 py-2 bg-red-50 dark:bg-red-500/10 border-b border-red-200 dark:border-red-800">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<input type="checkbox" class="ssl-checkbox rounded border-gray-300 dark:border-slate-600 text-primary focus:ring-primary" value="3" onchange="updateSSLBulkActions()">
<i class="fas fa-lock text-red-600 dark:text-red-400 mr-2" style="font-size: 14px;"></i>
<div>
<h3 class="text-xs font-semibold text-gray-900 dark:text-white">api.example.com</h3>
<p class="text-xs text-gray-500 dark:text-slate-400 mt-0.5">Certificate monitoring active</p>
<form method="POST" action="/domains/{{ domain.id }}/ssl/bulk-refresh" id="ssl-bulk-refresh-form" class="hidden">
{{ csrf_field()|raw }}
<input type="hidden" name="certificate_ids" id="ssl-bulk-refresh-ids">
</form>
<form method="POST" action="/domains/{{ domain.id }}/ssl/bulk-delete" id="ssl-bulk-delete-form" class="hidden">
{{ csrf_field()|raw }}
<input type="hidden" name="certificate_ids" id="ssl-bulk-delete-ids">
</form>
<div id="ssl-no-results" class="hidden bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-6 text-center">
<i class="fas fa-search text-gray-300 dark:text-slate-600 mb-2" style="font-size: 28px;"></i>
<p class="text-sm font-semibold text-gray-900 dark:text-white">No certificates match the current filter</p>
<p class="text-xs text-gray-500 dark:text-slate-400 mt-1">Try a different search term or status filter.</p>
</div>
<div id="ssl-list" class="space-y-3">
{% for certificate in sslCertificates %}
{% set validityText = 'Unknown' %}
{% if certificate.days_remaining is not null %}
{% if certificate.days_remaining < 0 %}
{% set validityText = (certificate.days_remaining|abs) ~ ' days ago' %}
{% else %}
{% set validityText = certificate.days_remaining ~ ' days' %}
{% endif %}
{% endif %}
<div
class="ssl-cert-item bg-white dark:bg-slate-800 rounded-lg border-2 {{ certificate.card_border_class }} overflow-hidden"
data-status="{{ certificate.status }}"
data-search="{{ (certificate.display_target ~ ' ' ~ certificate.hostname ~ ' ' ~ (certificate.issuer_name|default('')) ~ ' ' ~ (certificate.issuer_organization|default('')) ~ ' ' ~ (certificate.subject_name|default('')) ~ ' ' ~ (certificate.subject_organization|default('')))|lower }}"
>
<div class="px-4 py-2 border-b {{ certificate.header_class }}">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3">
<div class="flex items-center gap-3">
{% if certificate.can_delete %}
<input type="checkbox" class="ssl-checkbox rounded border-gray-300 dark:border-slate-600 text-primary focus:ring-primary" value="{{ certificate.id }}" onchange="updateSSLBulkActions()">
{% endif %}
<i class="fas fa-lock {{ certificate.accent_class }}" style="font-size: 14px;"></i>
<div>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{{ certificate.display_target }}
{% if certificate.is_root %}
<span class="ml-2 px-1.5 py-0.5 bg-primary text-white text-xs font-semibold rounded">Root</span>
{% endif %}
</h3>
<p class="text-xs text-gray-500 dark:text-slate-400 mt-0.5">
{{ certificate.is_trusted ? 'Trusted certificate' : 'Certificate issue detected' }}
</p>
</div>
</div>
<span class="inline-flex items-center px-2 py-1 text-xs font-semibold rounded border {{ certificate.status_badge_class }}">
<i class="fas {{ certificate.status_icon }} mr-1" style="font-size: 9px;"></i>
{{ certificate.status_label }}
</span>
</div>
</div>
<div class="p-4">
<div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
<div class="space-y-3">
<div>
<label class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Validity period</label>
<div class="mt-1.5 space-y-1.5">
<div class="flex justify-between items-center">
<span class="text-xs text-gray-600 dark:text-slate-400">Issued</span>
<span class="text-xs font-medium text-gray-900 dark:text-white">
{{ certificate.valid_from ? certificate.valid_from|date('M d, Y H:i') : 'Unknown' }}
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-xs text-gray-600 dark:text-slate-400">Expires</span>
<span class="text-xs font-semibold {{ certificate.accent_class }}">
{{ certificate.valid_to ? certificate.valid_to|date('M d, Y H:i') : 'Unknown' }}
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-xs text-gray-600 dark:text-slate-400">Time left</span>
<span class="text-xs font-semibold {{ certificate.accent_class }}">
{% if certificate.days_remaining is not null and certificate.days_remaining < 0 %}
Expired {{ validityText }}
{% else %}
{{ validityText }}
{% endif %}
</span>
</div>
</div>
</div>
<div>
<label class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Certificate authority</label>
<div class="mt-1.5">
<p class="text-xs font-medium text-gray-900 dark:text-white">{{ certificate.issuer_name|default('Unknown issuer') }}</p>
{% if certificate.issuer_organization and certificate.issuer_organization != certificate.issuer_name %}
<div class="flex justify-between items-center mt-1">
<span class="text-xs text-gray-600 dark:text-slate-400">Organization</span>
<span class="text-xs font-medium text-gray-900 dark:text-white text-right max-w-[60%] break-all">{{ certificate.issuer_organization }}</span>
</div>
{% endif %}
<p class="text-xs text-gray-500 dark:text-slate-400 mt-0.5">
{% if certificate.is_self_signed %}
Self-signed
{% elseif certificate.is_trusted %}
Trusted
{% else %}
Not trusted
{% endif %}
</p>
</div>
</div>
{% if certificate.last_error %}
<div class="bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-800 rounded p-2">
<p class="text-xs font-semibold text-red-900 dark:text-red-300 mb-0.5">Error details</p>
<p class="text-xs text-red-700 dark:text-red-400">{{ certificate.last_error }}</p>
</div>
{% endif %}
</div>
<div class="space-y-3">
<div>
<label class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Covered domains (SANs)</label>
<div class="mt-1.5 space-y-1 max-h-28 overflow-auto pr-1">
{% if certificate.san_list is not empty %}
{% for san in certificate.san_list %}
<div class="flex items-center text-xs">
<i class="fas fa-check {{ certificate.accent_class }} mr-1.5" style="font-size: 9px;"></i>
<span class="text-gray-900 dark:text-white font-mono break-all">{{ san }}</span>
</div>
{% endfor %}
{% else %}
<p class="text-xs text-gray-500 dark:text-slate-400">No SAN entries recorded.</p>
{% endif %}
</div>
</div>
<div>
<label class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Security details</label>
<div class="mt-1.5 space-y-1.5">
<div class="flex justify-between">
<span class="text-xs text-gray-600 dark:text-slate-400">Subject</span>
<span class="text-xs font-medium text-gray-900 dark:text-white text-right max-w-[60%] break-all">{{ certificate.subject_name|default('Unknown') }}</span>
</div>
{% if certificate.subject_organization and certificate.subject_organization != certificate.subject_name %}
<div class="flex justify-between">
<span class="text-xs text-gray-600 dark:text-slate-400">Subject org</span>
<span class="text-xs font-medium text-gray-900 dark:text-white text-right max-w-[60%] break-all">{{ certificate.subject_organization }}</span>
</div>
{% endif %}
<div class="flex justify-between">
<span class="text-xs text-gray-600 dark:text-slate-400">Signature</span>
<span class="text-xs font-medium text-gray-900 dark:text-white">{{ certificate.signature_algorithm|default('Unknown') }}</span>
</div>
<div class="flex justify-between">
<span class="text-xs text-gray-600 dark:text-slate-400">Key</span>
<span class="text-xs font-medium text-gray-900 dark:text-white">
{{ certificate.key_type|default('Unknown') }}{% if certificate.key_bits %} {{ certificate.key_bits }} bits{% endif %}
</span>
</div>
<div class="flex justify-between">
<span class="text-xs text-gray-600 dark:text-slate-400">Version</span>
<span class="text-xs font-medium text-gray-900 dark:text-white">{{ certificate.certificate_version|default('Unknown') }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mt-4 pt-4 border-t border-gray-200 dark:border-slate-700">
<div class="text-xs text-gray-500 dark:text-slate-400">
<i class="far fa-clock mr-1"></i>
Last checked: {{ certificate.last_checked ? certificate.last_checked|date('M d, Y H:i') : 'Never' }}
</div>
<div class="flex flex-wrap gap-2">
{% if certificate.is_root and not certificate.can_delete %}
<form method="POST" action="/domains/{{ domain.id }}/ssl/add" class="inline">
{{ csrf_field()|raw }}
<input type="hidden" name="hostname" value="@">
<button type="submit" class="inline-flex items-center px-2 py-1 bg-green-600 text-white text-xs rounded hover:bg-green-700 transition-colors font-medium">
<i class="fas fa-sync-alt mr-1" style="font-size: 9px;"></i>
Check Now
</button>
</form>
{% else %}
<form method="POST" action="/domains/{{ domain.id }}/ssl/{{ certificate.id }}/refresh" class="inline">
{{ csrf_field()|raw }}
<button type="submit" class="inline-flex items-center px-2 py-1 bg-green-600 text-white text-xs rounded hover:bg-green-700 transition-colors font-medium">
<i class="fas fa-sync-alt mr-1" style="font-size: 9px;"></i>
Check Now
</button>
</form>
<form method="POST" action="/domains/{{ domain.id }}/ssl/{{ certificate.id }}/delete" class="inline" onsubmit="return confirm('Remove SSL monitoring for {{ certificate.display_target }}?');">
{{ csrf_field()|raw }}
<button type="submit" class="inline-flex items-center px-2 py-1 bg-red-600 text-white text-xs rounded hover:bg-red-700 transition-colors font-medium">
<i class="fas fa-trash mr-1" style="font-size: 9px;"></i>
Remove
</button>
</form>
{% endif %}
</div>
</div>
<span class="inline-flex items-center px-2 py-1 bg-red-100 dark:bg-red-500/10 text-red-800 dark:text-red-400 text-xs font-semibold rounded border border-red-200 dark:border-red-800">
<i class="fas fa-times-circle mr-1" style="font-size: 9px;"></i>
EXPIRED
</span>
</div>
</div>
<div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-3">
<div>
<label class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Validity Period</label>
<div class="mt-1.5 space-y-1.5">
<div class="flex justify-between items-center"><span class="text-xs text-gray-600 dark:text-slate-400">Issued:</span><span class="text-xs font-medium text-gray-900 dark:text-white">Sep 26, 2024</span></div>
<div class="flex justify-between items-center"><span class="text-xs text-gray-600 dark:text-slate-400">Expires:</span><span class="text-xs font-semibold text-red-700 dark:text-red-400">Sep 30, 2025</span></div>
<div class="flex justify-between items-center"><span class="text-xs text-gray-600 dark:text-slate-400">Valid for:</span><span class="text-xs font-bold text-red-600 dark:text-red-400">30 days (expired)</span></div>
{% endfor %}
</div>
{% else %}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-8 text-center">
<i class="fas fa-lock text-gray-300 dark:text-slate-600 mb-3" style="font-size: 36px;"></i>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-1">No SSL certificates monitored yet</h3>
<p class="text-xs text-gray-500 dark:text-slate-400 mb-4">
Start with the root domain on port 443, or add specific hosts and custom HTTPS ports you want to monitor.
</p>
<div class="flex flex-wrap justify-center gap-2">
<form method="POST" action="/domains/{{ domain.id }}/ssl/add" class="inline">
{{ csrf_field()|raw }}
<input type="hidden" name="hostname" value="@">
<button type="submit" class="inline-flex items-center px-4 py-2 bg-green-600 text-white text-xs rounded-lg hover:bg-green-700 transition-colors font-medium">
<i class="fas fa-shield-alt mr-1.5" style="font-size: 10px;"></i>
Check Root SSL (443)
</button>
</form>
<form method="POST" action="/domains/{{ domain.id }}/ssl/refresh-all" class="inline">
{{ csrf_field()|raw }}
<button type="submit" class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-900 text-gray-700 dark:text-slate-300 text-xs rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors font-medium">
<i class="fas fa-sync-alt mr-1.5" style="font-size: 10px;"></i>
Check All
</button>
</form>
<button type="button" onclick="openAddSslEndpointModal()" class="inline-flex items-center px-4 py-2 bg-primary text-white text-xs rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-plus mr-1.5" style="font-size: 10px;"></i>
Add Endpoint
</button>
</div>
</div>
{% endif %}
{# Add SSL Endpoint Modal #}
<div id="addSslEndpointModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white dark:bg-slate-800 rounded-lg shadow-xl max-w-md w-full" onclick="event.stopPropagation()">
<form method="POST" action="/domains/{{ domain.id }}/ssl/add">
{{ csrf_field()|raw }}
<div class="px-6 py-4 border-b border-gray-200 dark:border-slate-700">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Add SSL Endpoint</h3>
</div>
<div class="px-6 py-4 space-y-4">
<div>
<label for="ssl-endpoint-hostname" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1">Endpoint</label>
<input type="text" id="ssl-endpoint-hostname" name="hostname" required
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary bg-white dark:bg-slate-700 text-gray-900 dark:text-white"
placeholder="mail, mail:8443, or mail.{{ domain.domain_name }}:8443">
<p class="text-xs text-gray-500 dark:text-slate-400 mt-1">
Use <code class="bg-gray-100 dark:bg-slate-700 px-1 rounded">@</code> for root, <code class="bg-gray-100 dark:bg-slate-700 px-1 rounded">@:8443</code> for root on custom port, or <code class="bg-gray-100 dark:bg-slate-700 px-1 rounded">mail</code>, <code class="bg-gray-100 dark:bg-slate-700 px-1 rounded">mail:8443</code>, or full hostname under {{ domain.domain_name }}.
</p>
</div>
</div>
<div>
<label class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Certificate Authority</label>
<div class="mt-1.5">
<p class="text-xs text-gray-900 dark:text-white font-medium">Self-Signed</p>
<p class="text-xs text-gray-500 dark:text-slate-400 mt-0.5">⚠️ Not Trusted</p>
</div>
<div class="px-6 py-4 bg-gray-50 dark:bg-slate-900 flex justify-end space-x-3 rounded-b-lg">
<button type="button" onclick="closeAddSslEndpointModal()"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-slate-300 bg-white dark:bg-slate-700 border border-gray-300 dark:border-slate-600 rounded-md hover:bg-gray-50 dark:hover:bg-slate-600">
Cancel
</button>
<button type="submit"
class="px-4 py-2 text-sm font-medium text-white bg-primary rounded-md hover:bg-primary-dark">
Add Endpoint
</button>
</div>
<div class="bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-800 rounded p-2">
<p class="text-xs font-semibold text-red-900 dark:text-red-300 mb-0.5">Error Details</p>
<p class="text-xs text-red-700 dark:text-red-400">Certificate has expired</p>
</div>
</div>
<div class="space-y-3">
<div>
<label class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Covered Domains (SANs)</label>
<div class="mt-1.5 space-y-1">
<div class="flex items-center text-xs"><i class="fas fa-check text-green-500 dark:text-green-400 mr-1.5" style="font-size: 9px;"></i><span class="text-gray-900 dark:text-white font-mono">api.example.com</span></div>
</div>
</div>
<div>
<label class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Security Details</label>
<div class="mt-1.5 space-y-1.5">
<div class="flex justify-between"><span class="text-xs text-gray-600 dark:text-slate-400">Signature:</span><span class="text-xs font-medium text-gray-900 dark:text-white">SHA256-RSA</span></div>
<div class="flex justify-between"><span class="text-xs text-gray-600 dark:text-slate-400">Key Size:</span><span class="text-xs font-medium text-gray-900 dark:text-white">2048 bits</span></div>
<div class="flex justify-between"><span class="text-xs text-gray-600 dark:text-slate-400">Version:</span><span class="text-xs font-medium text-gray-900 dark:text-white">v3</span></div>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-between mt-4 pt-4 border-t border-gray-200 dark:border-slate-700">
<div class="text-xs text-gray-500 dark:text-slate-400"><i class="far fa-clock mr-1"></i>Last checked: Today 11:00</div>
<div class="flex gap-2">
<button class="inline-flex items-center px-2 py-1 bg-green-600 text-white text-xs rounded hover:bg-green-700 transition-colors font-medium"><i class="fas fa-sync-alt mr-1" style="font-size: 9px;"></i>Check Now</button>
<button class="inline-flex items-center px-2 py-1 bg-red-600 text-white text-xs rounded hover:bg-red-700 transition-colors font-medium"><i class="fas fa-trash mr-1" style="font-size: 9px;"></i>Remove</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
</div>
<!-- Pagination Controls -->
<div class="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="text-sm text-gray-600 dark:text-slate-400">Page <span class="font-semibold text-gray-900 dark:text-white">1</span> of <span class="font-semibold text-gray-900 dark:text-white">1</span></div>
<div class="flex items-center gap-1">
<button disabled class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg bg-gray-100 dark:bg-slate-700 text-gray-400 dark:text-slate-500 cursor-not-allowed"><i class="fas fa-angle-double-left"></i></button>
<button disabled class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg bg-gray-100 dark:bg-slate-700 text-gray-400 dark:text-slate-500 cursor-not-allowed"><i class="fas fa-angle-left"></i> Previous</button>
<span class="px-3 py-2 text-sm bg-primary text-white rounded-lg font-semibold">1</span>
<button disabled class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg bg-gray-100 dark:bg-slate-700 text-gray-400 dark:text-slate-500 cursor-not-allowed">Next <i class="fas fa-angle-right"></i></button>
<button disabled class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg bg-gray-100 dark:bg-slate-700 text-gray-400 dark:text-slate-500 cursor-not-allowed"><i class="fas fa-angle-double-right"></i></button>
</div>
</div>
<script>
function getSelectedSSLIds() {
return Array.from(document.querySelectorAll('.ssl-checkbox:checked')).map((checkbox) => checkbox.value);
}
function updateSSLBulkActions() {
const checkboxes = document.querySelectorAll('.ssl-checkbox:checked');
const selectedIds = getSelectedSSLIds();
const bulkActions = document.getElementById('ssl-bulk-actions');
const selectedCount = document.getElementById('ssl-selected-count');
if (checkboxes.length > 0) {
if (!bulkActions || !selectedCount) {
return;
}
if (selectedIds.length > 0) {
bulkActions.classList.remove('hidden');
selectedCount.textContent = `${checkboxes.length} certificate(s) selected`;
selectedCount.textContent = `${selectedIds.length} endpoint(s) selected`;
} else {
bulkActions.classList.add('hidden');
selectedCount.textContent = '';
}
}
function clearSSLSelection() {
document.querySelectorAll('.ssl-checkbox').forEach(cb => cb.checked = false);
document.querySelectorAll('.ssl-checkbox').forEach((checkbox) => {
checkbox.checked = false;
});
updateSSLBulkActions();
}
function getSelectedSSLIds() {
return Array.from(document.querySelectorAll('.ssl-checkbox:checked')).map(cb => cb.value);
}
function bulkCheckSSL() {
const ids = getSelectedSSLIds();
console.log('Checking SSL certificates:', ids);
}
function bulkDeleteSSL() {
const ids = getSelectedSSLIds();
if (confirm(`Delete ${ids.length} certificate(s)? This action cannot be undone.`)) {
console.log('Deleting SSL certificates:', ids);
function submitBulkSslAction(action) {
const selectedIds = getSelectedSSLIds();
if (selectedIds.length === 0) {
return;
}
if (action === 'delete' && !window.confirm(`Remove SSL monitoring for ${selectedIds.length} endpoint(s)?`)) {
return;
}
const input = document.getElementById(`ssl-bulk-${action}-ids`);
const form = document.getElementById(`ssl-bulk-${action}-form`);
if (!input || !form) {
return;
}
input.value = selectedIds.join(',');
form.submit();
}
function checkAllCertificates() {
console.log('Checking all certificates...');
}
document.getElementById('ssl-search')?.addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
document.querySelectorAll('.ssl-cert-item').forEach(item => {
const text = item.textContent.toLowerCase();
item.style.display = text.includes(searchTerm) ? '' : 'none';
});
});
document.getElementById('ssl-filter')?.addEventListener('change', function(e) {
const filter = e.target.value;
document.querySelectorAll('.ssl-cert-item').forEach(item => {
if (filter === 'all') {
item.style.display = '';
} else {
const status = item.dataset.status;
item.style.display = status === filter ? '' : 'none';
function applySslFilters() {
const searchInput = document.getElementById('ssl-search');
const filterSelect = document.getElementById('ssl-filter');
const noResults = document.getElementById('ssl-no-results');
const items = document.querySelectorAll('.ssl-cert-item');
if (!searchInput || !filterSelect || items.length === 0) {
return;
}
const searchTerm = searchInput.value.trim().toLowerCase();
const filter = filterSelect.value;
let visibleCount = 0;
items.forEach((item) => {
const haystack = item.dataset.search || '';
const status = item.dataset.status || '';
const matchesSearch = searchTerm === '' || haystack.includes(searchTerm);
const matchesFilter = filter === 'all' || status === filter;
const visible = matchesSearch && matchesFilter;
item.style.display = visible ? '' : 'none';
if (visible) {
visibleCount += 1;
}
});
if (noResults) {
noResults.classList.toggle('hidden', visibleCount > 0);
}
}
document.getElementById('ssl-search')?.addEventListener('input', applySslFilters);
document.getElementById('ssl-filter')?.addEventListener('change', applySslFilters);
function openAddSslEndpointModal() {
document.getElementById('addSslEndpointModal')?.classList.remove('hidden');
}
function closeAddSslEndpointModal() {
const modal = document.getElementById('addSslEndpointModal');
modal?.classList.add('hidden');
modal?.querySelector('form')?.reset();
}
document.getElementById('addSslEndpointModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closeAddSslEndpointModal();
}
});
</script>