From 8bbc9058f2343f092c6c4de7dbff2268263b48be Mon Sep 17 00:00:00 2001 From: R4V3N Date: Sun, 15 Mar 2026 19:49:18 +0100 Subject: [PATCH 1/3] Add clickable article links to Live News Ticker RSS and GDELT ticker items now open the source article in a new tab when clicked. Telegram/WHO items without URLs remain non-clickable. Changes: - Extract from RSS feed items in inject.mjs - Carry url field through fetchAllNews() and buildNewsFeed() - Add data-url attribute and pointer cursor to clickable ticker cards - Add delegated click listener to open articles in new tab --- dashboard/inject.mjs | 16 +++++++++------- dashboard/public/jarvis.html | 9 ++++++++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/dashboard/inject.mjs b/dashboard/inject.mjs index b573c13..331171c 100644 --- a/dashboard/inject.mjs +++ b/dashboard/inject.mjs @@ -69,8 +69,9 @@ async function fetchRSS(url, source) { while ((match = itemRegex.exec(xml)) !== null) { const block = match[1]; const title = (block.match(/(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/title>/)?.[1] || '').trim(); + const link = (block.match(/<link>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/link>/)?.[1] || '').trim(); const pubDate = block.match(/<pubDate>(.*?)<\/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) { @@ -107,6 +108,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 @@ -409,17 +411,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: a.url }); } } diff --git a/dashboard/public/jarvis.html b/dashboard/public/jarvis.html index d12e2ed..94e5db3 100644 --- a/dashboard/public/jarvis.html +++ b/dashboard/public/jarvis.html @@ -182,6 +182,7 @@ 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: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} @@ -815,7 +816,8 @@ function renderLower(){ const tickerCards = feed.map(n => { const sc = srcClass(n.source); const age = n.timestamp ? getAge(n.timestamp) : ''; - return `<div class="tk-card ${n.urgent?'urgent':''}"><span class="tk-src ${sc}">${(n.source||'NEWS').substring(0,12)}</span><span class="tk-time">${age}</span><div class="tk-head">${cleanText(n.headline||'')}</div></div>`; + const urlAttr = n.url ? ` data-url="${String(n.url).replace(/&/g,'&').replace(/"/g,'"')}"` : ''; + return `<div class="tk-card ${n.urgent?'urgent':''} ${n.url?'clickable':''}"${urlAttr}><span class="tk-src ${sc}">${(n.source||'NEWS').substring(0,12)}</span><span class="tk-time">${age}</span><div class="tk-head">${cleanText(n.headline||'')}</div></div>`; }).join(''); const tickerDuration = Math.max(20, feed.length * 2.5); @@ -1047,6 +1049,11 @@ 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) window.open(card.dataset.url,'_blank','noopener'); + }); } document.addEventListener('DOMContentLoaded', () => { From 6b4deea96b56acd3228f6c5ff8f808bc0b236a6e Mon Sep 17 00:00:00 2001 From: R4V3N <o.joisten@live.se> Date: Sun, 15 Mar 2026 22:24:56 +0100 Subject: [PATCH 2/3] Add visual link icon to clickable ticker items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shows a ↗ arrow on ticker cards that have article URLs, highlighted cyan on hover to indicate clickability. --- dashboard/public/jarvis.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dashboard/public/jarvis.html b/dashboard/public/jarvis.html index 94e5db3..ff2bf47 100644 --- a/dashboard/public/jarvis.html +++ b/dashboard/public/jarvis.html @@ -183,6 +183,9 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s @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} @@ -817,7 +820,7 @@ function renderLower(){ const sc = srcClass(n.source); const age = n.timestamp ? getAge(n.timestamp) : ''; const urlAttr = n.url ? ` data-url="${String(n.url).replace(/&/g,'&').replace(/"/g,'"')}"` : ''; - return `<div class="tk-card ${n.urgent?'urgent':''} ${n.url?'clickable':''}"${urlAttr}><span class="tk-src ${sc}">${(n.source||'NEWS').substring(0,12)}</span><span class="tk-time">${age}</span><div class="tk-head">${cleanText(n.headline||'')}</div></div>`; + return `<div class="tk-card ${n.urgent?'urgent':''} ${n.url?'clickable':''}"${urlAttr}><span class="tk-src ${sc}">${(n.source||'NEWS').substring(0,12)}</span><span class="tk-time">${age}</span><div class="tk-head">${cleanText(n.headline||'')}</div>${n.url?'<span class="tk-link">↗</span>':''}</div>`; }).join(''); const tickerDuration = Math.max(20, feed.length * 2.5); From ee552dddef37de4ece673408e1dae8544550e09b Mon Sep 17 00:00:00 2001 From: calesthio <celesthioailabs@gmail.com> Date: Sun, 15 Mar 2026 17:27:07 -0700 Subject: [PATCH 3/3] fix: restrict ticker article links to http(s) --- dashboard/inject.mjs | 14 ++++++++++++-- dashboard/public/jarvis.html | 6 +++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/dashboard/inject.mjs b/dashboard/inject.mjs index 331171c..3f7bd4f 100644 --- a/dashboard/inject.mjs +++ b/dashboard/inject.mjs @@ -58,6 +58,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 { @@ -69,7 +79,7 @@ async function fetchRSS(url, source) { while ((match = itemRegex.exec(xml)) !== null) { const block = match[1]; const title = (block.match(/<title>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/title>/)?.[1] || '').trim(); - const link = (block.match(/<link>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/link>/)?.[1] || '').trim(); + const link = sanitizeExternalUrl((block.match(/<link>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/link>/)?.[1] || '').trim()); const pubDate = block.match(/<pubDate>(.*?)<\/pubDate>/)?.[1] || ''; if (title && title !== source) items.push({ title, date: pubDate, source, url: link || undefined }); } @@ -421,7 +431,7 @@ function buildNewsFeed(rssNews, gdeltData, tgUrgent, tgTop) { const geo = geoTagText(a.title); feed.push({ headline: a.title.substring(0, 100), source: 'GDELT', type: 'gdelt', - timestamp: new Date().toISOString(), region: geo?.region || 'Global', urgent: false, url: a.url + 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 ff2bf47..6af58f0 100644 --- a/dashboard/public/jarvis.html +++ b/dashboard/public/jarvis.html @@ -958,6 +958,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(){ @@ -1055,7 +1056,10 @@ function init(){ // Open article links from ticker cards document.addEventListener('click',e=>{ const card=e.target.closest('.tk-card[data-url]'); - if(card) window.open(card.dataset.url,'_blank','noopener'); + if(card){ + const url=safeExternalUrl(card.dataset.url); + if(url) window.open(url,'_blank','noopener'); + } }); }