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.
2026-03-03 18:21:32 +02:00
{% extends "layout/base.twig" %}
{% set title = 'Notifications' %}
{% set pageTitle = 'Notifications' %}
{% set pageDescription = 'View and manage your notifications' %}
{% set pageIcon = 'fas fa-bell' %}
{% block content %}
{% set filterType = filters .type ? ? '' %}
{% set filterStatus = filters .status ? ? '' %}
{% set filterDateRange = filters .date_range ? ? '' %}
{% set page = pagination .current_page %}
{% set totalPages = pagination .total_pages %}
{% set perPage = pagination .per_page %}
{% set totalNotifications = pagination .total %}
{% set offset = pagination .showing_from - 1 %}
<!-- Action Buttons -->
<div class="mb-4 flex flex-wrap gap-2 justify-between items-center">
<div class="flex gap-2">
</div>
<div class="flex gap-2">
<button onclick="markAllAsRead()" class="inline-flex items-center px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700 transition-colors font-medium">
<i class="fas fa-check-double mr-2"></i>
Mark All Read
</button>
<button onclick="clearAll()" class="inline-flex items-center px-4 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors font-medium">
<i class="fas fa-trash-alt mr-2"></i>
Clear All
</button>
</div>
</div>
<!-- Filters & Search -->
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-5 mb-4">
<form method="GET" action="/notifications" id="filter-form">
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
<!-- Status Filter -->
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-slate-300 mb-1.5">Status</label>
<select name="status" class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg text-sm focus:ring-2 focus:ring-primary focus:border-primary bg-white dark:bg-slate-700 text-gray-900 dark:text-white">
<option value="">All Notifications</option>
<option value="unread" {{ filterStatus == 'unread' ? 'selected' : '' }} >Unread Only</option>
<option value="read" {{ filterStatus == 'read' ? 'selected' : '' }} >Read Only</option>
</select>
</div>
<!-- Type Filter -->
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-slate-300 mb-1.5">Type</label>
<select name="type" class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg text-sm focus:ring-2 focus:ring-primary focus:border-primary bg-white dark:bg-slate-700 text-gray-900 dark:text-white">
<option value="">All Types</option>
<optgroup label="Domain">
<option value="domain_expiring" {{ filterType == 'domain_expiring' ? 'selected' : '' }} >Domain Expiring</option>
<option value="domain_expired" {{ filterType == 'domain_expired' ? 'selected' : '' }} >Domain Expired</option>
<option value="domain_available" {{ filterType == 'domain_available' ? 'selected' : '' }} >Domain Available</option>
<option value="domain_registered" {{ filterType == 'domain_registered' ? 'selected' : '' }} >Domain Registered</option>
<option value="domain_redemption" {{ filterType == 'domain_redemption' ? 'selected' : '' }} >Redemption Period</option>
<option value="domain_pending_delete" {{ filterType == 'domain_pending_delete' ? 'selected' : '' }} >Pending Delete</option>
<option value="domain_updated" {{ filterType == 'domain_updated' ? 'selected' : '' }} >Domain Updated</option>
<option value="whois_failed" {{ filterType == 'whois_failed' ? 'selected' : '' }} >WHOIS Failed</option>
</optgroup>
<optgroup label="System">
<option value="session_new" {{ filterType == 'session_new' ? 'selected' : '' }} >New Login</option>
<option value="session_failed" {{ filterType == 'session_failed' ? 'selected' : '' }} >Failed Login</option>
<option value="system_welcome" {{ filterType == 'system_welcome' ? 'selected' : '' }} >Welcome</option>
<option value="system_upgrade" {{ filterType == 'system_upgrade' ? 'selected' : '' }} >System Upgrade</option>
<option value="update_available" {{ filterType == 'update_available' ? 'selected' : '' }} >Update Available</option>
</optgroup>
</select>
</div>
<!-- Date Range -->
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-slate-300 mb-1.5">Date Range</label>
<select name="date_range" class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg text-sm focus:ring-2 focus:ring-primary focus:border-primary bg-white dark:bg-slate-700 text-gray-900 dark:text-white">
<option value="">All Time</option>
<option value="today" {{ filterDateRange == 'today' ? 'selected' : '' }} >Today</option>
<option value="week" {{ filterDateRange == 'week' ? 'selected' : '' }} >This Week</option>
<option value="month" {{ filterDateRange == 'month' ? 'selected' : '' }} >This Month</option>
</select>
</div>
<!-- Apply/Reset Buttons -->
<div class="flex items-end space-x-2">
<button type="submit" class="flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors text-sm font-medium">
<i class="fas fa-filter mr-2"></i>
Apply Filters
</button>
<a href="/notifications" class="px-4 py-2 border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-slate-300 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-sm font-medium">
<i class="fas fa-times mr-2"></i>
Clear
</a>
</div>
</div>
</form>
</div>
<!-- Pagination Info & Per Page Selector -->
<div class="mb-4 flex justify-between items-center">
<div class="text-sm text-gray-600 dark:text-slate-400">
Showing <span class="font-semibold text-gray-900 dark:text-white"> {{ offset + 1 }} </span> to
<span class="font-semibold text-gray-900 dark:text-white"> {{ min ( offset + perPage , totalNotifications ) }} </span> of
<span class="font-semibold text-gray-900 dark:text-white"> {{ totalNotifications }} </span> notification(s)
{% if unreadCount > 0 %}
<span class="text-gray-400 dark:text-slate-500">•</span>
<span class="font-semibold text-blue-600"> {{ unreadCount }} </span> unread
{% endif %}
</div>
<form method="GET" action="/notifications" class="flex items-center gap-2">
<input type="hidden" name="status" value=" {{ filterStatus }} ">
<input type="hidden" name="type" value=" {{ filterType }} ">
<label for="per_page" class="text-sm text-gray-600 dark:text-slate-400">Show:</label>
<select name="per_page" id="per_page" onchange="this.form.submit()" class="px-3 py-1.5 border border-gray-300 dark:border-slate-600 rounded-lg text-sm focus:ring-2 focus:ring-primary focus:border-primary bg-white dark:bg-slate-700 text-gray-900 dark:text-white">
<option value="10" {{ perPage == 1 0 ? 'selected' : '' }} >10</option>
<option value="25" {{ perPage == 2 5 ? 'selected' : '' }} >25</option>
<option value="50" {{ perPage == 5 0 ? 'selected' : '' }} >50</option>
<option value="100" {{ perPage == 1 0 0 ? 'selected' : '' }} >100</option>
</select>
</form>
</div>
<!-- Notifications List -->
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
{% if notifications is not empty %}
<div class="divide-y divide-gray-100 dark:divide-slate-700">
{% for notification in notifications %}
{% set bgClass = notification .is_read ? '' : 'bg-blue-50 dark:bg-blue-500/10' %}
{% set iconBgClass = 'bg-' ~ notification .color ~ '-100' %}
{% set iconTextClass = 'text-' ~ notification .color ~ '-600' %}
{% set hasDomain = notification .domain_id is not empty %}
{% set domainUrl = hasDomain ? '/domains/' ~ notification .domain_id : null %}
{% set clickUrl = null %}
{% if notification .type == 'update_available' %}
{% set clickUrl = '/notifications/' ~ notification .id ~ '/mark-read?redirect=settings' %}
{% elseif hasDomain and not notification .is_read %}
{% set clickUrl = '/notifications/' ~ notification .id ~ '/mark-read?redirect=domain&domain_id=' ~ notification .domain_id %}
{% elseif hasDomain %}
{% set clickUrl = domainUrl %}
{% endif %}
{% set loginData = notification .login_data ? ? null %}
{% set isLogin = ( notification .type == 'session_new' and loginData ) %}
{% set isFailedLogin = ( notification .type == 'session_failed' and loginData ) %}
<div class="px-4 py-3 hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors {{ bgClass }} ">
<div class="flex items-start gap-3">
<!-- Icon -->
{% if isFailedLogin %}
<div class="w-10 h-10 bg-red-100 dark:bg-red-500/10 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-shield-alt text-red-600"></i>
</div>
{% elseif isLogin %}
<div class="w-10 h-10 bg-blue-100 dark:bg-blue-500/10 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-sign-in-alt text-blue-600"></i>
</div>
{% elseif clickUrl %}
<a href=" {{ clickUrl }} " class="w-10 h-10 {{ iconBgClass }} rounded-lg flex items-center justify-center flex-shrink-0 hover:opacity-80 transition-opacity">
<i class="fas fa- {{ notification .icon }} {{ iconTextClass }} text-sm"></i>
</a>
{% else %}
<div class="w-10 h-10 {{ iconBgClass }} rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa- {{ notification .icon }} {{ iconTextClass }} text-sm"></i>
</div>
{% endif %}
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
{% if clickUrl %}
<a href=" {{ clickUrl }} " class="text-sm font-medium text-gray-900 dark:text-white hover:text-primary transition-colors"> {{ notification .title }} </a>
{% else %}
<h3 class="text-sm font-medium text-gray-900 dark:text-white"> {{ notification .title }} </h3>
{% endif %}
{% if not notification .is_read %}
<span class="flex h-1.5 w-1.5 relative">
<span class="animate-ping absolute inline-flex h-1.5 w-1.5 rounded-full bg-blue-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-blue-500"></span>
</span>
{% endif %}
<span class="text-xs text-gray-400 dark:text-slate-500 ml-auto flex-shrink-0">
<i class="fas fa-clock mr-1"></i>
{{ notification .time_ago }}
</span>
</div>
{% if isFailedLogin %}
<!-- Rich failed login details -->
<div class="mt-1.5 bg-red-50 dark:bg-red-500/10 rounded-lg p-3 border border-red-200 dark:border-red-500/20">
<div class="grid grid-cols-2 md:grid-cols-3 gap-x-4 gap-y-2 text-xs">
<!-- Location -->
<div class="flex items-center gap-1.5">
<i class="fas fa-map-marker-alt text-red-400 w-3.5 text-center"></i>
<span class="text-gray-500 dark:text-slate-400">Location:</span>
<span class="text-gray-800 dark:text-slate-200 font-medium">
{% if ( loginData .country_code ? ? 'xx' ) != 'xx' %}
<span class="fi fi- {{ loginData .country_code | lower }} text-xs mr-0.5 rounded-sm"></span>
{% endif %}
{{ loginData .location ? ? 'Unknown' }}
</span>
</div>
<!-- IP Address -->
<div class="flex items-center gap-1.5">
<i class="fas fa-network-wired text-blue-400 w-3.5 text-center"></i>
<span class="text-gray-500 dark:text-slate-400">IP:</span>
<span class="text-gray-800 dark:text-slate-200 font-medium font-mono text-[11px]"> {{ loginData .ip ? ? 'unknown' }} </span>
</div>
<!-- Browser -->
<div class="flex items-center gap-1.5">
<i class="fas fa-globe text-green-400 w-3.5 text-center"></i>
<span class="text-gray-500 dark:text-slate-400">Browser:</span>
<span class="text-gray-800 dark:text-slate-200 font-medium"> {{ loginData .browser ? ? 'Unknown' }} </span>
</div>
<!-- Device -->
<div class="flex items-center gap-1.5">
<i class="fas fa- {{ loginData .device_icon ? ? 'desktop' }} text-purple-400 w-3.5 text-center"></i>
<span class="text-gray-500 dark:text-slate-400">Device:</span>
<span class="text-gray-800 dark:text-slate-200 font-medium"> {{ loginData .device ? ? 'Unknown' }} </span>
</div>
<!-- OS -->
<div class="flex items-center gap-1.5">
<i class="fas fa-laptop-code text-indigo-400 w-3.5 text-center"></i>
<span class="text-gray-500 dark:text-slate-400">OS:</span>
<span class="text-gray-800 dark:text-slate-200 font-medium"> {{ loginData .os ? ? 'Unknown' }} </span>
</div>
<!-- ISP -->
<div class="flex items-center gap-1.5">
<i class="fas fa-server text-amber-400 w-3.5 text-center"></i>
<span class="text-gray-500 dark:text-slate-400">ISP:</span>
<span class="text-gray-800 dark:text-slate-200 font-medium truncate"> {{ loginData .isp ? ? 'Unknown' }} </span>
</div>
</div>
<!-- Reason -->
<div class="mt-2 pt-2 border-t border-red-200 dark:border-red-500/20 flex items-center gap-1.5 text-xs">
<i class="fas fa-exclamation-triangle text-gray-400 dark:text-slate-500 w-3.5 text-center"></i>
<span class="text-gray-500 dark:text-slate-400">Reason:</span>
<span class="inline-flex items-center px-1.5 py-0.5 bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400 rounded font-medium text-[11px]"> {{ loginData .reason ? ? 'Unknown' }} </span>
</div>
</div>
{% elseif isLogin %}
<!-- Rich login details -->
<div class="mt-1.5 bg-gray-50 dark:bg-slate-700 rounded-lg p-3 border border-gray-100 dark:border-slate-600">
<div class="grid grid-cols-2 md:grid-cols-3 gap-x-4 gap-y-2 text-xs">
<!-- Location -->
<div class="flex items-center gap-1.5">
<i class="fas fa-map-marker-alt text-red-400 w-3.5 text-center"></i>
<span class="text-gray-500 dark:text-slate-400">Location:</span>
<span class="text-gray-800 dark:text-slate-200 font-medium">
{% if loginData .country_code != 'xx' %}
<span class="fi fi- {{ loginData .country_code | lower }} text-xs mr-0.5 rounded-sm"></span>
{% endif %}
{{ loginData .location ? ? 'Unknown' }}
</span>
</div>
<!-- IP Address -->
<div class="flex items-center gap-1.5">
<i class="fas fa-network-wired text-blue-400 w-3.5 text-center"></i>
<span class="text-gray-500 dark:text-slate-400">IP:</span>
<span class="text-gray-800 dark:text-slate-200 font-medium font-mono text-[11px]"> {{ loginData .ip ? ? 'unknown' }} </span>
</div>
<!-- Browser -->
<div class="flex items-center gap-1.5">
<i class="fas fa-globe text-green-400 w-3.5 text-center"></i>
<span class="text-gray-500 dark:text-slate-400">Browser:</span>
<span class="text-gray-800 dark:text-slate-200 font-medium"> {{ loginData .browser ? ? 'Unknown' }} </span>
</div>
<!-- Device -->
<div class="flex items-center gap-1.5">
<i class="fas fa- {{ loginData .device_icon ? ? 'desktop' }} text-purple-400 w-3.5 text-center"></i>
<span class="text-gray-500 dark:text-slate-400">Device:</span>
<span class="text-gray-800 dark:text-slate-200 font-medium"> {{ loginData .device ? ? 'Unknown' }} </span>
</div>
<!-- OS -->
<div class="flex items-center gap-1.5">
<i class="fas fa-laptop-code text-indigo-400 w-3.5 text-center"></i>
<span class="text-gray-500 dark:text-slate-400">OS:</span>
<span class="text-gray-800 dark:text-slate-200 font-medium"> {{ loginData .os ? ? 'Unknown' }} </span>
</div>
<!-- ISP -->
<div class="flex items-center gap-1.5">
<i class="fas fa-server text-amber-400 w-3.5 text-center"></i>
<span class="text-gray-500 dark:text-slate-400">ISP:</span>
<span class="text-gray-800 dark:text-slate-200 font-medium truncate"> {{ loginData .isp ? ? 'Unknown' }} </span>
</div>
</div>
<!-- Login method -->
<div class="mt-2 pt-2 border-t border-gray-200 dark:border-slate-600 flex items-center gap-1.5 text-xs">
<i class="fas fa-key text-gray-400 dark:text-slate-500 w-3.5 text-center"></i>
<span class="text-gray-500 dark:text-slate-400">Method:</span>
<span class="inline-flex items-center px-1.5 py-0.5 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 rounded font-medium text-[11px]"> {{ loginData .method ? ? 'Login' }} </span>
</div>
</div>
{% else %}
<!-- Standard notification message -->
<p class="text-xs text-gray-600 dark:text-slate-400 mt-0.5"> {{ notification .message }} </p>
{% if hasDomain and clickUrl %}
<a href=" {{ clickUrl }} " class="text-xs text-primary mt-0.5 hover:underline inline-block">
<i class="fas fa-external-link-alt text-[10px] mr-1"></i>View domain
</a>
{% endif %}
{% endif %}
</div>
<!-- Actions -->
<div class="flex items-center gap-1 ml-2 flex-shrink-0">
{% if not notification .is_read %}
<a href="/notifications/ {{ notification .id }} /mark-read" class="w-7 h-7 flex items-center justify-center text-gray-400 dark:text-slate-500 hover:text-green-600 hover:bg-green-50 dark:hover:bg-green-500/10 rounded transition-colors" title="Mark as read">
<i class="fas fa-check text-xs"></i>
</a>
{% endif %}
Enhance DNS discovery, validation & transfers
Add comprehensive DNS management and input validation, plus safer transfer and logging behavior.
- Add CronHelper utilities for cron scripts and unify logging/formatting.
- Improve InputValidator: sanitizeDomainInput and validateRootDomain (handles multi-level TLDs) and use throughout domain import/create flows to reject subdomains.
- DomainController: refactor DNS refresh to support quick/deep discovery (background deep scans), add endpoints to discover, add/delete/bulk-delete DNS records, import BIND zone files, enrich IP metadata via enrichIpDetails, and strengthen bulk import/reporting messages.
- DnsRecord model: add source column handling (discovered/manual/imported), avoid auto-deleting manual/imported records, and add helpers for deleting, bulk deleting, manual adding and importing zone records.
- Tag, NotificationGroup and Domain transfer logic: unlink groups when ownership changes, remove tags that belong to other users, add audit logging via Logger and improved bulk transfer reporting. TagController/View: show transferable users for admins and skip global tags on transfer.
- Notification channels (Discord, Mattermost, etc.) and EmailHelper: allow explicit subjects and improve payload fields based on notification type.
- Add new migration 029_add_dns_record_source.sql and wire it into the installer; update migrations detection.
- Add new views/partials for confirm/import/transfer modals, update various domain/group/tag templates, and update cron scripts and routes for discovery.
These changes preserve manual/imported DNS records, improve root-domain validation, enable background deep discovery, and add better logging/audit trails for transfers and imports.
2026-03-10 22:54:28 +02:00
<form method="POST" action="/notifications/ {{ notification .id }} /delete" class="inline" onsubmit="return confirmSubmit(event, 'Delete this notification?')">
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.
2026-03-03 18:21:32 +02:00
{{ csrf_field ( ) }}
<button type="submit" class="w-7 h-7 flex items-center justify-center text-gray-400 dark:text-slate-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 rounded transition-colors" title="Delete">
<i class="fas fa-times text-xs"></i>
</button>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<!-- Empty State -->
<div class="p-12 text-center">
<i class="fas fa-bell-slash text-gray-300 dark:text-slate-600 text-4xl mb-3"></i>
<p class="text-sm text-gray-600 dark:text-slate-400">No notifications found</p>
<p class="text-xs text-gray-400 dark:text-slate-500 mt-1">Try adjusting your filters</p>
</div>
{% endif %}
</div>
<!-- Pagination Controls -->
{% if totalPages > 1 %}
<div class="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
<!-- Page Info -->
<div class="text-sm text-gray-600 dark:text-slate-400">
Page <span class="font-semibold text-gray-900 dark:text-white"> {{ page }} </span> of
<span class="font-semibold text-gray-900 dark:text-white"> {{ totalPages }} </span>
</div>
<!-- Pagination Buttons -->
<div class="flex items-center gap-1">
<!-- First Page -->
{% if page > 1 %}
<a href=" {{ pagination_url ( 1 , filters , perPage ) }} " class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-gray-700 dark:text-slate-300">
<i class="fas fa-angle-double-left"></i>
</a>
{% endif %}
<!-- Previous Page -->
{% if page > 1 %}
<a href=" {{ pagination_url ( page - 1 , filters , perPage ) }} " class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-gray-700 dark:text-slate-300">
<i class="fas fa-angle-left"></i> Previous
</a>
{% endif %}
<!-- Page Numbers -->
{% set range = 2 %}
{% set startPage = max ( 1 , page - range ) %}
{% set endPage = min ( totalPages , page + range ) %}
{% if startPage > 1 %}
<a href=" {{ pagination_url ( 1 , filters , perPage ) }} " class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-gray-700 dark:text-slate-300">1</a>
{% if startPage > 2 %}
<span class="px-2 text-gray-500 dark:text-slate-400">...</span>
{% endif %}
{% endif %}
{% for i in startPage .. endPage %}
{% if i == page %}
<span class="px-3 py-2 text-sm bg-primary text-white rounded-lg font-semibold"> {{ i }} </span>
{% else %}
<a href=" {{ pagination_url ( i , filters , perPage ) }} " class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-gray-700 dark:text-slate-300"> {{ i }} </a>
{% endif %}
{% endfor %}
{% if endPage < totalPages %}
{% if endPage < totalPages - 1 %}
<span class="px-2 text-gray-500 dark:text-slate-400">...</span>
{% endif %}
<a href=" {{ pagination_url ( totalPages , filters , perPage ) }} " class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-gray-700 dark:text-slate-300"> {{ totalPages }} </a>
{% endif %}
<!-- Next Page -->
{% if page < totalPages %}
<a href=" {{ pagination_url ( page + 1 , filters , perPage ) }} " class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-gray-700 dark:text-slate-300">
Next <i class="fas fa-angle-right"></i>
</a>
{% endif %}
<!-- Last Page -->
{% if page < totalPages %}
<a href=" {{ pagination_url ( totalPages , filters , perPage ) }} " class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-gray-700 dark:text-slate-300">
<i class="fas fa-angle-double-right"></i>
</a>
{% endif %}
</div>
</div>
{% endif %}
<!-- Hidden form for clear all -->
<form id="clearAllForm" method="POST" action="/notifications/clear-all" class="hidden">
{{ csrf_field ( ) }}
</form>
<script>
Enhance DNS discovery, validation & transfers
Add comprehensive DNS management and input validation, plus safer transfer and logging behavior.
- Add CronHelper utilities for cron scripts and unify logging/formatting.
- Improve InputValidator: sanitizeDomainInput and validateRootDomain (handles multi-level TLDs) and use throughout domain import/create flows to reject subdomains.
- DomainController: refactor DNS refresh to support quick/deep discovery (background deep scans), add endpoints to discover, add/delete/bulk-delete DNS records, import BIND zone files, enrich IP metadata via enrichIpDetails, and strengthen bulk import/reporting messages.
- DnsRecord model: add source column handling (discovered/manual/imported), avoid auto-deleting manual/imported records, and add helpers for deleting, bulk deleting, manual adding and importing zone records.
- Tag, NotificationGroup and Domain transfer logic: unlink groups when ownership changes, remove tags that belong to other users, add audit logging via Logger and improved bulk transfer reporting. TagController/View: show transferable users for admins and skip global tags on transfer.
- Notification channels (Discord, Mattermost, etc.) and EmailHelper: allow explicit subjects and improve payload fields based on notification type.
- Add new migration 029_add_dns_record_source.sql and wire it into the installer; update migrations detection.
- Add new views/partials for confirm/import/transfer modals, update various domain/group/tag templates, and update cron scripts and routes for discovery.
These changes preserve manual/imported DNS records, improve root-domain validation, enable background deep discovery, and add better logging/audit trails for transfers and imports.
2026-03-10 22:54:28 +02:00
async function markAllAsRead() {
var ok = await confirmAction( { message: 'Mark all notifications as read?', title: 'Mark All Read', icon: 'fa-check-double text-primary', confirmText: 'Mark Read', confirmClass: 'bg-primary hover:bg-primary-dark' });
if (ok) window.location.href = '/notifications/mark-all-read';
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.
2026-03-03 18:21:32 +02:00
}
Enhance DNS discovery, validation & transfers
Add comprehensive DNS management and input validation, plus safer transfer and logging behavior.
- Add CronHelper utilities for cron scripts and unify logging/formatting.
- Improve InputValidator: sanitizeDomainInput and validateRootDomain (handles multi-level TLDs) and use throughout domain import/create flows to reject subdomains.
- DomainController: refactor DNS refresh to support quick/deep discovery (background deep scans), add endpoints to discover, add/delete/bulk-delete DNS records, import BIND zone files, enrich IP metadata via enrichIpDetails, and strengthen bulk import/reporting messages.
- DnsRecord model: add source column handling (discovered/manual/imported), avoid auto-deleting manual/imported records, and add helpers for deleting, bulk deleting, manual adding and importing zone records.
- Tag, NotificationGroup and Domain transfer logic: unlink groups when ownership changes, remove tags that belong to other users, add audit logging via Logger and improved bulk transfer reporting. TagController/View: show transferable users for admins and skip global tags on transfer.
- Notification channels (Discord, Mattermost, etc.) and EmailHelper: allow explicit subjects and improve payload fields based on notification type.
- Add new migration 029_add_dns_record_source.sql and wire it into the installer; update migrations detection.
- Add new views/partials for confirm/import/transfer modals, update various domain/group/tag templates, and update cron scripts and routes for discovery.
These changes preserve manual/imported DNS records, improve root-domain validation, enable background deep discovery, and add better logging/audit trails for transfers and imports.
2026-03-10 22:54:28 +02:00
async function clearAll() {
var ok = await confirmAction( { message: 'Clear all notifications? This action cannot be undone.' });
if (ok) document.getElementById('clearAllForm').submit();
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.
2026-03-03 18:21:32 +02:00
}
</script>
{% endblock %}