feat: Add persistent domain filters with live data updates
- Add three filter dropdowns to domains tab: Consensus Status, Hash Type, and Ownership - Implement AND logic filtering with immediate table updates for live data - Create persistent filter settings system saving to cache/filterSettings.json - Add backend API endpoints: GET/POST /api/domain-filter-settings - Set default filters to: All Consensus States, Holesail Hash, All Ownership - Fix infinite scroll state management to handle filter changes properly - Auto-save filter preferences on change and restore on page load - Fix backend response handling (removed undefined sdk reference)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
const fs = require('fs').promises;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const state = require('../../../infrastructure/state');
|
||||
const { getAllEntries, getHashForDomain, doAutoVotes, getConsensusState, invalidateEntriesCache, removeOwnClaimAndVotes } = require('../../../core/core');
|
||||
const { addDomain } = require('../../../core/domains');
|
||||
@@ -12,6 +13,7 @@ const { broadcast } = require('../websocket');
|
||||
const { getPersistentPublicKey } = require('../../../infrastructure/utils');
|
||||
|
||||
const domainsFile = process.env.DOMAINS_FILE || './cache/domains.json';
|
||||
const filterSettingsFile = './cache/filterSettings.json';
|
||||
|
||||
async function handleDomainsRoutes(req, res) {
|
||||
const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname;
|
||||
@@ -327,6 +329,64 @@ async function handleDomainsRoutes(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
// Domain filter settings endpoints
|
||||
if (method === 'GET' && urlPath === '/api/domain-filter-settings') {
|
||||
try {
|
||||
let settings = { consensusStatus: 'all', hashType: 'holesail', ownership: 'all' };
|
||||
if (fs.existsSync(filterSettingsFile)) {
|
||||
const data = fs.readFileSync(filterSettingsFile, 'utf8');
|
||||
settings = { ...settings, ...JSON.parse(data) };
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(settings));
|
||||
return true;
|
||||
} catch (err) {
|
||||
logError('Admin', `Error loading domain filter settings: ${err.message}`);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ consensusStatus: 'all', hashType: 'holesail', ownership: 'all' }));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (method === 'POST' && urlPath === '/api/save-domain-filter-settings') {
|
||||
let body = '';
|
||||
req.on('data', chunk => { body += chunk; });
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const settings = JSON.parse(body);
|
||||
|
||||
// Validate settings
|
||||
const validConsensusValues = ['all', 'resolved', 'internal', 'conflict', 'tie', 'insufficient_quorum', 'no_claims', 'error', 'unknown'];
|
||||
const validHashTypeValues = ['all', 'internal', 'holesail'];
|
||||
const validOwnershipValues = ['all', 'local', 'remote'];
|
||||
|
||||
if (!validConsensusValues.includes(settings.consensusStatus) ||
|
||||
!validHashTypeValues.includes(settings.hashType) ||
|
||||
!validOwnershipValues.includes(settings.ownership)) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Invalid filter settings' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure cache directory exists
|
||||
const cacheDir = path.dirname(filterSettingsFile);
|
||||
if (!fs.existsSync(cacheDir)) {
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Save settings
|
||||
fs.writeFileSync(filterSettingsFile, JSON.stringify(settings, null, 2));
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true }));
|
||||
} catch (err) {
|
||||
logError('Admin', `Error saving domain filter settings: ${err.message}`);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Failed to save filter settings' }));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,11 @@ function initializeApp() {
|
||||
} else {
|
||||
window.genericFetch(tabId, true);
|
||||
}
|
||||
|
||||
// Load saved filter settings for domains tab
|
||||
if (tabId === 'domains' && window.loadDomainFilterSettings) {
|
||||
window.loadDomainFilterSettings();
|
||||
}
|
||||
}
|
||||
if (tabId === 'host') {
|
||||
// Show servers sub-tab by default
|
||||
|
||||
@@ -62,7 +62,30 @@
|
||||
<button onclick="openInfoModal('domains')" class="text-sm px-3 py-1 rounded transition-colors" style="background: var(--bg-glass); border: 1px solid var(--border-color); color: var(--text-secondary);" onmouseover="this.style.background='var(--bg-glass-hover)'" onmouseout="this.style.background='var(--bg-glass)'">Info</button>
|
||||
</h2>
|
||||
<div class="mb-6 flex-shrink-0">
|
||||
<input id="search-domains" type="text" placeholder="Search domains..." class="w-full p-3 rounded-lg focus:outline-none transition-all" style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary);" onfocus="this.style.borderColor='var(--primary)'; this.style.boxShadow='0 0 0 3px rgba(99, 102, 241, 0.1)'" onblur="this.style.borderColor='var(--border-color)'; this.style.boxShadow='none'" oninput="filterDomains()">
|
||||
<input id="search-domains" type="text" placeholder="Search domains..." class="w-full p-3 rounded-lg focus:outline-none transition-all mb-3" style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary);" onfocus="this.style.borderColor='var(--primary)'; this.style.boxShadow='0 0 0 3px rgba(99, 102, 241, 0.1)'" onblur="this.style.borderColor='var(--border-color)'; this.style.boxShadow='none'" oninput="filterDomains()">
|
||||
<div class="flex gap-3 flex-wrap">
|
||||
<select id="filter-consensus-status" onchange="filterDomains(); saveDomainFilterSettings()" class="flex-1 min-w-[150px] p-3 rounded-lg focus:outline-none transition-all" style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary);" onfocus="this.style.borderColor='var(--primary)'; this.style.boxShadow='0 0 0 3px rgba(99, 102, 241, 0.1)'" onblur="this.style.borderColor='var(--border-color)'; this.style.boxShadow='none'">
|
||||
<option value="all" selected>All Consensus Status</option>
|
||||
<option value="resolved">Resolved</option>
|
||||
<option value="internal">Internal</option>
|
||||
<option value="conflict">Conflict</option>
|
||||
<option value="tie">Tie</option>
|
||||
<option value="insufficient_quorum">No Quorum</option>
|
||||
<option value="no_claims">No Claims</option>
|
||||
<option value="error">Error</option>
|
||||
<option value="unknown">Unknown</option>
|
||||
</select>
|
||||
<select id="filter-hash-type" onchange="filterDomains(); saveDomainFilterSettings()" class="flex-1 min-w-[150px] p-3 rounded-lg focus:outline-none transition-all" style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary);" onfocus="this.style.borderColor='var(--primary)'; this.style.boxShadow='0 0 0 3px rgba(99, 102, 241, 0.1)'" onblur="this.style.borderColor='var(--border-color)'; this.style.boxShadow='none'">
|
||||
<option value="all">All Hash Types</option>
|
||||
<option value="internal">Internal</option>
|
||||
<option value="holesail" selected>Holesail Hash</option>
|
||||
</select>
|
||||
<select id="filter-ownership" onchange="filterDomains(); saveDomainFilterSettings()" class="flex-1 min-w-[150px] p-3 rounded-lg focus:outline-none transition-all" style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary);" onfocus="this.style.borderColor='var(--primary)'; this.style.boxShadow='0 0 0 3px rgba(99, 102, 241, 0.1)'" onblur="this.style.borderColor='var(--border-color)'; this.style.boxShadow='none'">
|
||||
<option value="all" selected>All Ownership</option>
|
||||
<option value="local">Local</option>
|
||||
<option value="remote">Remote</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto overflow-y-auto flex-1 min-h-0">
|
||||
<table class="w-full rounded-lg" style="background: var(--bg-glass); backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); border: 1px solid var(--border-color); box-shadow: var(--shadow-lg);">
|
||||
|
||||
@@ -41,7 +41,11 @@ async function genericFetch(tabId, shouldRender = true) {
|
||||
}
|
||||
|
||||
if (shouldRender) {
|
||||
genericFilter(tabId);
|
||||
if (tabId === 'domains' && window.filterDomains) {
|
||||
window.filterDomains();
|
||||
} else {
|
||||
genericFilter(tabId);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch ${tabId}:`, err);
|
||||
@@ -113,14 +117,36 @@ function genericRenderInfiniteScroll(tabId) {
|
||||
const searchEl = document.getElementById(config.searchId);
|
||||
const query = searchEl ? searchEl.value.toLowerCase() : '';
|
||||
const isNewSearch = state.lastQuery !== query;
|
||||
|
||||
// Reset if new search
|
||||
if (isNewSearch) {
|
||||
|
||||
// For domains tab, also check filter state
|
||||
let isNewFilterState = false;
|
||||
if (tabId === 'domains' && state.lastFilterState) {
|
||||
const consensusFilterEl = document.getElementById('filter-consensus-status');
|
||||
const hashTypeFilterEl = document.getElementById('filter-hash-type');
|
||||
const ownershipFilterEl = document.getElementById('filter-ownership');
|
||||
const consensusFilter = consensusFilterEl ? consensusFilterEl.value : 'all';
|
||||
const hashTypeFilter = hashTypeFilterEl ? hashTypeFilterEl.value : 'all';
|
||||
const ownershipFilter = ownershipFilterEl ? ownershipFilterEl.value : 'all';
|
||||
const currentFilterState = `${query}|${consensusFilter}|${hashTypeFilter}|${ownershipFilter}`;
|
||||
isNewFilterState = state.lastFilterState !== currentFilterState;
|
||||
}
|
||||
|
||||
// Reset if new search or new filter state
|
||||
if (isNewSearch || isNewFilterState) {
|
||||
state.loadedCount = 0;
|
||||
state.lastQuery = query;
|
||||
if (tabId === 'domains') {
|
||||
const consensusFilterEl = document.getElementById('filter-consensus-status');
|
||||
const hashTypeFilterEl = document.getElementById('filter-hash-type');
|
||||
const ownershipFilterEl = document.getElementById('filter-ownership');
|
||||
const consensusFilter = consensusFilterEl ? consensusFilterEl.value : 'all';
|
||||
const hashTypeFilter = hashTypeFilterEl ? hashTypeFilterEl.value : 'all';
|
||||
const ownershipFilter = ownershipFilterEl ? ownershipFilterEl.value : 'all';
|
||||
state.lastFilterState = `${query}|${consensusFilter}|${hashTypeFilter}|${ownershipFilter}`;
|
||||
}
|
||||
state.batchSize = calculateBatchSize(tabId);
|
||||
container.innerHTML = '';
|
||||
|
||||
|
||||
// Disconnect existing observer
|
||||
if (state.observer) {
|
||||
state.observer.disconnect();
|
||||
@@ -322,7 +348,106 @@ function renderEntriesLazy() {
|
||||
}
|
||||
|
||||
// Filter functions for each tab
|
||||
function filterDomains() { genericFilter('domains'); }
|
||||
function filterDomains() {
|
||||
const config = window.tabs.domains;
|
||||
if (!config) return;
|
||||
|
||||
const searchEl = document.getElementById(config.searchId);
|
||||
const consensusFilterEl = document.getElementById('filter-consensus-status');
|
||||
const hashTypeFilterEl = document.getElementById('filter-hash-type');
|
||||
const ownershipFilterEl = document.getElementById('filter-ownership');
|
||||
|
||||
if (!searchEl) return;
|
||||
|
||||
const query = searchEl.value.toLowerCase();
|
||||
const consensusFilter = consensusFilterEl ? consensusFilterEl.value : 'all';
|
||||
const hashTypeFilter = hashTypeFilterEl ? hashTypeFilterEl.value : 'all';
|
||||
const ownershipFilter = ownershipFilterEl ? ownershipFilterEl.value : 'all';
|
||||
|
||||
// Create a filter state key to track when filters change
|
||||
const currentFilterState = `${query}|${consensusFilter}|${hashTypeFilter}|${ownershipFilter}`;
|
||||
|
||||
// Check if infinite scroll state exists and if filters have changed
|
||||
const needsReset = !window.infiniteScrollState ||
|
||||
!window.infiniteScrollState['domains'] ||
|
||||
window.infiniteScrollState['domains'].lastFilterState !== currentFilterState;
|
||||
|
||||
const data = window[config.dataKey];
|
||||
if (!data || !Array.isArray(data)) return;
|
||||
|
||||
const filtered = data.filter(item => {
|
||||
// Text search filter (existing functionality)
|
||||
const matchesText = !query || config.filter(item, query);
|
||||
if (!matchesText) return false;
|
||||
|
||||
// Consensus status filter
|
||||
let matchesConsensus = true;
|
||||
if (consensusFilter !== 'all') {
|
||||
const itemStatus = item.consensusStatus || 'unknown';
|
||||
if (consensusFilter === 'insufficient_quorum') {
|
||||
matchesConsensus = itemStatus === 'insufficient_quorum';
|
||||
} else {
|
||||
matchesConsensus = itemStatus === consensusFilter;
|
||||
}
|
||||
}
|
||||
if (!matchesConsensus) return false;
|
||||
|
||||
// Hash type filter
|
||||
let matchesHashType = true;
|
||||
if (hashTypeFilter !== 'all') {
|
||||
if (hashTypeFilter === 'internal') {
|
||||
matchesHashType = item.hash === 'internal' || item.hash === 'none';
|
||||
} else if (hashTypeFilter === 'holesail') {
|
||||
matchesHashType = item.hash !== 'internal' && item.hash !== 'none';
|
||||
}
|
||||
}
|
||||
if (!matchesHashType) return false;
|
||||
|
||||
// Ownership filter
|
||||
let matchesOwnership = true;
|
||||
if (ownershipFilter !== 'all') {
|
||||
if (ownershipFilter === 'local') {
|
||||
matchesOwnership = item.isLocal || item.isOwner;
|
||||
} else if (ownershipFilter === 'remote') {
|
||||
matchesOwnership = !item.isLocal && !item.isOwner;
|
||||
}
|
||||
}
|
||||
if (!matchesOwnership) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
window[config.filteredKey] = filtered;
|
||||
|
||||
// Reset infinite scroll state if filters changed
|
||||
if (needsReset && window.infiniteScrollState && window.infiniteScrollState['domains']) {
|
||||
const state = window.infiniteScrollState['domains'];
|
||||
state.loadedCount = 0;
|
||||
state.lastQuery = query; // Keep lastQuery in sync
|
||||
state.lastFilterState = currentFilterState;
|
||||
state.batchSize = calculateBatchSize('domains');
|
||||
|
||||
// Disconnect existing observer
|
||||
if (state.observer) {
|
||||
state.observer.disconnect();
|
||||
state.observer = null;
|
||||
}
|
||||
|
||||
// Clear container
|
||||
const container = document.getElementById(config.containerId);
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Use infinite scroll for rendering
|
||||
genericRenderInfiniteScroll('domains');
|
||||
|
||||
// Update the filter state after rendering
|
||||
if (window.infiniteScrollState && window.infiniteScrollState['domains']) {
|
||||
window.infiniteScrollState['domains'].lastFilterState = currentFilterState;
|
||||
}
|
||||
}
|
||||
function filterEntries() { genericFilter('entries'); }
|
||||
function filterPeers() { genericFilter('peers'); }
|
||||
function filterCerts() { genericFilter('certs'); }
|
||||
|
||||
@@ -73,8 +73,63 @@ function removeDomain(domain) {
|
||||
}
|
||||
}
|
||||
|
||||
// Domain filter settings functions
|
||||
async function loadDomainFilterSettings() {
|
||||
try {
|
||||
const response = await fetch('/api/domain-filter-settings');
|
||||
if (response.ok) {
|
||||
const settings = await response.json();
|
||||
|
||||
// Apply saved settings to filter dropdowns
|
||||
const consensusEl = document.getElementById('filter-consensus-status');
|
||||
const hashTypeEl = document.getElementById('filter-hash-type');
|
||||
const ownershipEl = document.getElementById('filter-ownership');
|
||||
|
||||
if (consensusEl && settings.consensusStatus) {
|
||||
consensusEl.value = settings.consensusStatus;
|
||||
}
|
||||
if (hashTypeEl && settings.hashType) {
|
||||
hashTypeEl.value = settings.hashType;
|
||||
}
|
||||
if (ownershipEl && settings.ownership) {
|
||||
ownershipEl.value = settings.ownership;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('No saved domain filter settings found, using defaults');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDomainFilterSettings() {
|
||||
try {
|
||||
const consensusEl = document.getElementById('filter-consensus-status');
|
||||
const hashTypeEl = document.getElementById('filter-hash-type');
|
||||
const ownershipEl = document.getElementById('filter-ownership');
|
||||
|
||||
const settings = {
|
||||
consensusStatus: consensusEl ? consensusEl.value : 'all',
|
||||
hashType: hashTypeEl ? hashTypeEl.value : 'all',
|
||||
ownership: ownershipEl ? ownershipEl.value : 'all'
|
||||
};
|
||||
|
||||
const response = await fetch('/api/save-domain-filter-settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to save domain filter settings');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error saving domain filter settings:', err);
|
||||
}
|
||||
}
|
||||
|
||||
window.renderDnsConflicts = renderDnsConflicts;
|
||||
window.openAddModal = openAddModal;
|
||||
window.submitAddDomain = submitAddDomain;
|
||||
window.removeDomain = removeDomain;
|
||||
window.loadDomainFilterSettings = loadDomainFilterSettings;
|
||||
window.saveDomainFilterSettings = saveDomainFilterSettings;
|
||||
|
||||
|
||||
@@ -76,8 +76,12 @@ function connectWebSocket() {
|
||||
|
||||
// Clear domains data and show disconnected state when WebSocket disconnects
|
||||
window.domainsData = [];
|
||||
if (window.activeTab === 'domains' && window.genericFilter) {
|
||||
window.genericFilter('domains');
|
||||
if (window.activeTab === 'domains') {
|
||||
if (window.filterDomains) {
|
||||
window.filterDomains();
|
||||
} else if (window.genericFilter) {
|
||||
window.genericFilter('domains');
|
||||
}
|
||||
}
|
||||
if (reconnectTimeout) {
|
||||
clearTimeout(reconnectTimeout);
|
||||
@@ -182,6 +186,7 @@ function connectWebSocket() {
|
||||
if (window.infiniteScrollState && window.infiniteScrollState['domains']) {
|
||||
window.infiniteScrollState['domains'].loadedCount = 0;
|
||||
window.infiniteScrollState['domains'].lastQuery = '';
|
||||
window.infiniteScrollState['domains'].lastFilterState = '';
|
||||
// Disconnect existing observer
|
||||
if (window.infiniteScrollState['domains'].observer) {
|
||||
window.infiniteScrollState['domains'].observer.disconnect();
|
||||
@@ -195,8 +200,12 @@ function connectWebSocket() {
|
||||
}
|
||||
|
||||
// Render if domains tab is active
|
||||
if (window.activeTab === 'domains' && window.genericFilter) {
|
||||
window.genericFilter('domains');
|
||||
if (window.activeTab === 'domains') {
|
||||
if (window.filterDomains) {
|
||||
window.filterDomains();
|
||||
} else if (window.genericFilter) {
|
||||
window.genericFilter('domains');
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user