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:
Raven Scott
2025-12-26 21:50:25 -05:00
parent be7bd5ee2b
commit 1888289067
6 changed files with 289 additions and 12 deletions
+61 -1
View File
@@ -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;
}
+5
View File
@@ -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
+24 -1
View File
@@ -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);">
+131 -6
View File
@@ -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;
+13 -4
View File
@@ -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;