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
|
||||
"""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 concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
|
|
@ -978,5 +978,113 @@ def awesomelist_search():
|
|||
if len(results) >= limit:
|
||||
break
|
||||
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__':
|
||||
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 rel="stylesheet" href="/css/style.css">
|
||||
<link rel="stylesheet" href="/css/section.css">
|
||||
<link rel="stylesheet" href="/css/radar.css?v=20260404">
|
||||
<style>body{background:#0a0a0a;}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="scanline-overlay"></div>
|
||||
|
|
@ -37,19 +39,38 @@
|
|||
</div>
|
||||
|
||||
<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>
|
||||
<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 class="subpage-content">
|
||||
<div class="subpage-placeholder">
|
||||
<div class="placeholder-icon">◎</div>
|
||||
<div class="placeholder-status">UNDER CONSTRUCTION</div>
|
||||
<div class="placeholder-text">This section is being prepared. Content deployment imminent.</div>
|
||||
<div class="placeholder-classification">CLASSIFICATION: PENDING // STATUS: STANDBY</div>
|
||||
<div class="radar-container">
|
||||
<div class="radar-controls">
|
||||
<div class="radar-filters">
|
||||
<button class="radar-filter active" data-source="all">ALL FEEDS</button>
|
||||
<button class="radar-filter" data-source="hackernews">HACKER NEWS</button>
|
||||
<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>
|
||||
</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">
|
||||
<div class="footer-container">
|
||||
|
|
@ -65,5 +86,6 @@
|
|||
|
||||
<script src="/js/nav.js"></script>
|
||||
<script src="/js/clock.js"></script>
|
||||
<script src="/js/radar.js?v=20260404"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Reference in a new issue