- Add govdomains_sync.py: clones CISA dotgov-data, parses CSV, tracks first_seen dates - Add /api/govdomains and /api/govdomains/stats Flask endpoints with range/type/search filters - Add NEWS FEED | .GOV TRACKER toggle to RADAR page - Domain type badges (Federal=red, State=blue, City=green, County=amber) - New domain detection with pulsing green highlight and NEW badge - Responsive grid layout with stats bar and result count
394 lines
18 KiB
JavaScript
394 lines
18 KiB
JavaScript
/* ─── RADAR: Live Intelligence Feed + .GOV Domain Tracker ─── */
|
|
(function() {
|
|
'use strict';
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// SHARED UTILITIES
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
function esc(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s || '';
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function timeAgo(dateStr) {
|
|
if (!dateStr) return '';
|
|
const now = new Date();
|
|
const then = new Date(dateStr);
|
|
const diff = Math.floor((now - then) / 1000);
|
|
if (diff < 60) return diff + 's ago';
|
|
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
|
if (diff < 604800) return Math.floor(diff / 86400) + 'd ago';
|
|
return Math.floor(diff / 604800) + 'w ago';
|
|
}
|
|
|
|
function extractDomain(url) {
|
|
try { return new URL(url).hostname.replace('www.', ''); }
|
|
catch(e) { return ''; }
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// NEWS FEED MODULE
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
const NewsFeed = {
|
|
API: '/api/radar',
|
|
REFRESH_INTERVAL: 15 * 60 * 1000,
|
|
currentSource: 'all',
|
|
allItems: [],
|
|
refreshTimer: null,
|
|
|
|
renderFeed: function(items) {
|
|
const feed = document.getElementById('radarFeed');
|
|
if (!items || items.length === 0) {
|
|
feed.innerHTML = '<div class="radar-empty">NO SIGNALS DETECTED ON CURRENT FREQUENCY</div>';
|
|
return;
|
|
}
|
|
let html = '';
|
|
items.forEach(function(item) {
|
|
const domain = extractDomain(item.url);
|
|
const ago = timeAgo(item.published);
|
|
const sourceClass = 'source-' + item.source_id;
|
|
const sourceLabel = item.source || 'UNKNOWN';
|
|
html += '<div class="radar-item">';
|
|
html += ' <div class="radar-item-time">' + esc(ago) + '</div>';
|
|
html += ' <div class="radar-item-source ' + sourceClass + '">' + esc(sourceLabel) + '</div>';
|
|
html += ' <div class="radar-item-content">';
|
|
html += ' <a href="' + esc(item.url) + '" class="radar-item-title" target="_blank" rel="noopener">' + esc(item.title) + '</a>';
|
|
html += ' <div class="radar-item-meta">';
|
|
if (domain) html += '<span class="radar-item-domain">' + esc(domain) + '</span>';
|
|
if (item.comments_url) html += '<a href="' + esc(item.comments_url) + '" class="radar-item-comments" target="_blank" rel="noopener">COMMENTS</a>';
|
|
html += ' </div></div></div>';
|
|
});
|
|
feed.innerHTML = html;
|
|
},
|
|
|
|
applyFilters: function() {
|
|
var q = document.getElementById('radarSearch').value.trim().toLowerCase();
|
|
var filtered = this.allItems;
|
|
if (this.currentSource !== 'all') {
|
|
filtered = filtered.filter(function(i) { return i.source_id === NewsFeed.currentSource; });
|
|
}
|
|
if (q) {
|
|
filtered = filtered.filter(function(i) {
|
|
return (i.title || '').toLowerCase().includes(q) ||
|
|
(i.summary || '').toLowerCase().includes(q);
|
|
});
|
|
}
|
|
document.getElementById('statTotal').textContent = filtered.length;
|
|
this.renderFeed(filtered);
|
|
},
|
|
|
|
fetch: function(forceRefresh) {
|
|
var self = this;
|
|
var feed = document.getElementById('radarFeed');
|
|
feed.innerHTML = '<div class="radar-loading">SCANNING FREQUENCIES...</div>';
|
|
var chain = forceRefresh
|
|
? fetch(this.API + '/refresh', { method: 'POST' }).then(function() { return fetch(self.API); })
|
|
: fetch(this.API);
|
|
chain.then(function(res) { return res.json(); })
|
|
.then(function(data) {
|
|
self.allItems = data.items || [];
|
|
document.getElementById('statTotal').textContent = self.allItems.length;
|
|
if (data.last_updated) {
|
|
var d = new Date(data.last_updated);
|
|
var pad = function(n) { return String(n).padStart(2, '0'); };
|
|
document.getElementById('statUpdated').textContent =
|
|
pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()) + ' UTC';
|
|
}
|
|
self.applyFilters();
|
|
})
|
|
.catch(function(err) {
|
|
console.error('RADAR fetch error:', err);
|
|
feed.innerHTML = '<div class="radar-empty">⚠ SIGNAL LOST — UNABLE TO REACH FEED API</div>';
|
|
});
|
|
},
|
|
|
|
init: function() {
|
|
var self = this;
|
|
// Source filters
|
|
document.querySelectorAll('#viewNewsfeed .radar-filter').forEach(function(btn) {
|
|
btn.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
document.querySelectorAll('#viewNewsfeed .radar-filter').forEach(function(b) { b.classList.remove('active'); });
|
|
this.classList.add('active');
|
|
self.currentSource = this.dataset.source;
|
|
self.applyFilters();
|
|
});
|
|
});
|
|
// Search
|
|
var searchTimeout;
|
|
document.getElementById('radarSearch').addEventListener('input', function() {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(function() { self.applyFilters(); }, 200);
|
|
});
|
|
// Refresh
|
|
document.getElementById('radarRefresh').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
this.classList.add('spinning');
|
|
var btn = this;
|
|
self.fetch(true).then(function() {
|
|
setTimeout(function() { btn.classList.remove('spinning'); }, 500);
|
|
});
|
|
});
|
|
// Initial fetch
|
|
this.fetch(false);
|
|
// Auto-refresh
|
|
this.refreshTimer = setInterval(function() { self.fetch(false); }, this.REFRESH_INTERVAL);
|
|
}
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// .GOV TRACKER MODULE
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
const GovTracker = {
|
|
API: '/api/govdomains',
|
|
STATS_API: '/api/govdomains/stats',
|
|
currentRange: 'all',
|
|
currentType: '',
|
|
currentSearch: '',
|
|
allDomains: [],
|
|
statsData: null,
|
|
loaded: false,
|
|
|
|
TYPE_COLORS: {
|
|
'Federal': { color: '#ff4444', bg: 'rgba(255,68,68,0.08)', border: 'rgba(255,68,68,0.35)' },
|
|
'Executive': { color: '#ff4444', bg: 'rgba(255,68,68,0.08)', border: 'rgba(255,68,68,0.35)' },
|
|
'Judicial': { color: '#ff6b6b', bg: 'rgba(255,107,107,0.08)', border: 'rgba(255,107,107,0.3)' },
|
|
'Legislative': { color: '#ff6b6b', bg: 'rgba(255,107,107,0.08)', border: 'rgba(255,107,107,0.3)' },
|
|
'State': { color: '#4488ff', bg: 'rgba(68,136,255,0.08)', border: 'rgba(68,136,255,0.35)' },
|
|
'Interstate': { color: '#44aaff', bg: 'rgba(68,170,255,0.08)', border: 'rgba(68,170,255,0.3)' },
|
|
'City': { color: '#00cc44', bg: 'rgba(0,204,68,0.08)', border: 'rgba(0,204,68,0.35)' },
|
|
'County': { color: '#ffaa00', bg: 'rgba(255,170,0,0.08)', border: 'rgba(255,170,0,0.35)' },
|
|
'Independent Intrastate': { color: '#bb88ff', bg: 'rgba(187,136,255,0.08)', border: 'rgba(187,136,255,0.3)' },
|
|
'Native Sovereign Nation': { color: '#ff8844', bg: 'rgba(255,136,68,0.08)', border: 'rgba(255,136,68,0.3)' }
|
|
},
|
|
|
|
getTypeStyle: function(type) {
|
|
return this.TYPE_COLORS[type] || { color: '#888', bg: 'rgba(136,136,136,0.08)', border: 'rgba(136,136,136,0.3)' };
|
|
},
|
|
|
|
renderDomains: function(domains) {
|
|
var feed = document.getElementById('govFeed');
|
|
if (!domains || domains.length === 0) {
|
|
feed.innerHTML = '<div class="radar-empty">NO .GOV DOMAINS MATCH CURRENT FILTERS</div>';
|
|
document.getElementById('govResultCount').textContent = '0 RESULTS';
|
|
return;
|
|
}
|
|
|
|
// Show count
|
|
document.getElementById('govResultCount').textContent =
|
|
domains.length.toLocaleString() + ' DOMAIN' + (domains.length !== 1 ? 'S' : '') + ' FOUND';
|
|
|
|
// Only render first 200 for performance
|
|
var visible = domains.slice(0, 200);
|
|
var html = '';
|
|
|
|
visible.forEach(function(d) {
|
|
var style = GovTracker.getTypeStyle(d.type);
|
|
var isNew = d.is_new;
|
|
var itemClass = 'gov-domain-item' + (isNew ? ' gov-new' : '');
|
|
|
|
html += '<div class="' + itemClass + '">';
|
|
|
|
// Domain name + link
|
|
html += '<div class="gov-domain-main">';
|
|
html += ' <a href="https://' + esc(d.domain) + '" class="gov-domain-name" target="_blank" rel="noopener">';
|
|
html += esc(d.domain);
|
|
html += ' </a>';
|
|
if (isNew) html += ' <span class="gov-new-badge">NEW</span>';
|
|
html += '</div>';
|
|
|
|
// Type badge
|
|
html += '<div class="gov-domain-type" style="color:' + style.color + ';background:' + style.bg + ';border-color:' + style.border + '">';
|
|
html += esc(d.type || 'UNKNOWN');
|
|
html += '</div>';
|
|
|
|
// Details
|
|
html += '<div class="gov-domain-details">';
|
|
if (d.agency) html += '<span class="gov-detail"><span class="gov-detail-label">AGENCY:</span> ' + esc(d.agency) + '</span>';
|
|
if (d.organization) html += '<span class="gov-detail"><span class="gov-detail-label">ORG:</span> ' + esc(d.organization) + '</span>';
|
|
if (d.city || d.state) {
|
|
var loc = [d.city, d.state].filter(Boolean).join(', ');
|
|
html += '<span class="gov-detail"><span class="gov-detail-label">LOC:</span> ' + esc(loc) + '</span>';
|
|
}
|
|
html += '</div>';
|
|
|
|
// First seen
|
|
html += '<div class="gov-domain-date">' + esc(d.first_seen || '—') + '</div>';
|
|
|
|
html += '</div>';
|
|
});
|
|
|
|
if (domains.length > 200) {
|
|
html += '<div class="gov-truncated">SHOWING 200 OF ' + domains.length.toLocaleString() + ' — REFINE SEARCH TO SEE MORE</div>';
|
|
}
|
|
|
|
feed.innerHTML = html;
|
|
},
|
|
|
|
fetchStats: function() {
|
|
var self = this;
|
|
return fetch(this.STATS_API)
|
|
.then(function(res) { return res.json(); })
|
|
.then(function(stats) {
|
|
self.statsData = stats;
|
|
document.getElementById('govStatTotal').textContent = (stats.total || 0).toLocaleString();
|
|
document.getElementById('govStatNew24').textContent = (stats.new_24h || 0).toLocaleString();
|
|
document.getElementById('govStatFederal').textContent = ((stats.by_type || {})['Federal'] || 0).toLocaleString();
|
|
document.getElementById('govStatState').textContent = ((stats.by_type || {})['State'] || 0).toLocaleString();
|
|
|
|
if (stats.last_sync) {
|
|
var d = new Date(stats.last_sync);
|
|
var pad = function(n) { return String(n).padStart(2, '0'); };
|
|
document.getElementById('govStatSync').textContent =
|
|
pad(d.getHours()) + ':' + pad(d.getMinutes()) + ' UTC';
|
|
}
|
|
|
|
// Populate type filter dropdown
|
|
var select = document.getElementById('govTypeFilter');
|
|
var currentVal = select.value;
|
|
// Clear existing options except first
|
|
while (select.options.length > 1) select.remove(1);
|
|
(stats.types_list || []).forEach(function(t) {
|
|
var opt = document.createElement('option');
|
|
opt.value = t;
|
|
opt.textContent = t.toUpperCase() + ' (' + ((stats.by_type || {})[t] || 0) + ')';
|
|
select.appendChild(opt);
|
|
});
|
|
if (currentVal) select.value = currentVal;
|
|
})
|
|
.catch(function(err) {
|
|
console.error('Gov stats error:', err);
|
|
});
|
|
},
|
|
|
|
fetchDomains: function() {
|
|
var self = this;
|
|
var feed = document.getElementById('govFeed');
|
|
feed.innerHTML = '<div class="radar-loading">QUERYING .GOV REGISTRY...</div>';
|
|
|
|
// Build query params
|
|
var params = new URLSearchParams();
|
|
if (this.currentRange !== 'all') params.set('range', this.currentRange);
|
|
if (this.currentType) params.set('type', this.currentType);
|
|
if (this.currentSearch) params.set('search', this.currentSearch);
|
|
params.set('limit', '2000');
|
|
|
|
var url = this.API + '?' + params.toString();
|
|
|
|
return fetch(url)
|
|
.then(function(res) { return res.json(); })
|
|
.then(function(data) {
|
|
self.allDomains = data.domains || [];
|
|
self.renderDomains(self.allDomains);
|
|
})
|
|
.catch(function(err) {
|
|
console.error('Gov domains error:', err);
|
|
feed.innerHTML = '<div class="radar-empty">⚠ UNABLE TO QUERY .GOV REGISTRY</div>';
|
|
});
|
|
},
|
|
|
|
loadAll: function() {
|
|
var self = this;
|
|
return Promise.all([this.fetchStats(), this.fetchDomains()]).then(function() {
|
|
self.loaded = true;
|
|
});
|
|
},
|
|
|
|
init: function() {
|
|
var self = this;
|
|
|
|
// Range filters
|
|
document.querySelectorAll('.gov-range').forEach(function(btn) {
|
|
btn.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
document.querySelectorAll('.gov-range').forEach(function(b) { b.classList.remove('active'); });
|
|
this.classList.add('active');
|
|
self.currentRange = this.dataset.range;
|
|
self.fetchDomains();
|
|
});
|
|
});
|
|
|
|
// Type filter
|
|
document.getElementById('govTypeFilter').addEventListener('change', function() {
|
|
self.currentType = this.value;
|
|
self.fetchDomains();
|
|
});
|
|
|
|
// Search
|
|
var searchTimeout;
|
|
document.getElementById('govSearch').addEventListener('input', function() {
|
|
clearTimeout(searchTimeout);
|
|
var val = this.value.trim();
|
|
searchTimeout = setTimeout(function() {
|
|
self.currentSearch = val;
|
|
self.fetchDomains();
|
|
}, 300);
|
|
});
|
|
|
|
// Refresh
|
|
document.getElementById('govRefresh').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
this.classList.add('spinning');
|
|
var btn = this;
|
|
self.loadAll().then(function() {
|
|
setTimeout(function() { btn.classList.remove('spinning'); }, 500);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// VIEW TOGGLE CONTROLLER
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
function initViewToggle() {
|
|
var subtitles = {
|
|
newsfeed: '> Live intelligence feed — intercepted from Hacker News, Reddit, and Lobste.rs',
|
|
govtracker: '> .GOV domain intelligence — tracking U.S. government infrastructure registry'
|
|
};
|
|
|
|
document.querySelectorAll('.radar-view-btn').forEach(function(btn) {
|
|
btn.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
var view = this.dataset.view;
|
|
|
|
// Toggle button states
|
|
document.querySelectorAll('.radar-view-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
this.classList.add('active');
|
|
|
|
// Toggle views
|
|
document.getElementById('viewNewsfeed').classList.toggle('hidden', view !== 'newsfeed');
|
|
document.getElementById('viewGovtracker').classList.toggle('hidden', view !== 'govtracker');
|
|
|
|
// Update subtitle
|
|
document.getElementById('radarSubtitle').innerHTML = subtitles[view] || '';
|
|
|
|
// Lazy-load gov tracker data on first switch
|
|
if (view === 'govtracker' && !GovTracker.loaded) {
|
|
GovTracker.loadAll();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// BOOT
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
function init() {
|
|
initViewToggle();
|
|
NewsFeed.init();
|
|
GovTracker.init();
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|