feat: RADAR live tech news feed — HN, Reddit, Lobsters
This commit is contained in:
parent
82b71fc9a0
commit
c66e122786
4 changed files with 626 additions and 11 deletions
110
api/app.py
110
api/app.py
|
|
@ -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
316
css/radar.css
Normal 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
169
js/radar.js
Normal 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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -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">> Latest cutting-edge tools, tech, and developments detected on the wire.</p>
|
<p class="section-header-sub">> 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>
|
||||||
Loading…
Add table
Reference in a new issue