Files
domnitor/app/Views/domains/tabs/ssl.twig
Hosteroid 5916daa293 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.
2026-03-08 21:12:09 +02:00

508 lines
30 KiB
Twig

{% 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 %}
<div class="space-y-3">
{% 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>
{% 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 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>
{% 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="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>
<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>
</div>
</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 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>
</form>
</div>
</div>
</div>
{% endif %}
</div>
<script>
function getSelectedSSLIds() {
return Array.from(document.querySelectorAll('.ssl-checkbox:checked')).map((checkbox) => checkbox.value);
}
function updateSSLBulkActions() {
const selectedIds = getSelectedSSLIds();
const bulkActions = document.getElementById('ssl-bulk-actions');
const selectedCount = document.getElementById('ssl-selected-count');
if (!bulkActions || !selectedCount) {
return;
}
if (selectedIds.length > 0) {
bulkActions.classList.remove('hidden');
selectedCount.textContent = `${selectedIds.length} endpoint(s) selected`;
} else {
bulkActions.classList.add('hidden');
selectedCount.textContent = '';
}
}
function clearSSLSelection() {
document.querySelectorAll('.ssl-checkbox').forEach((checkbox) => {
checkbox.checked = false;
});
updateSSLBulkActions();
}
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 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>