diff --git a/dashboard/inject.mjs b/dashboard/inject.mjs
index 38464ac..bdad625 100644
--- a/dashboard/inject.mjs
+++ b/dashboard/inject.mjs
@@ -89,6 +89,16 @@ function geoTagText(text) {
return null;
}
+function sanitizeExternalUrl(raw) {
+ if (!raw) return undefined;
+ try {
+ const url = new URL(raw);
+ return url.protocol === 'http:' || url.protocol === 'https:' ? url.toString() : undefined;
+ } catch {
+ return undefined;
+ }
+}
+
// === RSS Fetching ===
async function fetchRSS(url, source) {
try {
@@ -100,8 +110,9 @@ async function fetchRSS(url, source) {
while ((match = itemRegex.exec(xml)) !== null) {
const block = match[1];
const title = (block.match(/
(?:)?<\/title>/)?.[1] || '').trim();
+ const link = sanitizeExternalUrl((block.match(/(?:)?<\/link>/)?.[1] || '').trim());
const pubDate = block.match(/(.*?)<\/pubDate>/)?.[1] || '';
- if (title && title !== source) items.push({ title, date: pubDate, source });
+ if (title && title !== source) items.push({ title, date: pubDate, source, url: link || undefined });
}
return items;
} catch (e) {
@@ -142,6 +153,7 @@ export async function fetchAllNews() {
title: item.title.substring(0, 100),
source: item.source,
date: item.date,
+ url: item.url,
lat: geo.lat + (Math.random() - 0.5) * 2,
lon: geo.lon + (Math.random() - 0.5) * 2,
region: geo.region
@@ -460,17 +472,17 @@ function buildNewsFeed(rssNews, gdeltData, tgUrgent, tgTop) {
for (const n of rssNews) {
feed.push({
headline: n.title, source: n.source, type: 'rss',
- timestamp: n.date, region: n.region, urgent: false
+ timestamp: n.date, region: n.region, urgent: false, url: n.url
});
}
// GDELT top articles
- for (const title of (gdeltData.allArticles || []).slice(0, 10).map(a => a.title)) {
- if (title) {
- const geo = geoTagText(title);
+ for (const a of (gdeltData.allArticles || []).slice(0, 10)) {
+ if (a.title) {
+ const geo = geoTagText(a.title);
feed.push({
- headline: title.substring(0, 100), source: 'GDELT', type: 'gdelt',
- timestamp: new Date().toISOString(), region: geo?.region || 'Global', urgent: false
+ headline: a.title.substring(0, 100), source: 'GDELT', type: 'gdelt',
+ timestamp: new Date().toISOString(), region: geo?.region || 'Global', urgent: false, url: sanitizeExternalUrl(a.url)
});
}
}
diff --git a/dashboard/public/jarvis.html b/dashboard/public/jarvis.html
index 9912b21..7acfca3 100644
--- a/dashboard/public/jarvis.html
+++ b/dashboard/public/jarvis.html
@@ -191,6 +191,10 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
.ticker-wrap:hover .ticker-track{animation-play-state:paused}
@keyframes tickerScroll{0%{transform:translateY(0)}100%{transform:translateY(-50%)}}
.tk-card{padding:8px 10px;border-bottom:1px solid rgba(255,255,255,0.03);cursor:default;transition:background 0.2s}
+.tk-card.clickable{cursor:pointer}
+.tk-card .tk-link{display:none;margin-left:auto;font-size:10px;color:var(--dim);transition:color 0.2s}
+.tk-card.clickable .tk-link{display:inline-flex;align-items:center}
+.tk-card.clickable:hover .tk-link{color:var(--accent)}
.tk-card:hover{background:rgba(100,240,200,0.04)}
.tk-card.urgent{border-left:2px solid var(--danger)}
.tk-src{font-family:var(--mono);font-size:8px;letter-spacing:0.08em;text-transform:uppercase;padding:1px 5px;border:1px solid;display:inline-block;margin-right:4px}
@@ -953,7 +957,8 @@ function renderLower(){
const tickerCards = feed.map(n => {
const sc = srcClass(n.source);
const age = n.timestamp ? getAge(n.timestamp) : '';
- return `${(n.source||'NEWS').substring(0,12)}${age}${cleanText(n.headline||'')}
`;
+ const urlAttr = n.url ? ` data-url="${String(n.url).replace(/&/g,'&').replace(/"/g,'"')}"` : '';
+ return `${(n.source||'NEWS').substring(0,12)}${age}${cleanText(n.headline||'')}
${n.url?'
↗':''}
`;
}).join('');
const tickerDuration = Math.max(20, feed.length * 2.5);
@@ -1091,6 +1096,7 @@ function renderRight(){
// === HELPERS ===
function getAge(d){const ms=Date.now()-new Date(d).getTime();const h=Math.floor(ms/3600000);if(h<1)return 'just now';if(h<24)return h+'h ago';return Math.floor(h/24)+'d ago'}
function cleanText(t){return t.replace(/'/g,"'").replace(/!/g,"!").replace(/&/g,"&").replace(/<[^>]+>/g,'')}
+function safeExternalUrl(raw){try{const u=new URL(raw,location.href);return u.protocol==='http:'||u.protocol==='https:'?u.toString():null}catch{return null}}
// === BOOT SEQUENCE ===
function runBoot(){
@@ -1185,6 +1191,14 @@ function init(){
document.getElementById('mapContainer').addEventListener('click',e=>{
if(!e.target.closest('.map-popup')) closePopup();
});
+ // Open article links from ticker cards
+ document.addEventListener('click',e=>{
+ const card=e.target.closest('.tk-card[data-url]');
+ if(card){
+ const url=safeExternalUrl(card.dataset.url);
+ if(url) window.open(url,'_blank','noopener');
+ }
+ });
}
document.addEventListener('DOMContentLoaded', () => {