p2ns.admin updates for local DNS
This commit is contained in:
@@ -165,94 +165,206 @@
|
||||
</div>
|
||||
|
||||
<div id="local-dns" class="tab-content hidden flex flex-col admin-tab-panel--fixed">
|
||||
<h2 class="text-2xl font-bold mb-4 flex items-center gap-2 flex-shrink-0">
|
||||
Local DNS
|
||||
</h2>
|
||||
<div class="admin-subtabs flex gap-2 mb-4 flex-shrink-0">
|
||||
<button onclick="showSubTab('local-dns', 'records')" id="local-dns-subtab-records" class="admin-subtab-btn admin-subtab-btn--active px-4 py-2 rounded transition-colors">DNS Records</button>
|
||||
<button onclick="showSubTab('local-dns', 'conflicts')" id="local-dns-subtab-conflicts" class="admin-subtab-btn px-4 py-2 rounded transition-colors">DNS Conflicts</button>
|
||||
<button onclick="showSubTab('local-dns', 'p2p-conflicts')" id="local-dns-subtab-p2p-conflicts" class="admin-subtab-btn px-4 py-2 rounded transition-colors">P2P Conflicts</button>
|
||||
</div>
|
||||
|
||||
<!-- DNS Records Sub-tab -->
|
||||
<div id="local-dns-records" class="sub-tab-content flex flex-col flex-1 min-h-0">
|
||||
<div class="mb-4 flex items-center gap-2 flex-shrink-0">
|
||||
<h3 class="text-xl font-bold">Custom Local DNS Records</h3>
|
||||
<button onclick="openInfoModal('local-dns')" class="text-sm px-3 py-1 theme-button-info rounded transition-colors">Info</button>
|
||||
<header class="admin-tab-header flex-shrink-0">
|
||||
<div class="admin-tab-header-main">
|
||||
<h2 class="admin-tab-title">Local DNS</h2>
|
||||
<p class="admin-tab-description">Override resolution with custom records, choose public vs P2P when both exist, and set hash preferences for conflicting domains.</p>
|
||||
</div>
|
||||
<div class="mb-6 flex-shrink-0">
|
||||
<input id="search-local-dns" type="text" placeholder="Search records..." class="w-full p-3 rounded-lg theme-input focus:outline-none focus:ring-2 focus:ring-primary" oninput="filterLocalDNS()">
|
||||
</div>
|
||||
<div class="overflow-x-auto overflow-y-auto flex-1 min-h-0">
|
||||
<table class="w-full theme-table rounded-lg">
|
||||
<thead class="theme-table thead sticky top-0 z-10">
|
||||
<tr>
|
||||
<th class="p-3 text-left">Name</th>
|
||||
<th class="p-3 text-left">Type</th>
|
||||
<th class="p-3 text-left">Value</th>
|
||||
<th class="p-3 text-left">TTL</th>
|
||||
<th class="p-3 text-left">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="localDnsTable"></tbody>
|
||||
</table>
|
||||
<div id="localDnsScrollSentinel" class="scroll-sentinel"></div>
|
||||
</div>
|
||||
<div id="localDnsPagination" class="flex justify-center mt-4 space-x-2 flex-shrink-0"></div>
|
||||
<div class="mt-4 flex justify-end flex-shrink-0">
|
||||
<button onclick="openLocalDnsModal()" class="px-6 py-2.5 bg-primary text-white rounded-lg hover:bg-primary-hover transition-all font-medium shadow-md hover:shadow-lg" style="min-width: auto; width: auto;">Add Record</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DNS Conflicts Sub-tab -->
|
||||
<div id="local-dns-conflicts" class="sub-tab-content hidden flex flex-col flex-1 min-h-0">
|
||||
<div class="mb-4 flex items-center gap-2 flex-shrink-0">
|
||||
<h3 class="text-xl font-bold">DNS Conflict Selector</h3>
|
||||
<button onclick="openInfoModal('dns-conflicts')" class="text-sm px-3 py-1 theme-button-info rounded transition-colors">Info</button>
|
||||
</div>
|
||||
<div class="mb-6 flex-shrink-0">
|
||||
<input id="search-dns-conflicts" type="text" placeholder="Search conflicts..." class="w-full p-3 rounded-lg theme-input focus:outline-none focus:ring-2 focus:ring-primary" oninput="filterDnsConflicts()">
|
||||
</div>
|
||||
<div class="overflow-x-auto overflow-y-auto flex-1 min-h-0">
|
||||
<table class="w-full theme-table rounded-lg">
|
||||
<thead class="theme-table thead sticky top-0 z-10">
|
||||
<tr>
|
||||
<th class="p-3 text-left">Domain</th>
|
||||
<th class="p-3 text-left">Public IP</th>
|
||||
<th class="p-3 text-left">Mode</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dnsConflictsTable"></tbody>
|
||||
</table>
|
||||
<div id="dnsConflictsScrollSentinel" class="scroll-sentinel"></div>
|
||||
</div>
|
||||
<div id="dnsConflictsPagination" class="flex justify-center mt-4 space-x-2 flex-shrink-0"></div>
|
||||
<button type="button" onclick="openInfoModal('local-dns')" class="theme-button-info text-sm px-3 py-1 rounded">Info</button>
|
||||
</header>
|
||||
|
||||
<div class="admin-subtabs flex flex-shrink-0 mb-4">
|
||||
<button type="button" onclick="showSubTab('local-dns', 'records')" id="local-dns-subtab-records" class="admin-subtab-btn admin-subtab-btn--active">
|
||||
<i class="fas fa-list-ul admin-subtab-icon" aria-hidden="true"></i>
|
||||
DNS Records
|
||||
<span id="localDnsSubtabBadge" class="admin-subtab-badge" hidden>0</span>
|
||||
</button>
|
||||
<button type="button" onclick="showSubTab('local-dns', 'conflicts')" id="local-dns-subtab-conflicts" class="admin-subtab-btn">
|
||||
<i class="fas fa-arrows-left-right admin-subtab-icon" aria-hidden="true"></i>
|
||||
DNS Conflicts
|
||||
<span id="dnsConflictsSubtabBadge" class="admin-subtab-badge" hidden>0</span>
|
||||
</button>
|
||||
<button type="button" onclick="showSubTab('local-dns', 'p2p-conflicts')" id="local-dns-subtab-p2p-conflicts" class="admin-subtab-btn">
|
||||
<i class="fas fa-code-branch admin-subtab-icon" aria-hidden="true"></i>
|
||||
P2P Conflicts
|
||||
<span id="p2pConflictsSubtabBadge" class="admin-subtab-badge" hidden>0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- P2P Domain Conflicts Sub-tab -->
|
||||
<!-- DNS Records -->
|
||||
<div id="local-dns-records" class="sub-tab-content flex flex-col flex-1 min-h-0">
|
||||
<div class="local-dns-layout flex-1 min-h-0 grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<section class="theme-card admin-panel local-dns-panel lg:col-span-2 flex flex-col min-h-0">
|
||||
<div class="admin-panel-header">
|
||||
<h3 class="admin-panel-title">Custom records</h3>
|
||||
<p class="admin-panel-subtitle" id="localDnsCount">Loading…</p>
|
||||
</div>
|
||||
<div class="admin-toolbar flex-shrink-0">
|
||||
<label class="admin-search-field">
|
||||
<i class="fas fa-magnifying-glass admin-search-icon" aria-hidden="true"></i>
|
||||
<input id="search-local-dns" type="search" placeholder="Search by name, type, or value…" class="theme-input admin-search-input" oninput="filterLocalDNS()" autocomplete="off">
|
||||
</label>
|
||||
</div>
|
||||
<div class="local-dns-table-scroll flex-1 min-h-0 overflow-auto">
|
||||
<table class="w-full theme-table local-dns-table">
|
||||
<thead class="theme-table thead sticky top-0 z-10">
|
||||
<tr>
|
||||
<th class="p-3 text-left">Name</th>
|
||||
<th class="p-3 text-left w-[5.5rem]">Type</th>
|
||||
<th class="p-3 text-left">Value</th>
|
||||
<th class="p-3 text-left w-[4.5rem]">TTL</th>
|
||||
<th class="p-3 text-right w-[6.5rem]"><span class="sr-only">Actions</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="localDnsTable"></tbody>
|
||||
</table>
|
||||
<div id="localDnsEmpty" class="local-dns-empty">
|
||||
<i class="fas fa-sitemap local-dns-empty-icon" aria-hidden="true"></i>
|
||||
<p class="local-dns-empty-title">No local DNS records</p>
|
||||
<p class="local-dns-empty-text theme-text-tertiary">Add overrides that take precedence over P2NS resolution on this node.</p>
|
||||
</div>
|
||||
<div id="localDnsFilteredEmpty" class="local-dns-empty">
|
||||
<i class="fas fa-filter local-dns-empty-icon" aria-hidden="true"></i>
|
||||
<p class="local-dns-empty-title">No matching records</p>
|
||||
<p class="local-dns-empty-text theme-text-tertiary">Try a different search term.</p>
|
||||
</div>
|
||||
<div id="localDnsEnd" class="local-dns-list-end" aria-live="polite">You have reached the end</div>
|
||||
<div id="localDnsScrollSentinel" class="scroll-sentinel"></div>
|
||||
</div>
|
||||
<div id="localDnsPagination" class="hidden"></div>
|
||||
<div class="admin-panel-footer flex justify-end flex-shrink-0">
|
||||
<button type="button" onclick="openLocalDnsModal()" class="admin-btn admin-btn--primary">
|
||||
<i class="fas fa-plus" aria-hidden="true"></i> Add record
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="local-dns-sidebar flex flex-col gap-4 min-h-0">
|
||||
<section class="theme-card admin-panel local-dns-summary-panel">
|
||||
<div class="admin-panel-header">
|
||||
<h3 class="admin-panel-title">Overview</h3>
|
||||
<p class="admin-panel-subtitle" id="localDnsOverviewLine">—</p>
|
||||
</div>
|
||||
<dl class="local-dns-stat-grid">
|
||||
<div class="local-dns-stat">
|
||||
<dt>Total</dt>
|
||||
<dd id="localDnsStatTotal">—</dd>
|
||||
</div>
|
||||
<div class="local-dns-stat">
|
||||
<dt>A / AAAA</dt>
|
||||
<dd id="localDnsStatAddress">—</dd>
|
||||
</div>
|
||||
<div class="local-dns-stat">
|
||||
<dt>CNAME</dt>
|
||||
<dd id="localDnsStatCname">—</dd>
|
||||
</div>
|
||||
<div class="local-dns-stat">
|
||||
<dt>Other</dt>
|
||||
<dd id="localDnsStatOther">—</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
<section class="theme-card admin-panel local-dns-note-panel">
|
||||
<div class="admin-panel-header">
|
||||
<h3 class="admin-panel-title">About local DNS</h3>
|
||||
</div>
|
||||
<p class="local-dns-note-text theme-text-tertiary">Records in <code class="local-dns-inline-code">cache/local_dns.json</code> override P2NS and public resolution for matching names on this node.</p>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DNS Conflicts -->
|
||||
<div id="local-dns-conflicts" class="sub-tab-content hidden flex flex-col flex-1 min-h-0">
|
||||
<section class="theme-card admin-panel local-dns-conflicts-panel flex flex-col flex-1 min-h-0">
|
||||
<div class="admin-panel-header">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<h3 class="admin-panel-title">DNS conflict selector</h3>
|
||||
<p class="admin-panel-subtitle" id="dnsConflictsCount">Loading…</p>
|
||||
</div>
|
||||
<button type="button" onclick="openInfoModal('dns-conflicts')" class="theme-button-info text-sm px-3 py-1 rounded flex-shrink-0">Info</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-toolbar flex-shrink-0">
|
||||
<label class="admin-search-field">
|
||||
<i class="fas fa-magnifying-glass admin-search-icon" aria-hidden="true"></i>
|
||||
<input id="search-dns-conflicts" type="search" placeholder="Search by domain or IP…" class="theme-input admin-search-input" oninput="filterDnsConflicts()" autocomplete="off">
|
||||
</label>
|
||||
</div>
|
||||
<div class="local-dns-table-scroll flex-1 min-h-0 overflow-auto">
|
||||
<table class="w-full theme-table local-dns-conflicts-table">
|
||||
<thead class="theme-table thead sticky top-0 z-10">
|
||||
<tr>
|
||||
<th class="p-3 text-left">Domain</th>
|
||||
<th class="p-3 text-left">Public IP</th>
|
||||
<th class="p-3 text-left w-[11rem]">Preferred source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dnsConflictsTable"></tbody>
|
||||
</table>
|
||||
<div id="dnsConflictsEmpty" class="local-dns-empty">
|
||||
<i class="fas fa-circle-check local-dns-empty-icon" aria-hidden="true"></i>
|
||||
<p class="local-dns-empty-title">No DNS conflicts</p>
|
||||
<p class="local-dns-empty-text theme-text-tertiary">Domains with both public and P2P resolution will appear here.</p>
|
||||
</div>
|
||||
<div id="dnsConflictsFilteredEmpty" class="local-dns-empty">
|
||||
<i class="fas fa-filter local-dns-empty-icon" aria-hidden="true"></i>
|
||||
<p class="local-dns-empty-title">No matching conflicts</p>
|
||||
<p class="local-dns-empty-text theme-text-tertiary">Try a different search term.</p>
|
||||
</div>
|
||||
<div id="dnsConflictsEnd" class="local-dns-list-end" aria-live="polite">You have reached the end</div>
|
||||
<div id="dnsConflictsScrollSentinel" class="scroll-sentinel"></div>
|
||||
</div>
|
||||
<div id="dnsConflictsPagination" class="hidden"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- P2P Domain Conflicts -->
|
||||
<div id="local-dns-p2p-conflicts" class="sub-tab-content hidden flex flex-col flex-1 min-h-0">
|
||||
<div class="mb-4 flex items-center gap-2 flex-shrink-0">
|
||||
<h3 class="text-xl font-bold">P2P Domain Conflicts</h3>
|
||||
<button onclick="openInfoModal('p2p-domain-conflicts')" class="text-sm px-3 py-1 theme-button-info rounded transition-colors">Info</button>
|
||||
</div>
|
||||
<div class="mb-6 flex-shrink-0">
|
||||
<input id="search-p2p-domain-conflicts" type="text" placeholder="Search conflicts..." class="w-full p-3 rounded-lg theme-input focus:outline-none focus:ring-2 focus:ring-primary" oninput="filterP2pDomainConflicts()">
|
||||
</div>
|
||||
<div class="overflow-x-auto overflow-y-auto flex-1 min-h-0">
|
||||
<table class="w-full theme-table rounded-lg">
|
||||
<thead class="theme-table thead sticky top-0 z-10">
|
||||
<tr>
|
||||
<th class="p-3 text-left">Domain</th>
|
||||
<th class="p-3 text-left">Local Hash</th>
|
||||
<th class="p-3 text-left">Resolved Hash</th>
|
||||
<th class="p-3 text-left">Hash Preference</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="p2pDomainConflictsTable"></tbody>
|
||||
</table>
|
||||
<div id="p2pDomainConflictsScrollSentinel" class="scroll-sentinel"></div>
|
||||
</div>
|
||||
<div id="p2pDomainConflictsPagination" class="flex justify-center mt-4 space-x-2 flex-shrink-0"></div>
|
||||
<section class="theme-card admin-panel local-dns-p2p-panel flex flex-col flex-1 min-h-0">
|
||||
<div class="admin-panel-header">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<h3 class="admin-panel-title">P2P domain conflicts</h3>
|
||||
<p class="admin-panel-subtitle" id="p2pConflictsCount">Loading…</p>
|
||||
</div>
|
||||
<button type="button" onclick="openInfoModal('p2p-domain-conflicts')" class="theme-button-info text-sm px-3 py-1 rounded flex-shrink-0">Info</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-toolbar flex-shrink-0">
|
||||
<label class="admin-search-field">
|
||||
<i class="fas fa-magnifying-glass admin-search-icon" aria-hidden="true"></i>
|
||||
<input id="search-p2p-domain-conflicts" type="search" placeholder="Search by domain or hash…" class="theme-input admin-search-input" oninput="filterP2pDomainConflicts()" autocomplete="off">
|
||||
</label>
|
||||
</div>
|
||||
<div class="local-dns-table-scroll flex-1 min-h-0 overflow-auto">
|
||||
<table class="w-full theme-table local-dns-p2p-table">
|
||||
<thead class="theme-table thead sticky top-0 z-10">
|
||||
<tr>
|
||||
<th class="p-3 text-left">Domain</th>
|
||||
<th class="p-3 text-left">Local hash</th>
|
||||
<th class="p-3 text-left">Resolved hash</th>
|
||||
<th class="p-3 text-left w-[11rem]">Hash preference</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="p2pDomainConflictsTable"></tbody>
|
||||
</table>
|
||||
<div id="p2pConflictsEmpty" class="local-dns-empty">
|
||||
<i class="fas fa-circle-check local-dns-empty-icon" aria-hidden="true"></i>
|
||||
<p class="local-dns-empty-title">No P2P conflicts</p>
|
||||
<p class="local-dns-empty-text theme-text-tertiary">Domains where your local claim differs from network consensus appear here.</p>
|
||||
</div>
|
||||
<div id="p2pConflictsFilteredEmpty" class="local-dns-empty">
|
||||
<i class="fas fa-filter local-dns-empty-icon" aria-hidden="true"></i>
|
||||
<p class="local-dns-empty-title">No matching conflicts</p>
|
||||
<p class="local-dns-empty-text theme-text-tertiary">Try a different search term.</p>
|
||||
</div>
|
||||
<div id="p2pConflictsEnd" class="local-dns-list-end" aria-live="polite">You have reached the end</div>
|
||||
<div id="p2pDomainConflictsScrollSentinel" class="scroll-sentinel"></div>
|
||||
</div>
|
||||
<div id="p2pDomainConflictsPagination" class="hidden"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1145,27 +1257,48 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="localDnsModal" class="p-6 theme-glass rounded-lg shadow-xl w-full max-w-md">
|
||||
<h3 id="local-dns-title" class="text-xl font-bold mb-4">Add Local DNS Record</h3>
|
||||
<input id="local-name" placeholder="Name (domain)" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
|
||||
<select id="local-type" onchange="updateLocalForm()" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
|
||||
<option value="A">A</option>
|
||||
<option value="AAAA">AAAA</option>
|
||||
<option value="CNAME">CNAME</option>
|
||||
<option value="MX">MX</option>
|
||||
<option value="TXT">TXT</option>
|
||||
<option value="SRV">SRV</option>
|
||||
<option value="SOA">SOA</option>
|
||||
<option value="CAA">CAA</option>
|
||||
<option value="NS">NS</option>
|
||||
<option value="PTR">PTR</option>
|
||||
<option value="OTHER">OTHER</option>
|
||||
</select>
|
||||
<div id="local-value-fields" class="mb-4"></div>
|
||||
<input id="local-ttl" type="number" placeholder="TTL" value="3600" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button onclick="document.getElementById('localDnsModal').close()" class="px-4 py-2 theme-button-info theme-text-primary rounded theme-glass-hover">Cancel</button>
|
||||
<button id="local-submit" onclick="submitLocalDns(this)" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Add</button>
|
||||
<dialog id="localDnsModal" class="local-dns-modal theme-glass rounded-lg shadow-xl w-full max-w-md p-0 border-0">
|
||||
<div class="local-dns-modal-inner">
|
||||
<div class="local-dns-modal-header">
|
||||
<div>
|
||||
<h3 id="local-dns-title" class="local-dns-modal-title">Add Local DNS Record</h3>
|
||||
<p class="local-dns-modal-subtitle theme-text-tertiary">Overrides apply on this node only.</p>
|
||||
</div>
|
||||
<button type="button" onclick="document.getElementById('localDnsModal').close()" class="admin-btn admin-btn--secondary admin-btn--sm" aria-label="Close">
|
||||
<i class="fas fa-xmark" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="local-dns-modal-body">
|
||||
<label class="local-dns-form-field">
|
||||
<span class="local-dns-form-label">Name</span>
|
||||
<input id="local-name" placeholder="example.tld or host.example.tld" class="theme-input w-full">
|
||||
</label>
|
||||
<label class="local-dns-form-field">
|
||||
<span class="local-dns-form-label">Type</span>
|
||||
<select id="local-type" onchange="updateLocalForm()" class="theme-input w-full">
|
||||
<option value="A">A</option>
|
||||
<option value="AAAA">AAAA</option>
|
||||
<option value="CNAME">CNAME</option>
|
||||
<option value="MX">MX</option>
|
||||
<option value="TXT">TXT</option>
|
||||
<option value="SRV">SRV</option>
|
||||
<option value="SOA">SOA</option>
|
||||
<option value="CAA">CAA</option>
|
||||
<option value="NS">NS</option>
|
||||
<option value="PTR">PTR</option>
|
||||
<option value="OTHER">OTHER</option>
|
||||
</select>
|
||||
</label>
|
||||
<div id="local-value-fields" class="local-dns-dynamic-fields"></div>
|
||||
<label class="local-dns-form-field">
|
||||
<span class="local-dns-form-label">TTL (seconds)</span>
|
||||
<input id="local-ttl" type="number" placeholder="3600" value="3600" class="theme-input w-full" min="0">
|
||||
</label>
|
||||
</div>
|
||||
<div class="local-dns-modal-footer">
|
||||
<button type="button" onclick="document.getElementById('localDnsModal').close()" class="admin-btn admin-btn--secondary">Cancel</button>
|
||||
<button type="button" id="local-submit" onclick="submitLocalDns(this)" class="admin-btn admin-btn--primary">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
|
||||
@@ -3663,6 +3663,281 @@ pre.theme-glass, code.theme-glass {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Local DNS tab */
|
||||
.admin-subtab-icon {
|
||||
margin-right: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.admin-subtab-badge {
|
||||
margin-left: 0.375rem;
|
||||
padding: 0.0625rem 0.4375rem;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
background: var(--admin-brand-muted);
|
||||
border: 1px solid var(--admin-brand-border);
|
||||
color: var(--admin-brand-light);
|
||||
}
|
||||
|
||||
.admin-subtab-badge[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.local-dns-layout {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.local-dns-panel,
|
||||
.local-dns-conflicts-panel,
|
||||
.local-dns-p2p-panel {
|
||||
min-height: 12rem;
|
||||
}
|
||||
|
||||
.local-dns-table-scroll {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.local-dns-table--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.local-dns-empty {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 2.5rem 1rem;
|
||||
border: 1px dashed var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--admin-bg-glass);
|
||||
}
|
||||
|
||||
.local-dns-empty--visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.local-dns-empty-icon {
|
||||
font-size: 1.5rem;
|
||||
color: var(--admin-brand-light);
|
||||
margin-bottom: 0.75rem;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.local-dns-empty-title {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.local-dns-empty-text {
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.8125rem;
|
||||
max-width: 22rem;
|
||||
}
|
||||
|
||||
.local-dns-list-end {
|
||||
display: none;
|
||||
padding: 0.875rem 0.5rem 0.25rem;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.local-dns-list-end--visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.local-dns-stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.local-dns-stat dt {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.local-dns-stat dd {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.local-dns-note-text {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.local-dns-inline-code {
|
||||
font-family: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--admin-bg-muted);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--admin-brand-light);
|
||||
}
|
||||
|
||||
.local-dns-row:hover {
|
||||
background: var(--admin-row-hover);
|
||||
}
|
||||
|
||||
.local-dns-name,
|
||||
.local-dns-hash {
|
||||
font-family: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.local-dns-value {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.local-dns-row-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.dns-type-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.dns-type-badge--address {
|
||||
color: rgb(134, 239, 172);
|
||||
background: rgba(34, 197, 94, 0.12);
|
||||
border-color: rgba(34, 197, 94, 0.28);
|
||||
}
|
||||
|
||||
.dns-type-badge--cname {
|
||||
color: rgb(147, 197, 253);
|
||||
background: rgba(59, 130, 246, 0.12);
|
||||
border-color: rgba(59, 130, 246, 0.28);
|
||||
}
|
||||
|
||||
.dns-type-badge--mail {
|
||||
color: rgb(253, 186, 116);
|
||||
background: rgba(249, 115, 22, 0.12);
|
||||
border-color: rgba(249, 115, 22, 0.28);
|
||||
}
|
||||
|
||||
.dns-type-badge--service {
|
||||
color: rgb(196, 181, 253);
|
||||
background: rgba(139, 92, 246, 0.12);
|
||||
border-color: rgba(139, 92, 246, 0.28);
|
||||
}
|
||||
|
||||
.dns-type-badge--muted {
|
||||
color: var(--text-secondary);
|
||||
background: var(--admin-bg-muted);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.local-dns-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.local-dns-toggle-label {
|
||||
min-width: 4.25rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.local-dns-modal {
|
||||
max-width: 28rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.local-dns-modal-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: min(90vh, 40rem);
|
||||
}
|
||||
|
||||
.local-dns-modal-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 1rem 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.local-dns-modal-title {
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.local-dns-modal-subtitle {
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.local-dns-modal-body {
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.local-dns-modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem 1rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.local-dns-form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.local-dns-form-label {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.local-dns-dynamic-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.entries-sidebar {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -432,6 +432,7 @@ window.tabs = {
|
||||
filteredKey: 'filteredLocalDns',
|
||||
containerId: 'localDnsTable',
|
||||
paginationId: 'localDnsPagination',
|
||||
sentinelId: 'localDnsScrollSentinel',
|
||||
sort: (a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }),
|
||||
filter: (item, query) => {
|
||||
const queryLower = query.toLowerCase();
|
||||
@@ -442,36 +443,40 @@ window.tabs = {
|
||||
);
|
||||
},
|
||||
renderItem: (item) => {
|
||||
let valueStr = '';
|
||||
if (item.type === 'MX') {
|
||||
valueStr = `${item.preference || ''} ${item.exchange || ''}`.trim();
|
||||
} else if (item.type === 'SRV') {
|
||||
valueStr = `${item.priority || ''} ${item.weight || ''} ${item.port || ''} ${item.target || ''}`.trim();
|
||||
} else if (item.type === 'SOA') {
|
||||
valueStr = `${item.mname || ''} ${item.rname || ''} ${item.serial || ''} ${item.refresh || ''} ${item.retry || ''} ${item.expire || ''} ${item.minimum || ''}`.trim();
|
||||
} else if (item.type === 'CAA') {
|
||||
valueStr = `${item.flags || ''} ${item.tag || ''} ${item.value || ''}`.trim();
|
||||
} else {
|
||||
valueStr = item.data || item.value || '';
|
||||
}
|
||||
const esc = window.escapeHtml || ((value) => String(value));
|
||||
const typeInfo = window.classifyDnsRecordType ? window.classifyDnsRecordType(item.type) : { label: item.type, className: 'dns-type-badge--muted' };
|
||||
const valueStr = window.formatLocalDnsValue ? window.formatLocalDnsValue(item) : (item.data || item.value || '');
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'border-b hover:bg-gray-50 dark:hover:bg-gray-700';
|
||||
tr.className = 'local-dns-row';
|
||||
tr.innerHTML = `
|
||||
<td class="p-3">${item.name}</td>
|
||||
<td class="p-3">${item.type}</td>
|
||||
<td class="p-3 break-all">${valueStr}</td>
|
||||
<td class="p-3">${item.ttl}</td>
|
||||
<td class="p-3">
|
||||
<button onclick="editLocalDns(${item.index})" class="px-2 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 mr-2">Edit</button>
|
||||
<button onclick="deleteLocalDns(${item.index})" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Delete</button>
|
||||
<td class="p-3"><code class="local-dns-name">${esc(item.name)}</code></td>
|
||||
<td class="p-3"><span class="dns-type-badge ${typeInfo.className}">${esc(typeInfo.label)}</span></td>
|
||||
<td class="p-3"><span class="local-dns-value">${esc(valueStr)}</span></td>
|
||||
<td class="p-3 tabular-nums">${esc(String(item.ttl ?? ''))}</td>
|
||||
<td class="p-3 text-right">
|
||||
<div class="local-dns-row-actions">
|
||||
<button type="button" class="admin-btn admin-btn--secondary admin-btn--sm" onclick="editLocalDns(${item.index})" title="Edit record">
|
||||
<i class="fas fa-pen" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="admin-btn admin-btn--danger admin-btn--sm" onclick="deleteLocalDns(${item.index})" title="Delete record">
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>`;
|
||||
return tr;
|
||||
},
|
||||
preRender: (visibleCount) => {
|
||||
if (window.updateLocalDnsChrome) window.updateLocalDnsChrome(visibleCount);
|
||||
},
|
||||
onAllItemsLoaded: () => {
|
||||
if (window.showLocalDnsListEnd) window.showLocalDnsListEnd();
|
||||
},
|
||||
postFetch: (data) => {
|
||||
window.dnsConflictsData = data.conflicts || [];
|
||||
window.filteredDnsConflicts = window.dnsConflictsData;
|
||||
data.records = data.records.map((rec, index) => ({ ...rec, index }));
|
||||
if (window.renderDnsConflicts) window.renderDnsConflicts();
|
||||
if (window.updateLocalDnsSubtabBadges) window.updateLocalDnsSubtabBadges();
|
||||
return data.records;
|
||||
}
|
||||
},
|
||||
@@ -482,28 +487,37 @@ window.tabs = {
|
||||
filteredKey: 'filteredDnsConflicts',
|
||||
containerId: 'dnsConflictsTable',
|
||||
paginationId: 'dnsConflictsPagination',
|
||||
sentinelId: 'dnsConflictsScrollSentinel',
|
||||
sort: (a, b) => a.domain.localeCompare(b.domain, undefined, { sensitivity: 'base' }),
|
||||
filter: (item, query) => item.domain.toLowerCase().includes(query) || item.version.toLowerCase().includes(query) || item.publicIP.toLowerCase().includes(query),
|
||||
renderItem: (item) => {
|
||||
const esc = window.escapeHtml || ((value) => String(value));
|
||||
const domainAttr = esc(item.domain).replace(/"/g, '"');
|
||||
const isPublic = item.version === 'public';
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'border-b hover:bg-gray-50 dark:hover:bg-gray-700';
|
||||
tr.className = 'local-dns-row';
|
||||
tr.innerHTML = `
|
||||
<td class="p-3">${item.domain}</td>
|
||||
<td class="p-3">${item.publicIP}</td>
|
||||
<td class="p-3"><code class="local-dns-name">${esc(item.domain)}</code></td>
|
||||
<td class="p-3"><span class="local-dns-value">${esc(item.publicIP || '—')}</span></td>
|
||||
<td class="p-3">
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<span class="mr-2">${item.version === 'public' ? 'Public' : 'P2P'}</span>
|
||||
<input type="checkbox" ${item.version === 'public' ? 'checked' : ''} onchange="toggleVersionPreference('${item.domain}', this.checked)" class="sr-only peer">
|
||||
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary rounded-full peer peer-checked:bg-primary">
|
||||
<div class="absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5"></div>
|
||||
</div>
|
||||
<label class="local-dns-toggle">
|
||||
<span class="local-dns-toggle-label">${isPublic ? 'Public' : 'P2P'}</span>
|
||||
<input type="checkbox" class="sr-only" ${isPublic ? 'checked' : ''} data-domain="${domainAttr}" onchange="toggleVersionPreference(this.dataset.domain, this.checked)">
|
||||
<span class="plugin-toggle-switch" aria-hidden="true"></span>
|
||||
</label>
|
||||
</td>`;
|
||||
return tr;
|
||||
},
|
||||
preRender: (visibleCount) => {
|
||||
if (window.updateDnsConflictsChrome) window.updateDnsConflictsChrome(visibleCount);
|
||||
},
|
||||
onAllItemsLoaded: () => {
|
||||
if (window.showDnsConflictsListEnd) window.showDnsConflictsListEnd();
|
||||
},
|
||||
postFetch: (data) => {
|
||||
window.localDnsData = data.records.map((rec, index) => ({ ...rec, index }));
|
||||
window.filteredLocalDns = window.localDnsData;
|
||||
if (window.updateLocalDnsSubtabBadges) window.updateLocalDnsSubtabBadges();
|
||||
return data.conflicts || [];
|
||||
}
|
||||
},
|
||||
@@ -514,27 +528,36 @@ window.tabs = {
|
||||
filteredKey: 'filteredP2pDomainConflicts',
|
||||
containerId: 'p2pDomainConflictsTable',
|
||||
paginationId: 'p2pDomainConflictsPagination',
|
||||
sentinelId: 'p2pDomainConflictsScrollSentinel',
|
||||
sort: (a, b) => a.domain.localeCompare(b.domain, undefined, { sensitivity: 'base' }),
|
||||
filter: (item, query) => item.domain.toLowerCase().includes(query) || item.localHash.toLowerCase().includes(query) || item.resolvedHash.toLowerCase().includes(query),
|
||||
renderItem: (item) => {
|
||||
const esc = window.escapeHtml || ((value) => String(value));
|
||||
const domainAttr = esc(item.domain).replace(/"/g, '"');
|
||||
const useLocal = item.hashPreference === 'local';
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'border-b hover:bg-gray-50 dark:hover:bg-gray-700';
|
||||
tr.className = 'local-dns-row';
|
||||
tr.innerHTML = `
|
||||
<td class="p-3">${item.domain}</td>
|
||||
<td class="p-3 break-all">${item.localHash || 'N/A'}</td>
|
||||
<td class="p-3 break-all">${item.resolvedHash || 'N/A'}</td>
|
||||
<td class="p-3"><code class="local-dns-name">${esc(item.domain)}</code></td>
|
||||
<td class="p-3"><code class="local-dns-hash">${esc(item.localHash || 'N/A')}</code></td>
|
||||
<td class="p-3"><code class="local-dns-hash">${esc(item.resolvedHash || 'N/A')}</code></td>
|
||||
<td class="p-3">
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<span class="mr-2">${item.hashPreference === 'local' ? 'Local' : 'Resolved'}</span>
|
||||
<input type="checkbox" ${item.hashPreference === 'local' ? 'checked' : ''} onchange="toggleHashPreference('${item.domain}', this.checked)" class="sr-only peer">
|
||||
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary rounded-full peer peer-checked:bg-primary">
|
||||
<div class="absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5"></div>
|
||||
</div>
|
||||
<label class="local-dns-toggle">
|
||||
<span class="local-dns-toggle-label">${useLocal ? 'Local' : 'Resolved'}</span>
|
||||
<input type="checkbox" class="sr-only" ${useLocal ? 'checked' : ''} data-domain="${domainAttr}" onchange="toggleHashPreference(this.dataset.domain, this.checked)">
|
||||
<span class="plugin-toggle-switch" aria-hidden="true"></span>
|
||||
</label>
|
||||
</td>`;
|
||||
return tr;
|
||||
},
|
||||
preRender: (visibleCount) => {
|
||||
if (window.updateP2pConflictsChrome) window.updateP2pConflictsChrome(visibleCount);
|
||||
},
|
||||
onAllItemsLoaded: () => {
|
||||
if (window.showP2pConflictsListEnd) window.showP2pConflictsListEnd();
|
||||
},
|
||||
postFetch: (data) => {
|
||||
if (window.updateLocalDnsSubtabBadges) window.updateLocalDnsSubtabBadges();
|
||||
return data.conflicts || [];
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,4 +1,196 @@
|
||||
// Local DNS UI functions
|
||||
|
||||
const LOCAL_DNS_FIELD = 'theme-input w-full';
|
||||
|
||||
function classifyDnsRecordType(type) {
|
||||
const t = String(type || '').toUpperCase();
|
||||
if (t === 'A' || t === 'AAAA') return { label: t, className: 'dns-type-badge--address' };
|
||||
if (t === 'CNAME') return { label: t, className: 'dns-type-badge--cname' };
|
||||
if (t === 'MX' || t === 'TXT') return { label: t, className: 'dns-type-badge--mail' };
|
||||
if (t === 'SRV' || t === 'NS' || t === 'PTR') return { label: t, className: 'dns-type-badge--service' };
|
||||
return { label: t || 'OTHER', className: 'dns-type-badge--muted' };
|
||||
}
|
||||
|
||||
function formatLocalDnsValue(item) {
|
||||
if (!item) return '';
|
||||
if (item.type === 'MX') {
|
||||
return `${item.preference || ''} ${item.exchange || ''}`.trim();
|
||||
}
|
||||
if (item.type === 'SRV') {
|
||||
return `${item.priority || ''} ${item.weight || ''} ${item.port || ''} ${item.target || ''}`.trim();
|
||||
}
|
||||
if (item.type === 'SOA') {
|
||||
return `${item.mname || ''} ${item.rname || ''} ${item.serial || ''} ${item.refresh || ''} ${item.retry || ''} ${item.expire || ''} ${item.minimum || ''}`.trim();
|
||||
}
|
||||
if (item.type === 'CAA') {
|
||||
return `${item.flags || ''} ${item.tag || ''} ${item.value || ''}`.trim();
|
||||
}
|
||||
return item.data || item.value || '';
|
||||
}
|
||||
|
||||
function computeLocalDnsStats(data) {
|
||||
const records = Array.isArray(data) ? data : [];
|
||||
let address = 0;
|
||||
let cname = 0;
|
||||
let other = 0;
|
||||
|
||||
for (const rec of records) {
|
||||
const type = String(rec?.type || '').toUpperCase();
|
||||
if (type === 'A' || type === 'AAAA') address += 1;
|
||||
else if (type === 'CNAME') cname += 1;
|
||||
else other += 1;
|
||||
}
|
||||
|
||||
return { total: records.length, address, cname, other };
|
||||
}
|
||||
|
||||
function setSubtabBadge(id, count) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
if (count > 0) {
|
||||
el.textContent = count.toLocaleString();
|
||||
el.hidden = false;
|
||||
} else {
|
||||
el.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function updateLocalDnsSubtabBadges() {
|
||||
setSubtabBadge('localDnsSubtabBadge', (window.localDnsData || []).length);
|
||||
setSubtabBadge('dnsConflictsSubtabBadge', (window.dnsConflictsData || []).length);
|
||||
setSubtabBadge('p2pConflictsSubtabBadge', (window.p2pDomainConflictsData || []).length);
|
||||
}
|
||||
|
||||
function updateListChrome({ totalData, filteredData, searchId, emptyId, filteredEmptyId, tableSelector, endId, countId, countEmptyLabel, countSingular, countPlural }) {
|
||||
const totalCount = (totalData || []).length;
|
||||
const visibleCount = (filteredData || totalData || []).length;
|
||||
const searchEl = document.getElementById(searchId);
|
||||
const hasSearch = Boolean(searchEl?.value?.trim());
|
||||
|
||||
const trulyEmpty = totalCount === 0;
|
||||
const filteredToZero = !trulyEmpty && visibleCount === 0 && hasSearch;
|
||||
|
||||
const emptyEl = document.getElementById(emptyId);
|
||||
const filteredEmptyEl = document.getElementById(filteredEmptyId);
|
||||
const endEl = document.getElementById(endId);
|
||||
const tableEl = document.querySelector(tableSelector);
|
||||
const countEl = document.getElementById(countId);
|
||||
|
||||
if (emptyEl) emptyEl.classList.toggle('local-dns-empty--visible', trulyEmpty);
|
||||
if (filteredEmptyEl) filteredEmptyEl.classList.toggle('local-dns-empty--visible', filteredToZero);
|
||||
if (tableEl) tableEl.classList.toggle('local-dns-table--hidden', trulyEmpty || filteredToZero);
|
||||
if (endEl) endEl.classList.remove('local-dns-list-end--visible');
|
||||
|
||||
if (countEl) {
|
||||
if (trulyEmpty) {
|
||||
countEl.textContent = countEmptyLabel;
|
||||
} else if (hasSearch && visibleCount !== totalCount) {
|
||||
countEl.textContent = `${visibleCount.toLocaleString()} of ${totalCount.toLocaleString()} shown`;
|
||||
} else {
|
||||
const noun = totalCount === 1 ? countSingular : countPlural;
|
||||
countEl.textContent = `${totalCount.toLocaleString()} ${noun}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateLocalDnsChrome(filteredCount) {
|
||||
const visibleCount = filteredCount != null
|
||||
? filteredCount
|
||||
: (window.filteredLocalDns || window.localDnsData || []).length;
|
||||
|
||||
updateListChrome({
|
||||
totalData: window.localDnsData,
|
||||
filteredData: window.filteredLocalDns,
|
||||
searchId: 'search-local-dns',
|
||||
emptyId: 'localDnsEmpty',
|
||||
filteredEmptyId: 'localDnsFilteredEmpty',
|
||||
tableSelector: '.local-dns-table',
|
||||
endId: 'localDnsEnd',
|
||||
countId: 'localDnsCount',
|
||||
countEmptyLabel: 'No local DNS records',
|
||||
countSingular: 'record',
|
||||
countPlural: 'records'
|
||||
});
|
||||
|
||||
const stats = computeLocalDnsStats(window.localDnsData || []);
|
||||
const setStat = (id, value) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = value.toLocaleString();
|
||||
};
|
||||
setStat('localDnsStatTotal', stats.total);
|
||||
setStat('localDnsStatAddress', stats.address);
|
||||
setStat('localDnsStatCname', stats.cname);
|
||||
setStat('localDnsStatOther', stats.other);
|
||||
|
||||
const overviewEl = document.getElementById('localDnsOverviewLine');
|
||||
if (overviewEl) {
|
||||
overviewEl.textContent = stats.total === 0
|
||||
? 'No overrides configured'
|
||||
: `${stats.address} address · ${stats.cname} CNAME`;
|
||||
}
|
||||
|
||||
updateLocalDnsSubtabBadges();
|
||||
}
|
||||
|
||||
function updateDnsConflictsChrome(filteredCount) {
|
||||
updateListChrome({
|
||||
totalData: window.dnsConflictsData,
|
||||
filteredData: window.filteredDnsConflicts,
|
||||
searchId: 'search-dns-conflicts',
|
||||
emptyId: 'dnsConflictsEmpty',
|
||||
filteredEmptyId: 'dnsConflictsFilteredEmpty',
|
||||
tableSelector: '.local-dns-conflicts-table',
|
||||
endId: 'dnsConflictsEnd',
|
||||
countId: 'dnsConflictsCount',
|
||||
countEmptyLabel: 'No DNS conflicts',
|
||||
countSingular: 'conflict',
|
||||
countPlural: 'conflicts'
|
||||
});
|
||||
updateLocalDnsSubtabBadges();
|
||||
}
|
||||
|
||||
function updateP2pConflictsChrome(filteredCount) {
|
||||
updateListChrome({
|
||||
totalData: window.p2pDomainConflictsData,
|
||||
filteredData: window.filteredP2pDomainConflicts,
|
||||
searchId: 'search-p2p-domain-conflicts',
|
||||
emptyId: 'p2pConflictsEmpty',
|
||||
filteredEmptyId: 'p2pConflictsFilteredEmpty',
|
||||
tableSelector: '.local-dns-p2p-table',
|
||||
endId: 'p2pConflictsEnd',
|
||||
countId: 'p2pConflictsCount',
|
||||
countEmptyLabel: 'No P2P conflicts',
|
||||
countSingular: 'conflict',
|
||||
countPlural: 'conflicts'
|
||||
});
|
||||
updateLocalDnsSubtabBadges();
|
||||
}
|
||||
|
||||
function showLocalDnsListEnd() {
|
||||
const totalCount = (window.localDnsData || []).length;
|
||||
const visibleCount = (window.filteredLocalDns || window.localDnsData || []).length;
|
||||
const endEl = document.getElementById('localDnsEnd');
|
||||
if (endEl && totalCount > 0 && visibleCount > 0) endEl.classList.add('local-dns-list-end--visible');
|
||||
}
|
||||
|
||||
function showDnsConflictsListEnd() {
|
||||
const totalCount = (window.dnsConflictsData || []).length;
|
||||
const visibleCount = (window.filteredDnsConflicts || window.dnsConflictsData || []).length;
|
||||
const endEl = document.getElementById('dnsConflictsEnd');
|
||||
if (endEl && totalCount > 0 && visibleCount > 0) endEl.classList.add('local-dns-list-end--visible');
|
||||
}
|
||||
|
||||
function showP2pConflictsListEnd() {
|
||||
const totalCount = (window.p2pDomainConflictsData || []).length;
|
||||
const visibleCount = (window.filteredP2pDomainConflicts || window.p2pDomainConflictsData || []).length;
|
||||
const endEl = document.getElementById('p2pConflictsEnd');
|
||||
if (endEl && totalCount > 0 && visibleCount > 0) endEl.classList.add('local-dns-list-end--visible');
|
||||
}
|
||||
|
||||
function localDnsFieldHtml(label, inputHtml) {
|
||||
return `<label class="local-dns-form-field"><span class="local-dns-form-label">${label}</span>${inputHtml}</label>`;
|
||||
}
|
||||
|
||||
function openLocalDnsModal(editIndex = -1) {
|
||||
const modal = document.getElementById('localDnsModal');
|
||||
const title = document.getElementById('local-dns-title');
|
||||
@@ -42,32 +234,44 @@ function updateLocalForm(rec = null) {
|
||||
switch (type) {
|
||||
case 'A':
|
||||
case 'AAAA':
|
||||
inputHtml = `<input id="local-data" placeholder="IP Address" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">`;
|
||||
inputHtml = localDnsFieldHtml('Value', `<input id="local-data" placeholder="IP address" class="${LOCAL_DNS_FIELD}">`);
|
||||
break;
|
||||
case 'CNAME':
|
||||
inputHtml = `<input id="local-data" placeholder="Target Domain" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">`;
|
||||
inputHtml = localDnsFieldHtml('Target', `<input id="local-data" placeholder="Target domain" class="${LOCAL_DNS_FIELD}">`);
|
||||
break;
|
||||
case 'TXT':
|
||||
inputHtml = `<input id="local-data" placeholder="Text" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">`;
|
||||
inputHtml = localDnsFieldHtml('Text', `<input id="local-data" placeholder="Text value" class="${LOCAL_DNS_FIELD}">`);
|
||||
break;
|
||||
case 'MX':
|
||||
inputHtml = `<input id="local-preference" type="number" placeholder="Preference" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"><input id="local-exchange" placeholder="Exchange" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">`;
|
||||
inputHtml = localDnsFieldHtml('Preference', `<input id="local-preference" type="number" placeholder="10" class="${LOCAL_DNS_FIELD}">`)
|
||||
+ localDnsFieldHtml('Exchange', `<input id="local-exchange" placeholder="mail.example.tld" class="${LOCAL_DNS_FIELD}">`);
|
||||
break;
|
||||
case 'SRV':
|
||||
inputHtml = `<input id="local-priority" type="number" placeholder="Priority" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"><input id="local-weight" type="number" placeholder="Weight" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"><input id="local-port" type="number" placeholder="Port" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"><input id="local-target" placeholder="Target" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">`;
|
||||
inputHtml = localDnsFieldHtml('Priority', `<input id="local-priority" type="number" placeholder="0" class="${LOCAL_DNS_FIELD}">`)
|
||||
+ localDnsFieldHtml('Weight', `<input id="local-weight" type="number" placeholder="0" class="${LOCAL_DNS_FIELD}">`)
|
||||
+ localDnsFieldHtml('Port', `<input id="local-port" type="number" placeholder="443" class="${LOCAL_DNS_FIELD}">`)
|
||||
+ localDnsFieldHtml('Target', `<input id="local-target" placeholder="host.example.tld" class="${LOCAL_DNS_FIELD}">`);
|
||||
break;
|
||||
case 'SOA':
|
||||
inputHtml = `<input id="local-mname" placeholder="Primary Name Server" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"><input id="local-rname" placeholder="Responsible Person" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"><input id="local-serial" type="number" placeholder="Serial" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"><input id="local-refresh" type="number" placeholder="Refresh" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"><input id="local-retry" type="number" placeholder="Retry" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"><input id="local-expire" type="number" placeholder="Expire" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"><input id="local-minimum" type="number" placeholder="Minimum TTL" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">`;
|
||||
inputHtml = localDnsFieldHtml('Primary NS', `<input id="local-mname" placeholder="ns1.example.tld" class="${LOCAL_DNS_FIELD}">`)
|
||||
+ localDnsFieldHtml('Responsible', `<input id="local-rname" placeholder="hostmaster.example.tld" class="${LOCAL_DNS_FIELD}">`)
|
||||
+ localDnsFieldHtml('Serial', `<input id="local-serial" type="number" placeholder="1" class="${LOCAL_DNS_FIELD}">`)
|
||||
+ localDnsFieldHtml('Refresh', `<input id="local-refresh" type="number" placeholder="3600" class="${LOCAL_DNS_FIELD}">`)
|
||||
+ localDnsFieldHtml('Retry', `<input id="local-retry" type="number" placeholder="600" class="${LOCAL_DNS_FIELD}">`)
|
||||
+ localDnsFieldHtml('Expire', `<input id="local-expire" type="number" placeholder="86400" class="${LOCAL_DNS_FIELD}">`)
|
||||
+ localDnsFieldHtml('Minimum TTL', `<input id="local-minimum" type="number" placeholder="300" class="${LOCAL_DNS_FIELD}">`);
|
||||
break;
|
||||
case 'CAA':
|
||||
inputHtml = `<input id="local-flags" type="number" placeholder="Flags" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"><input id="local-tag" placeholder="Tag (e.g., issue, issuewild, iodef)" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"><input id="local-value" placeholder="Value" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">`;
|
||||
inputHtml = localDnsFieldHtml('Flags', `<input id="local-flags" type="number" placeholder="0" class="${LOCAL_DNS_FIELD}">`)
|
||||
+ localDnsFieldHtml('Tag', `<input id="local-tag" placeholder="issue, issuewild, iodef" class="${LOCAL_DNS_FIELD}">`)
|
||||
+ localDnsFieldHtml('Value', `<input id="local-value" placeholder="ca.example.tld" class="${LOCAL_DNS_FIELD}">`);
|
||||
break;
|
||||
case 'NS':
|
||||
case 'PTR':
|
||||
inputHtml = `<input id="local-data" placeholder="Name Server" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">`;
|
||||
inputHtml = localDnsFieldHtml('Name server', `<input id="local-data" placeholder="ns.example.tld" class="${LOCAL_DNS_FIELD}">`);
|
||||
break;
|
||||
default:
|
||||
inputHtml = `<input id="local-data" placeholder="Record Data" class="w-full p-3 mb-4 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">`;
|
||||
inputHtml = localDnsFieldHtml('Record data', `<input id="local-data" placeholder="Record data" class="${LOCAL_DNS_FIELD}">`);
|
||||
break;
|
||||
}
|
||||
fields.innerHTML = inputHtml;
|
||||
@@ -299,4 +503,14 @@ window.submitLocalDns = submitLocalDns;
|
||||
window.editLocalDns = editLocalDns;
|
||||
window.deleteLocalDns = deleteLocalDns;
|
||||
window.toggleVersionPreference = toggleVersionPreference;
|
||||
window.toggleHashPreference = toggleHashPreference;
|
||||
window.classifyDnsRecordType = classifyDnsRecordType;
|
||||
window.formatLocalDnsValue = formatLocalDnsValue;
|
||||
window.updateLocalDnsChrome = updateLocalDnsChrome;
|
||||
window.updateDnsConflictsChrome = updateDnsConflictsChrome;
|
||||
window.updateP2pConflictsChrome = updateP2pConflictsChrome;
|
||||
window.updateLocalDnsSubtabBadges = updateLocalDnsSubtabBadges;
|
||||
window.showLocalDnsListEnd = showLocalDnsListEnd;
|
||||
window.showDnsConflictsListEnd = showDnsConflictsListEnd;
|
||||
window.showP2pConflictsListEnd = showP2pConflictsListEnd;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user