feat: RADAR live tech news feed — HN, Reddit, Lobsters

This commit is contained in:
jae 2026-04-04 16:11:38 +00:00
parent 82b71fc9a0
commit c66e122786
4 changed files with 626 additions and 11 deletions

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""JAESWIFT HUD Backend API""" """JAESWIFT HUD Backend API"""
import json, os, time, subprocess, random, datetime, hashlib, zipfile, io, smtplib import json, os, time, subprocess, random, datetime, hashlib, zipfile, io, smtplib, threading
from functools import wraps from functools import wraps
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path from pathlib import Path
@ -978,5 +978,113 @@ def awesomelist_search():
if len(results) >= limit: if len(results) >= limit:
break break
return jsonify({'query': q, 'results': results, 'total': len(results)}) return jsonify({'query': q, 'results': results, 'total': len(results)})
# ─── RADAR: Live Tech News Feed ──────────────────────
import feedparser
RADAR_CACHE = {'items': [], 'last_fetch': 0}
RADAR_LOCK = threading.Lock()
RADAR_TTL = 900 # 15 minutes
RADAR_FEEDS = {
'hackernews': {
'url': 'https://hnrss.org/frontpage?count=50',
'label': 'HACKER NEWS',
'color': '#ff6600'
},
'reddit_technology': {
'url': 'https://www.reddit.com/r/technology/hot.rss?limit=30',
'label': 'R/TECHNOLOGY',
'color': '#ff4500'
},
'reddit_programming': {
'url': 'https://www.reddit.com/r/programming/hot.rss?limit=30',
'label': 'R/PROGRAMMING',
'color': '#ff4500'
},
'reddit_netsec': {
'url': 'https://www.reddit.com/r/netsec/hot.rss?limit=30',
'label': 'R/NETSEC',
'color': '#ff4500'
},
'lobsters': {
'url': 'https://lobste.rs/rss',
'label': 'LOBSTERS',
'color': '#ac130d'
}
}
def fetch_radar_feeds():
items = []
for src_id, src in RADAR_FEEDS.items():
try:
feed = feedparser.parse(src['url'])
for entry in feed.entries:
pub = ''
if hasattr(entry, 'published_parsed') and entry.published_parsed:
pub = time.strftime('%Y-%m-%dT%H:%M:%SZ', entry.published_parsed)
elif hasattr(entry, 'updated_parsed') and entry.updated_parsed:
pub = time.strftime('%Y-%m-%dT%H:%M:%SZ', entry.updated_parsed)
# Extract points/score from HN
score = 0
comments = 0
comments_url = ''
if 'hnrss' in src['url']:
# HN RSS includes comments link and points in description
if hasattr(entry, 'comments'):
comments_url = entry.comments
items.append({
'title': entry.get('title', 'Untitled'),
'url': entry.get('link', ''),
'source': src['label'],
'source_id': src_id,
'source_color': src['color'],
'published': pub,
'comments_url': comments_url,
'summary': (entry.get('summary', '') or '')[:200]
})
except Exception as e:
print(f'RADAR feed error ({src_id}): {e}')
# Sort by published date descending
items.sort(key=lambda x: x.get('published', ''), reverse=True)
return items
def get_radar_items():
now = time.time()
with RADAR_LOCK:
if now - RADAR_CACHE['last_fetch'] > RADAR_TTL or not RADAR_CACHE['items']:
RADAR_CACHE['items'] = fetch_radar_feeds()
RADAR_CACHE['last_fetch'] = now
return RADAR_CACHE['items']
@app.route('/api/radar')
def api_radar():
source = request.args.get('source', 'all').lower()
q = request.args.get('q', '').strip().lower()
limit = min(int(request.args.get('limit', 200)), 500)
items = get_radar_items()
if source != 'all':
items = [i for i in items if i['source_id'] == source or i['source'].lower() == source]
if q:
items = [i for i in items if q in i['title'].lower() or q in i.get('summary', '').lower()]
return jsonify({
'total': len(items[:limit]),
'sources': list(RADAR_FEEDS.keys()),
'last_updated': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(RADAR_CACHE.get('last_fetch', 0))),
'items': items[:limit]
})
@app.route('/api/radar/refresh', methods=['POST'])
def api_radar_refresh():
with RADAR_LOCK:
RADAR_CACHE['items'] = fetch_radar_feeds()
RADAR_CACHE['last_fetch'] = time.time()
return jsonify({'status': 'ok', 'total': len(RADAR_CACHE['items'])})
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False) app.run(host='0.0.0.0', port=5000, debug=False)

316
css/radar.css Normal file
View file

@ -0,0 +1,316 @@
/* ─── RADAR: Live Intelligence Feed ─────────────── */
.radar-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 2rem 3rem;
}
/* ─── Controls Bar ─────────────────────────────── */
.radar-controls {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.radar-filters {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.radar-filter {
font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem;
letter-spacing: 1.5px;
padding: 0.4rem 0.8rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.35);
cursor: pointer;
transition: all 0.2s ease;
}
.radar-filter:hover {
background: rgba(255, 170, 0, 0.06);
border-color: rgba(255, 170, 0, 0.2);
color: rgba(255, 170, 0, 0.7);
}
.radar-filter.active {
background: rgba(255, 170, 0, 0.08);
border-color: rgba(255, 170, 0, 0.4);
color: rgba(255, 170, 0, 0.9);
}
.radar-actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
.radar-search {
font-family: 'JetBrains Mono', monospace;
font-size: 0.65rem;
letter-spacing: 1px;
padding: 0.4rem 0.8rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.7);
width: 200px;
outline: none;
transition: border-color 0.2s;
}
.radar-search:focus {
border-color: rgba(255, 170, 0, 0.4);
}
.radar-search::placeholder {
color: rgba(255, 255, 255, 0.2);
}
.radar-refresh {
font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem;
letter-spacing: 1px;
padding: 0.4rem 0.8rem;
background: rgba(255, 170, 0, 0.06);
border: 1px solid rgba(255, 170, 0, 0.2);
color: rgba(255, 170, 0, 0.7);
cursor: pointer;
transition: all 0.2s;
}
.radar-refresh:hover {
background: rgba(255, 170, 0, 0.12);
border-color: rgba(255, 170, 0, 0.5);
color: rgba(255, 170, 0, 1);
}
.radar-refresh.spinning {
animation: spin 0.8s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* ─── Stats Bar ────────────────────────────────── */
.radar-stats {
display: flex;
gap: 1.5rem;
margin-bottom: 1.5rem;
padding: 0.6rem 1rem;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.05);
flex-wrap: wrap;
}
.radar-stat {
font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem;
letter-spacing: 1px;
color: rgba(255, 255, 255, 0.4);
}
.stat-label {
color: rgba(255, 255, 255, 0.2);
}
.stat-auto {
color: rgba(0, 204, 68, 0.7);
}
.radar-live {
display: flex;
align-items: center;
gap: 0.4rem;
color: rgba(0, 204, 68, 0.8);
margin-left: auto;
}
.live-dot {
width: 6px;
height: 6px;
background: #00cc44;
border-radius: 50%;
animation: pulse-dot 1.5s ease infinite;
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; box-shadow: 0 0 4px #00cc44; }
50% { opacity: 0.4; box-shadow: 0 0 1px #00cc44; }
}
/* ─── Feed Items ───────────────────────────────── */
.radar-feed {
display: flex;
flex-direction: column;
gap: 0;
}
.radar-loading {
font-family: 'JetBrains Mono', monospace;
font-size: 0.7rem;
color: rgba(255, 170, 0, 0.5);
text-align: center;
padding: 3rem;
letter-spacing: 2px;
animation: pulse-text 1.5s ease infinite;
}
@keyframes pulse-text {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.radar-item {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 0.8rem 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
transition: background 0.15s;
}
.radar-item:hover {
background: rgba(255, 255, 255, 0.02);
}
.radar-item-time {
font-family: 'JetBrains Mono', monospace;
font-size: 0.55rem;
color: rgba(255, 255, 255, 0.18);
min-width: 55px;
letter-spacing: 0.5px;
padding-top: 0.15rem;
flex-shrink: 0;
}
.radar-item-source {
font-family: 'JetBrains Mono', monospace;
font-size: 0.5rem;
letter-spacing: 1.5px;
padding: 0.15rem 0.5rem;
min-width: 90px;
text-align: center;
flex-shrink: 0;
border: 1px solid;
}
.source-hackernews {
color: #ff6600;
border-color: rgba(255, 102, 0, 0.3);
background: rgba(255, 102, 0, 0.05);
}
.source-reddit_technology,
.source-reddit_programming,
.source-reddit_netsec {
color: #ff4500;
border-color: rgba(255, 69, 0, 0.3);
background: rgba(255, 69, 0, 0.05);
}
.source-lobsters {
color: #ac130d;
border-color: rgba(172, 19, 13, 0.3);
background: rgba(172, 19, 13, 0.05);
}
.radar-item-content {
flex: 1;
min-width: 0;
}
.radar-item-title {
font-family: 'JetBrains Mono', monospace;
font-size: 0.7rem;
color: rgba(255, 255, 255, 0.75);
text-decoration: none;
display: block;
line-height: 1.4;
transition: color 0.15s;
}
.radar-item-title:hover {
color: rgba(255, 170, 0, 0.9);
}
.radar-item-meta {
display: flex;
gap: 1rem;
margin-top: 0.3rem;
}
.radar-item-domain {
font-family: 'JetBrains Mono', monospace;
font-size: 0.5rem;
color: rgba(255, 255, 255, 0.15);
letter-spacing: 0.5px;
}
.radar-item-comments {
font-family: 'JetBrains Mono', monospace;
font-size: 0.5rem;
color: rgba(255, 170, 0, 0.35);
text-decoration: none;
letter-spacing: 0.5px;
}
.radar-item-comments:hover {
color: rgba(255, 170, 0, 0.7);
}
.radar-empty {
font-family: 'JetBrains Mono', monospace;
font-size: 0.65rem;
color: rgba(255, 255, 255, 0.2);
text-align: center;
padding: 3rem;
letter-spacing: 2px;
}
/* ─── Responsive ───────────────────────────────── */
@media (max-width: 768px) {
.radar-controls {
flex-direction: column;
}
.radar-actions {
width: 100%;
}
.radar-search {
flex: 1;
}
.radar-item {
flex-wrap: wrap;
gap: 0.4rem;
}
.radar-item-time {
min-width: auto;
}
.radar-item-source {
min-width: auto;
}
.radar-stats {
gap: 0.8rem;
}
}
@media (max-width: 480px) {
.radar-container {
padding: 0 1rem 2rem;
}
.radar-filters {
gap: 0.3rem;
}
.radar-filter {
font-size: 0.5rem;
padding: 0.3rem 0.5rem;
}
}

169
js/radar.js Normal file
View file

@ -0,0 +1,169 @@
/* ─── RADAR: Live Intelligence Feed ─────────────── */
(function() {
'use strict';
const API = '/api/radar';
const REFRESH_INTERVAL = 15 * 60 * 1000; // 15 minutes
let currentSource = 'all';
let allItems = [];
let refreshTimer = null;
// ─── Time Ago ──────────────────────────────────
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';
}
// ─── Extract Domain ────────────────────────────
function extractDomain(url) {
try {
const u = new URL(url);
return u.hostname.replace('www.', '');
} catch(e) {
return '';
}
}
// ─── Escape HTML ───────────────────────────────
function esc(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
// ─── Render Feed ───────────────────────────────
function renderFeed(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(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>';
html += ' </div>';
html += '</div>';
});
feed.innerHTML = html;
}
// ─── Filter & Search ───────────────────────────
function applyFilters() {
const q = document.getElementById('radarSearch').value.trim().toLowerCase();
let filtered = allItems;
if (currentSource !== 'all') {
filtered = filtered.filter(i => i.source_id === currentSource);
}
if (q) {
filtered = filtered.filter(i =>
(i.title || '').toLowerCase().includes(q) ||
(i.summary || '').toLowerCase().includes(q)
);
}
document.getElementById('statTotal').textContent = filtered.length;
renderFeed(filtered);
}
// ─── Fetch Data ────────────────────────────────
async function fetchRadar(forceRefresh) {
const feed = document.getElementById('radarFeed');
feed.innerHTML = '<div class="radar-loading">SCANNING FREQUENCIES...</div>';
try {
if (forceRefresh) {
await fetch(API + '/refresh', { method: 'POST' });
}
const res = await fetch(API);
const data = await res.json();
allItems = data.items || [];
document.getElementById('statTotal').textContent = allItems.length;
// Format last updated
if (data.last_updated) {
const d = new Date(data.last_updated);
const pad = n => String(n).padStart(2, '0');
document.getElementById('statUpdated').textContent =
pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()) + ' UTC';
}
applyFilters();
} catch(err) {
console.error('RADAR fetch error:', err);
feed.innerHTML = '<div class="radar-empty">⚠ SIGNAL LOST — UNABLE TO REACH FEED API</div>';
}
}
// ─── Event Listeners ───────────────────────────
function init() {
// Source filters
document.querySelectorAll('.radar-filter').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
document.querySelectorAll('.radar-filter').forEach(b => b.classList.remove('active'));
this.classList.add('active');
currentSource = this.dataset.source;
applyFilters();
});
});
// Search
let searchTimeout;
document.getElementById('radarSearch').addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(applyFilters, 200);
});
// Refresh button
document.getElementById('radarRefresh').addEventListener('click', function(e) {
e.preventDefault();
this.classList.add('spinning');
fetchRadar(true).then(() => {
setTimeout(() => this.classList.remove('spinning'), 500);
});
});
// Initial fetch
fetchRadar(false);
// Auto-refresh every 15 min
refreshTimer = setInterval(() => fetchRadar(false), REFRESH_INTERVAL);
}
// ─── Boot ──────────────────────────────────────
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View file

@ -8,6 +8,8 @@
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/section.css"> <link rel="stylesheet" href="/css/section.css">
<link rel="stylesheet" href="/css/radar.css?v=20260404">
<style>body{background:#0a0a0a;}</style>
</head> </head>
<body> <body>
<div class="scanline-overlay"></div> <div class="scanline-overlay"></div>
@ -37,19 +39,38 @@
</div> </div>
<section class="section-header" style="padding-top: calc(var(--nav-height) + 1.5rem);"> <section class="section-header" style="padding-top: calc(var(--nav-height) + 1.5rem);">
<div class="section-header-label">TRANSMISSIONS // OUTBOUND SIGNALS</div> <div class="section-header-label">TRANSMISSIONS // INCOMING SIGNALS</div>
<h1 class="section-header-title">RADAR</h1> <h1 class="section-header-title">RADAR</h1>
<p class="section-header-sub">&gt; Latest cutting-edge tools, tech, and developments detected on the wire.</p> <p class="section-header-sub">&gt; Live intelligence feed — intercepted from Hacker News, Reddit, and Lobste.rs</p>
</section> </section>
<section class="subpage-content"> <div class="radar-container">
<div class="subpage-placeholder"> <div class="radar-controls">
<div class="placeholder-icon"></div> <div class="radar-filters">
<div class="placeholder-status">UNDER CONSTRUCTION</div> <button class="radar-filter active" data-source="all">ALL FEEDS</button>
<div class="placeholder-text">This section is being prepared. Content deployment imminent.</div> <button class="radar-filter" data-source="hackernews">HACKER NEWS</button>
<div class="placeholder-classification">CLASSIFICATION: PENDING // STATUS: STANDBY</div> <button class="radar-filter" data-source="reddit_technology">R/TECHNOLOGY</button>
<button class="radar-filter" data-source="reddit_programming">R/PROGRAMMING</button>
<button class="radar-filter" data-source="reddit_netsec">R/NETSEC</button>
<button class="radar-filter" data-source="lobsters">LOBSTERS</button>
</div>
<div class="radar-actions">
<input type="text" class="radar-search" placeholder="SEARCH FEED..." id="radarSearch">
<button class="radar-refresh" id="radarRefresh" title="Force refresh">↻ REFRESH</button>
</div>
</div> </div>
</section>
<div class="radar-stats">
<span class="radar-stat"><span class="stat-label">INTERCEPTS:</span> <span id="statTotal"></span></span>
<span class="radar-stat"><span class="stat-label">LAST SYNC:</span> <span id="statUpdated"></span></span>
<span class="radar-stat"><span class="stat-label">AUTO-REFRESH:</span> <span class="stat-auto">15 MIN</span></span>
<span class="radar-stat radar-live"><span class="live-dot"></span> LIVE</span>
</div>
<div class="radar-feed" id="radarFeed">
<div class="radar-loading">SCANNING FREQUENCIES...</div>
</div>
</div>
<footer class="footer"> <footer class="footer">
<div class="footer-container"> <div class="footer-container">
@ -65,5 +86,6 @@
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
<script src="/js/radar.js?v=20260404"></script>
</body> </body>
</html> </html>