696 lines
25 KiB
HTML
696 lines
25 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="theme-color" content="#000000">
|
|
<title>Peer Directory</title>
|
|
<link rel="manifest" href="/manifest.json">
|
|
<link rel="stylesheet" href="/css/tailwind.css">
|
|
<link rel="stylesheet" href="/css/style.css">
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<!-- Header -->
|
|
<header class="header">
|
|
<div class="header-content">
|
|
<h1>P2NS Directory</h1>
|
|
</div>
|
|
<div class="header-actions">
|
|
<a
|
|
href="https://p2ns.admin"
|
|
target="_blank"
|
|
class="btn btn-primary"
|
|
>
|
|
Go To Admin
|
|
</a>
|
|
<div class="status" role="status" aria-live="polite">
|
|
<div class="status-indicator" id="statusIndicator" aria-label="Connection status"></div>
|
|
<span id="statusText">Connecting...</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Search and Controls -->
|
|
<div class="mb-6 space-y-4 flex-shrink-0">
|
|
<input
|
|
id="searchInput"
|
|
type="text"
|
|
placeholder="Search domains or hashes..."
|
|
class="input"
|
|
autofocus>
|
|
<div class="flex gap-4 items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<label for="sortBy" class="text-sm" style="color: var(--text-secondary);">Sort by:</label>
|
|
<select
|
|
id="sortBy"
|
|
class="select"
|
|
style="width: auto; min-width: 250px;"
|
|
>
|
|
<option value="type" selected>Type (Remote → Local → Internal)</option>
|
|
<option value="name">Name (A-Z)</option>
|
|
<option value="name-desc">Name (Z-A)</option>
|
|
<option value="hash">Hash (A-Z)</option>
|
|
<option value="hash-desc">Hash (Z-A)</option>
|
|
<option value="type-desc">Type (Internal → Local → Remote)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Domain Table -->
|
|
<div class="table-container">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Domain</th>
|
|
<th>Hash</th>
|
|
<th>Type</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="domainList"></tbody>
|
|
</table>
|
|
<div id="scrollSentinel" style="height: 20px; width: 100%; flex-shrink: 0;"></div>
|
|
</div>
|
|
|
|
<!-- Loading indicator and info -->
|
|
<div class="mb-4 text-center text-sm text-tertiary flex-shrink-0" style="min-height: 1.5rem;">
|
|
<span id="paginationInfo">Loading...</span>
|
|
</div>
|
|
</div>
|
|
<script src="/js/websocket.js"></script>
|
|
<script>
|
|
let currentPage = 1;
|
|
let currentLimit = 20;
|
|
let currentSort = 'type';
|
|
let allDomains = [];
|
|
let totalDomains = 0;
|
|
let isSearching = false;
|
|
let isLoading = false;
|
|
let hasMore = true;
|
|
let scrollObserver = null;
|
|
|
|
function getTypeBadge(type) {
|
|
const badges = {
|
|
internal: {
|
|
icon: '⚙️',
|
|
text: 'Internal',
|
|
class: 'badge badge-internal'
|
|
},
|
|
local: {
|
|
icon: '🏠',
|
|
text: 'Local',
|
|
class: 'badge badge-local'
|
|
},
|
|
remote: {
|
|
icon: '🌐',
|
|
text: 'Remote',
|
|
class: 'badge badge-remote'
|
|
}
|
|
};
|
|
const badge = badges[type] || badges.remote;
|
|
return `<span class="${badge.class}">
|
|
<span>${badge.icon}</span>
|
|
<span>${badge.text}</span>
|
|
</span>`;
|
|
}
|
|
|
|
function renderDomain(domainObj, options = {}) {
|
|
const tr = document.createElement('tr');
|
|
|
|
// Add animation classes if specified
|
|
if (options.animateIn) {
|
|
tr.classList.add('fade-in');
|
|
}
|
|
if (options.highlight) {
|
|
tr.classList.add('highlight');
|
|
}
|
|
|
|
// Domain cell with link
|
|
const domainCell = document.createElement('td');
|
|
const domainLink = document.createElement('a');
|
|
domainLink.href = 'https://' + domainObj.domain;
|
|
domainLink.textContent = domainObj.domain;
|
|
domainLink.target = '_blank';
|
|
domainCell.appendChild(domainLink);
|
|
|
|
// Hash cell
|
|
const hashCell = document.createElement('td');
|
|
hashCell.style.cssText = 'word-break: break-all; color: var(--text-tertiary);';
|
|
hashCell.textContent = domainObj.hash || 'none';
|
|
|
|
// Type cell with badge
|
|
const typeCell = document.createElement('td');
|
|
typeCell.innerHTML = getTypeBadge(domainObj.type);
|
|
|
|
tr.appendChild(domainCell);
|
|
tr.appendChild(hashCell);
|
|
tr.appendChild(typeCell);
|
|
|
|
return tr;
|
|
}
|
|
|
|
async function fetchAllDomains(sort = 'type') {
|
|
try {
|
|
// Fetch all domains with a large limit
|
|
const params = new URLSearchParams({ page: '1', limit: '10000', sort });
|
|
const response = await fetch('/domains?' + params.toString());
|
|
const data = await response.json();
|
|
|
|
if (data.domains) {
|
|
allDomains = data.domains;
|
|
totalDomains = data.total;
|
|
} else if (Array.isArray(data)) {
|
|
// Fallback for old API format
|
|
allDomains = data.map(d => typeof d === 'string' ? { domain: d, type: 'remote', hash: 'none', isLocal: false } : d);
|
|
totalDomains = allDomains.length;
|
|
}
|
|
return allDomains;
|
|
} catch (err) {
|
|
console.error('Failed to fetch all domains:', err);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function fetchDomains(page = 1, limit = 20, sort = 'type', searchQuery = '', append = false, retryCount = 0) {
|
|
if (isLoading) return;
|
|
|
|
// If we have WebSocket data, use it instead of making API calls
|
|
if (allDomains.length > 0 && window.wsClient && window.wsClient.isConnected) {
|
|
// Use WebSocket data
|
|
handleDomainsFromWebSocket(page, limit, sort, searchQuery, append);
|
|
return;
|
|
}
|
|
|
|
// Fallback to REST API
|
|
await fetchDomainsFromAPI(page, limit, sort, searchQuery, append, retryCount);
|
|
}
|
|
|
|
function handleDomainsFromWebSocket(page = 1, limit = 20, sort = 'type', searchQuery = '', append = false) {
|
|
let domains = [];
|
|
let total = 0;
|
|
|
|
if (searchQuery) {
|
|
// Filter domains client-side
|
|
const query = searchQuery.toLowerCase();
|
|
const filtered = allDomains.filter(d =>
|
|
d.domain.toLowerCase().includes(query) ||
|
|
(d.hash && d.hash.toLowerCase().includes(query))
|
|
);
|
|
const startIndex = (page - 1) * limit;
|
|
const endIndex = startIndex + limit;
|
|
domains = filtered.slice(startIndex, endIndex);
|
|
total = filtered.length;
|
|
hasMore = endIndex < filtered.length;
|
|
} else {
|
|
// Use all domains
|
|
const startIndex = (page - 1) * limit;
|
|
const endIndex = startIndex + limit;
|
|
domains = allDomains.slice(startIndex, endIndex);
|
|
total = allDomains.length;
|
|
hasMore = endIndex < allDomains.length;
|
|
}
|
|
|
|
renderDomains(domains, append);
|
|
updatePaginationInfo(page, total, limit);
|
|
|
|
// Setup infinite scroll if there's more to load
|
|
setTimeout(() => {
|
|
if (hasMore) {
|
|
setupInfiniteScroll();
|
|
} else {
|
|
cleanupInfiniteScroll();
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
async function fetchDomainsFromAPI(page = 1, limit = 20, sort = 'type', searchQuery = '', append = false, retryCount = 0) {
|
|
if (isLoading) return;
|
|
|
|
try {
|
|
isLoading = true;
|
|
let domains = [];
|
|
let total = 0;
|
|
|
|
if (searchQuery) {
|
|
// When searching, fetch all domains and filter client-side
|
|
if (allDomains.length === 0 || currentSort !== sort) {
|
|
await fetchAllDomains(sort);
|
|
currentSort = sort;
|
|
}
|
|
|
|
const query = searchQuery.toLowerCase();
|
|
const filtered = allDomains.filter(d =>
|
|
d.domain.toLowerCase().includes(query) ||
|
|
(d.hash && d.hash.toLowerCase().includes(query))
|
|
);
|
|
const startIndex = (page - 1) * limit;
|
|
const endIndex = startIndex + limit;
|
|
domains = filtered.slice(startIndex, endIndex);
|
|
total = filtered.length;
|
|
hasMore = endIndex < filtered.length;
|
|
|
|
renderDomains(domains, append);
|
|
updatePaginationInfo(page, total, limit);
|
|
} else {
|
|
// Normal paginated API call
|
|
const params = new URLSearchParams({ page: page.toString(), limit: limit.toString(), sort });
|
|
const response = await fetch('/domains?' + params.toString());
|
|
const data = await response.json();
|
|
|
|
if (data.domains) {
|
|
domains = data.domains;
|
|
total = data.total;
|
|
const totalPages = data.totalPages;
|
|
hasMore = page < totalPages;
|
|
|
|
renderDomains(domains, append);
|
|
updatePaginationInfo(page, total, limit);
|
|
totalDomains = data.total;
|
|
} else {
|
|
// Fallback
|
|
if (allDomains.length === 0) {
|
|
allDomains = Array.isArray(data) ? data.map(d => typeof d === 'string' ? { domain: d, type: 'remote', hash: 'none', isLocal: false } : d) : [];
|
|
totalDomains = allDomains.length;
|
|
}
|
|
const startIndex = (page - 1) * limit;
|
|
const endIndex = startIndex + limit;
|
|
domains = allDomains.slice(startIndex, endIndex);
|
|
total = allDomains.length;
|
|
hasMore = endIndex < allDomains.length;
|
|
|
|
renderDomains(domains, append);
|
|
updatePaginationInfo(page, total, limit);
|
|
}
|
|
}
|
|
|
|
// Setup infinite scroll if there's more to load
|
|
// Use setTimeout to ensure DOM is updated
|
|
setTimeout(() => {
|
|
if (hasMore) {
|
|
setupInfiniteScroll();
|
|
} else {
|
|
cleanupInfiniteScroll();
|
|
}
|
|
}, 100);
|
|
|
|
// If no domains found and not searching, retry after a delay
|
|
if (domains.length === 0 && !searchQuery && !append) {
|
|
const domainList = document.getElementById('domainList');
|
|
domainList.innerHTML = '<tr><td colspan="3" class="p-8 text-center" style="color: var(--text-tertiary);">No domains found. Retrying...</td>';
|
|
|
|
// Wait 2 seconds before retrying
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
// Retry the request
|
|
isLoading = false;
|
|
return fetchDomainsFromAPI(page, limit, sort, searchQuery, append, retryCount + 1);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch domains:', err);
|
|
const domainList = document.getElementById('domainList');
|
|
if (!append) {
|
|
domainList.innerHTML = '<tr><td colspan="3" class="p-4 text-center" style="color: var(--error);">Failed to load domains. Retrying...</td>';
|
|
}
|
|
|
|
// Wait 2 seconds before retrying on error
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
// Retry the request
|
|
isLoading = false;
|
|
return fetchDomainsFromAPI(page, limit, sort, searchQuery, append, retryCount + 1);
|
|
} finally {
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
function renderDomains(domains, append = false, animationOptions = {}) {
|
|
const domainList = document.getElementById('domainList');
|
|
const tableContainer = document.querySelector('.table-container');
|
|
|
|
if (!append) {
|
|
domainList.innerHTML = '';
|
|
}
|
|
|
|
if (domains.length === 0 && !append) {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = '<td colspan="3" class="p-8 text-center" style="color: var(--text-tertiary);">No domains found.</td>';
|
|
domainList.appendChild(tr);
|
|
// Remove few-rows class when empty
|
|
if (tableContainer) {
|
|
tableContainer.classList.remove('few-rows');
|
|
}
|
|
return;
|
|
}
|
|
|
|
domains.forEach((domainObj, index) => {
|
|
const options = {
|
|
animateIn: animationOptions.animateAll || (animationOptions.animateIndices && animationOptions.animateIndices.includes(index)),
|
|
highlight: animationOptions.highlightAll || (animationOptions.highlightIndices && animationOptions.highlightIndices.includes(index))
|
|
};
|
|
const tr = renderDomain(domainObj, options);
|
|
|
|
if (animationOptions.animateAll && animationOptions.cascade) {
|
|
// Add cascading delay for bulk animations
|
|
tr.style.animationDelay = `${index * 50}ms`;
|
|
}
|
|
|
|
domainList.appendChild(tr);
|
|
});
|
|
|
|
// Add 'few-rows' class if there are 5 or fewer total rows for better appearance
|
|
if (tableContainer) {
|
|
const totalRows = domainList.querySelectorAll('tr').length;
|
|
if (totalRows <= 5) {
|
|
tableContainer.classList.add('few-rows');
|
|
} else {
|
|
tableContainer.classList.remove('few-rows');
|
|
}
|
|
}
|
|
}
|
|
|
|
function updatePaginationInfo(page, total, limit) {
|
|
const paginationInfo = document.getElementById('paginationInfo');
|
|
const domainList = document.getElementById('domainList');
|
|
const loadedCount = domainList.querySelectorAll('tr').length;
|
|
paginationInfo.textContent = `Showing ${loadedCount} of ${total} domains${hasMore ? ' (scroll for more)' : ''}`;
|
|
}
|
|
|
|
function sortDomains(sortBy) {
|
|
// Define priority order for domain types
|
|
const typeOrder = { remote: 0, local: 1, internal: 2, plugin: 3 };
|
|
|
|
// Sort by domain name (ascending - A to Z)
|
|
if (sortBy === 'name') {
|
|
allDomains.sort((a, b) => a.domain.localeCompare(b.domain, undefined, { sensitivity: 'base' }));
|
|
}
|
|
// Sort by domain name (descending - Z to A)
|
|
else if (sortBy === 'name-desc') {
|
|
allDomains.sort((a, b) => b.domain.localeCompare(a.domain, undefined, { sensitivity: 'base' }));
|
|
}
|
|
// Sort by hash (ascending - alphabetical)
|
|
else if (sortBy === 'hash') {
|
|
allDomains.sort((a, b) => {
|
|
const hashA = (a.hash || 'none').toLowerCase();
|
|
const hashB = (b.hash || 'none').toLowerCase();
|
|
return hashA.localeCompare(hashB, undefined, { sensitivity: 'base' });
|
|
});
|
|
}
|
|
// Sort by hash (descending - reverse alphabetical)
|
|
else if (sortBy === 'hash-desc') {
|
|
allDomains.sort((a, b) => {
|
|
const hashA = (a.hash || 'none').toLowerCase();
|
|
const hashB = (b.hash || 'none').toLowerCase();
|
|
return hashB.localeCompare(hashA, undefined, { sensitivity: 'base' });
|
|
});
|
|
}
|
|
// Sort by type (descending - remote, local, internal) then alphabetically
|
|
else if (sortBy === 'type-desc') {
|
|
allDomains.sort((a, b) => {
|
|
// First compare by type (reverse order)
|
|
const typeDiff = typeOrder[b.type] - typeOrder[a.type];
|
|
if (typeDiff !== 0) return typeDiff;
|
|
// If same type, sort alphabetically by domain name
|
|
return a.domain.localeCompare(b.domain, undefined, { sensitivity: 'base' });
|
|
});
|
|
}
|
|
// Default: Sort by type (ascending - remote, local, internal) then alphabetically
|
|
else {
|
|
allDomains.sort((a, b) => {
|
|
// First compare by type (remote < local < internal)
|
|
const typeDiff = typeOrder[a.type] - typeOrder[b.type];
|
|
if (typeDiff !== 0) return typeDiff;
|
|
// If same type, sort alphabetically by domain name
|
|
return a.domain.localeCompare(b.domain, undefined, { sensitivity: 'base' });
|
|
});
|
|
}
|
|
}
|
|
|
|
function setupInfiniteScroll() {
|
|
cleanupInfiniteScroll();
|
|
|
|
const sentinel = document.getElementById('scrollSentinel');
|
|
const tableContainer = document.querySelector('.table-container');
|
|
if (!sentinel || !tableContainer) {
|
|
// Retry after a short delay if elements aren't ready
|
|
setTimeout(setupInfiniteScroll, 100);
|
|
return;
|
|
}
|
|
|
|
scrollObserver = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting && hasMore && !isLoading) {
|
|
console.log('Loading more domains...');
|
|
loadMoreDomains();
|
|
}
|
|
});
|
|
}, {
|
|
root: tableContainer,
|
|
rootMargin: '100px',
|
|
threshold: 0.1
|
|
});
|
|
|
|
scrollObserver.observe(sentinel);
|
|
console.log('Infinite scroll observer set up');
|
|
}
|
|
|
|
function cleanupInfiniteScroll() {
|
|
if (scrollObserver) {
|
|
scrollObserver.disconnect();
|
|
scrollObserver = null;
|
|
}
|
|
}
|
|
|
|
async function loadMoreDomains() {
|
|
if (isLoading || !hasMore) return;
|
|
|
|
currentPage++;
|
|
const searchQuery = document.getElementById('searchInput').value;
|
|
await fetchDomains(currentPage, currentLimit, currentSort, searchQuery, true);
|
|
}
|
|
|
|
// Event listeners
|
|
document.getElementById('searchInput').addEventListener('input', async (e) => {
|
|
const searchQuery = e.target.value;
|
|
currentPage = 1;
|
|
isSearching = searchQuery.length > 0;
|
|
cleanupInfiniteScroll();
|
|
|
|
if (isSearching && allDomains.length === 0) {
|
|
// If searching but no data loaded yet, fetch from API
|
|
await fetchDomains(currentPage, currentLimit, currentSort, searchQuery, false);
|
|
} else {
|
|
// Use WebSocket data or cached data
|
|
handleDomainsFromWebSocket(currentPage, currentLimit, currentSort, searchQuery, false);
|
|
}
|
|
});
|
|
|
|
document.getElementById('sortBy').addEventListener('change', async (e) => {
|
|
currentSort = e.target.value;
|
|
currentPage = 1;
|
|
cleanupInfiniteScroll();
|
|
|
|
// Sort the domains if we have them
|
|
if (allDomains.length > 0) {
|
|
sortDomains(currentSort);
|
|
const searchQuery = document.getElementById('searchInput').value;
|
|
handleDomainsFromWebSocket(currentPage, currentLimit, currentSort, searchQuery, false);
|
|
} else {
|
|
// No data yet, fetch from API
|
|
const searchQuery = document.getElementById('searchInput').value;
|
|
await fetchDomains(currentPage, currentLimit, currentSort, searchQuery, false);
|
|
}
|
|
});
|
|
|
|
// Show brief visual indicator for updates
|
|
function showUpdateIndicator() {
|
|
// Add a smooth highlight effect to the table container
|
|
const tableContainer = document.querySelector('.table-container');
|
|
if (tableContainer) {
|
|
tableContainer.classList.add('updating');
|
|
tableContainer.style.boxShadow = 'var(--shadow-glow)';
|
|
setTimeout(() => {
|
|
tableContainer.style.boxShadow = 'var(--shadow-lg), inset 0 1px 0 rgba(255, 255, 255, 0.05)';
|
|
tableContainer.classList.remove('updating');
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
// WebSocket Integration
|
|
let domainsPopulated = false;
|
|
let domainRequestInterval = null;
|
|
|
|
function requestDomainsUntilPopulated() {
|
|
if (domainsPopulated) {
|
|
if (domainRequestInterval) {
|
|
clearInterval(domainRequestInterval);
|
|
domainRequestInterval = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (window.wsClient && window.wsClient.isConnected) {
|
|
console.log('Requesting domains until populated...');
|
|
window.wsClient.requestDomains();
|
|
}
|
|
}
|
|
|
|
function initWebSocket() {
|
|
// Connect to WebSocket
|
|
window.wsClient.connect();
|
|
|
|
// Handle initial data from WebSocket
|
|
window.wsClient.on('init', (data) => {
|
|
console.log('WebSocket init received:', data);
|
|
if (data.domains && data.domains.length > 0) {
|
|
// Mark domains as populated
|
|
domainsPopulated = true;
|
|
// Clean up the request interval since we got domains
|
|
if (domainRequestInterval) {
|
|
clearInterval(domainRequestInterval);
|
|
domainRequestInterval = null;
|
|
}
|
|
|
|
// Replace entire domain list with fresh data (used for initial load and reconnection)
|
|
allDomains = data.domains;
|
|
totalDomains = data.domains.length;
|
|
|
|
// Clear current search and reset to first page
|
|
currentPage = 1;
|
|
hasMore = false; // WebSocket provides all data at once
|
|
isSearching = false;
|
|
cleanupInfiniteScroll();
|
|
|
|
// Clear search input and apply current sort
|
|
document.getElementById('searchInput').value = '';
|
|
sortDomains(currentSort);
|
|
|
|
// Render the complete domain list
|
|
renderDomains(allDomains, false);
|
|
updatePaginationInfo(1, totalDomains, currentLimit);
|
|
|
|
console.log(`Loaded ${totalDomains} domains via WebSocket`);
|
|
}
|
|
});
|
|
|
|
// Handle individual domain additions
|
|
window.wsClient.on('domain-added', (data) => {
|
|
console.log('Domain added via WebSocket:', data);
|
|
if (data && data.domain) {
|
|
// Add the domain to our list if not already present
|
|
const exists = allDomains.some(d => d.domain === data.domain);
|
|
if (!exists) {
|
|
console.log('Adding domain:', data);
|
|
allDomains.push(data);
|
|
sortDomains(currentSort);
|
|
console.log('Domains after add:', allDomains.map(d => ({domain: d.domain, type: d.type})));
|
|
showUpdateIndicator();
|
|
totalDomains = allDomains.length;
|
|
|
|
// Smoothly animate the new domain into view
|
|
const searchQuery = document.getElementById('searchInput').value;
|
|
if (!searchQuery) {
|
|
// For non-search mode, find where the new domain appears in the sorted list
|
|
const visibleDomains = allDomains.slice(0, currentLimit);
|
|
const newDomainIndex = visibleDomains.findIndex(d => d.domain === data.domain);
|
|
|
|
if (newDomainIndex >= 0) {
|
|
// New domain is in visible range, animate it in
|
|
renderDomains(visibleDomains, false, {
|
|
animateIndices: [newDomainIndex]
|
|
});
|
|
} else {
|
|
// New domain is not in visible range, just update normally
|
|
handleDomainsFromWebSocket(currentPage, currentLimit, currentSort, searchQuery, false);
|
|
}
|
|
} else {
|
|
// For search mode, just update normally
|
|
handleDomainsFromWebSocket(currentPage, currentLimit, currentSort, searchQuery, false);
|
|
}
|
|
} else {
|
|
console.log('Domain already exists:', data.domain);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle individual domain removals
|
|
window.wsClient.on('domain-removed', (data) => {
|
|
console.log('Domain removed via WebSocket:', data);
|
|
if (data.domain) {
|
|
// First, animate the existing row out if it's visible
|
|
const domainList = document.getElementById('domainList');
|
|
const rows = Array.from(domainList.children);
|
|
const rowToRemove = rows.find(row => {
|
|
const link = row.querySelector('a');
|
|
return link && link.textContent === data.domain;
|
|
});
|
|
|
|
if (rowToRemove) {
|
|
// Animate the row out
|
|
rowToRemove.classList.add('fade-out');
|
|
rowToRemove.addEventListener('animationend', () => {
|
|
// After animation completes, update the data and re-render
|
|
updateDataAfterRemoval(data.domain);
|
|
}, { once: true });
|
|
} else {
|
|
// Row not visible, just update data directly
|
|
updateDataAfterRemoval(data.domain);
|
|
}
|
|
}
|
|
});
|
|
|
|
function updateDataAfterRemoval(domainName) {
|
|
// Remove the domain from our list
|
|
const initialLength = allDomains.length;
|
|
console.log('Removing domain:', domainName);
|
|
console.log('Domains before remove:', allDomains.map(d => ({domain: d.domain, type: d.type})));
|
|
allDomains = allDomains.filter(d => d.domain !== domainName);
|
|
console.log('Domains after remove:', allDomains.map(d => ({domain: d.domain, type: d.type})));
|
|
|
|
if (allDomains.length !== initialLength) {
|
|
// Re-sort after removal to maintain consistency
|
|
sortDomains(currentSort);
|
|
showUpdateIndicator();
|
|
totalDomains = allDomains.length;
|
|
|
|
// Re-render the visible list to respect sort order and pagination
|
|
const searchQuery = document.getElementById('searchInput').value;
|
|
handleDomainsFromWebSocket(currentPage, currentLimit, currentSort, searchQuery, false);
|
|
}
|
|
}
|
|
|
|
|
|
// Handle connection status changes
|
|
window.wsClient.on('connected', () => {
|
|
console.log('WebSocket connected');
|
|
// If we don't have domains yet, keep requesting until populated
|
|
if (!domainsPopulated) {
|
|
// Request immediately
|
|
window.wsClient.requestDomains();
|
|
// Keep requesting every 2 seconds until domains are populated
|
|
domainRequestInterval = setInterval(requestDomainsUntilPopulated, 2000);
|
|
}
|
|
});
|
|
|
|
window.wsClient.on('disconnected', () => {
|
|
console.log('WebSocket disconnected');
|
|
domainsPopulated = false; // Reset flag on disconnect
|
|
// Clean up the request interval
|
|
if (domainRequestInterval) {
|
|
clearInterval(domainRequestInterval);
|
|
domainRequestInterval = null;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize WebSocket on page load
|
|
initWebSocket();
|
|
|
|
// Initial fetch (fallback if WebSocket not available)
|
|
fetchDomains(currentPage, currentLimit, currentSort, '', false);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|
|
|