Skip to main content

◆ Sync Dashboard

Page Sections
Drag ☰ to reorder sections. Changes persist.
☰Market Ticker
☰Global Clocks / FX
☰Threat Level
☰Hot News
☰Fed Watch / Commodities
☰Debt Clocks
☰Word of the Day
☰Quote of the Day
☰On This Day
☰Weather
☰Countdown Timers
☰Energy / Infrastructure
☰Earth Cams
☰Session Stats
☰Health Bar
☰Stats Bar
☰Status Bar
☰Filter / Search Bar
🔥 HOT — Last 10 Minutes 0 ▼
⚠ THREAT --
⏱ FOMC -- | -- | | 📈 Commodities CME FedWatch ↗
⚠ Debt Clocks
📚 Word -- Loading...
💭 Quote --
📅 On This Day ▼
☔ Weather + City
⏳ Countdowns
⚡ Energy ⚡ EIA ⚡ ERCOT (TX) ⚡ CAISO (CA) ⚡ ISO-NE ⚡ PJM OUTAGES: 🔴 Duke Energy 🔴 Xcel Energy 🔴 Core Electric 🔴 FPL/NextEra 🔴 Dominion 🔴 Southern Co 🔴 PG&E 🔴 ConEd 🔴 AEP 🔴 PowerOutage.us 🔴 Downdetector 📡 Internet 🏜 Drought 🌬 AQI
📷 Earth Cams 🇬🇸 Times Square 🇬🇧 Abbey Road 🌳 Explore.org 🌎 Skyline Webcams 🌌 Windy Cams ▶ YouTube Live 📷 Opentopia 🌐 Insecam
Ready 0 sources 0 items Never synced ⚠ Offline
↻
Syncing Sources
Preparing...
?
Cat:
Region:
Status:AllOKHas ContentErrorsEmpty

📝 Intel Notes

Keyboard Shortcuts

Sync allS
Focus search/
Toggle themeT
Add sourceN
This help?
CloseEsc
Toggle WordW
Toggle HistoryH
Jump to sourceG
Cards/TL/Saved/List1-4
Sources/Reports5-6
Vid/Social/GEOINT7-9

Add Data Source

RSS, Atom, or any URL.

Manage Sources

Region Manager

Import Sources

Confirm

Test RSS Feed URL

Tests via allorigins proxy, rss2json fallback, and direct fetch. Shows parsed items if successful.

Custom Keyword Tracker

Add keywords to scan across ALL sources. Matched items appear in the SBX Tracker tab alongside the built-in SBX/missile defense keywords.

Add Countdown

🔒 Premium API Keys

Enter your API keys below to unlock premium news feeds. All keys are stored locally in your browser only — never sent to any third party.
Tip: Most services offer free tiers. Click the registration links to get your keys.
🔒 Authenticated RSS Feeds

If your subscription provides a private RSS URL (with a token in the URL), add it as a regular source via "+ Source". It will be fetched through the normal proxy chain.

For Basic Auth feeds, use the format: https://user:pass@example.com/feed.rss

Add Weather Location

Uses Open-Meteo geocoding. Up to 6 locations supported.

🕓 Manage Clocks

Market hours are optional. Use decimal (9.5 = 9:30 AM). Leave defaults if no exchange.
`; const blob=new Blob([html],{type:'text/html'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='threat-assessment-'+new Date().toISOString().slice(0,10)+'.html';a.click();URL.revokeObjectURL(a.href); toast('Threat assessment exported','ok'); } function generateSentimentReport(){ const{items,days}=collectReportItems(); const now=new Date(); // Compute detailed sentiment analytics const sentBuckets={};const daySent={};const catSent={};const regSent={}; items.forEach(it=>{ const label=it.sentiment; sentBuckets[label]=(sentBuckets[label]||0)+1; const dayKey=it.pubDate.toISOString().slice(0,10); if(!daySent[dayKey]) daySent[dayKey]={pos:0,neg:0,neut:0,total:0}; daySent[dayKey].total++; if(it.sentimentScore>=1) daySent[dayKey].pos++; else if(it.sentimentScore<=-1) daySent[dayKey].neg++; else daySent[dayKey].neut++; if(!catSent[it.category]) catSent[it.category]={pos:0,neg:0,neut:0,total:0}; catSent[it.category].total++; if(it.sentimentScore>=1) catSent[it.category].pos++; else if(it.sentimentScore<=-1) catSent[it.category].neg++; else catSent[it.category].neut++; if(!regSent[it.region]) regSent[it.region]={pos:0,neg:0,neut:0,total:0}; regSent[it.region].total++; if(it.sentimentScore>=1) regSent[it.region].pos++; else if(it.sentimentScore<=-1) regSent[it.region].neg++; else regSent[it.region].neut++; }); const totalPos=items.filter(i=>i.sentimentScore>=1).length; const totalNeg=items.filter(i=>i.sentimentScore<=-1).length; const totalNeut=items.length-totalPos-totalNeg; const overallRatio=items.length?(totalPos-totalNeg)/items.length:0; let html=`Sentiment Analysis Report - ${now.toLocaleDateString()}

📈 Sentiment Analysis Report

Generated: ${now.toLocaleString()} | Period: ${days} days | Items analyzed: ${items.length}

`; // Overall sentiment summary html+=`

Overall Sentiment

${totalPos}
Positive
${items.length?Math.round(totalPos/items.length*100):0}%
${totalNeut}
Neutral
${items.length?Math.round(totalNeut/items.length*100):0}%
${totalNeg}
Negative
${items.length?Math.round(totalNeg/items.length*100):0}%
${overallRatio>=0?'+':''}${(overallRatio*100).toFixed(1)}%
Net Sentiment
`; // Daily trend const sortedDays=Object.keys(daySent).sort(); if(sortedDays.length>1){ html+=`

Daily Sentiment Trend

`; sortedDays.forEach(d=>{ const s=daySent[d];const maxW=200; const pw=s.total?Math.round(s.pos/s.total*maxW):0; const nw=s.total?Math.round(s.neg/s.total*maxW):0; const uw=maxW-pw-nw; html+=``; }); html+='
DateTotalPositiveNegativeNeutralDistribution
${d}${s.total}${s.pos}${s.neg}${s.neut}
'; } // Category breakdown html+=`

Sentiment by Category

`; Object.entries(catSent).sort((a,b)=>b[1].total-a[1].total).forEach(([cat,s])=>{ const net=s.total?(s.pos-s.neg)/s.total:0; html+=``; }); html+='
CategoryItemsPositiveNegativeNet Ratio
${esc(cat)}${s.total}${s.pos}${s.neg}${net>=0?'+':''}${(net*100).toFixed(1)}%
'; // Region breakdown html+=`

Sentiment by Region

`; Object.entries(regSent).sort((a,b)=>b[1].total-a[1].total).forEach(([reg,s])=>{ const net=s.total?(s.pos-s.neg)/s.total:0; html+=``; }); html+='
RegionItemsPositiveNegativeNet Ratio
${esc(reg)}${s.total}${s.pos}${s.neg}${net>=0?'+':''}${(net*100).toFixed(1)}%
'; // Most negative items const negItems=items.filter(i=>i.sentimentScore<=-2).sort((a,b)=>a.sentimentScore-b.sentimentScore).slice(0,20); if(negItems.length){ html+=`

Most Negative Headlines (${negItems.length})

`; negItems.forEach(it=>{ html+=``; }); html+='
ScoreSourceHeadlineDate
${it.sentimentScore}${esc(it.sourceName)}${esc(it.title)}${it.pubDate.toLocaleDateString()}
'; } html+=`
Generated by Sync Dashboard v9 Sentiment Engine | ${items.length} items analyzed across ${Object.keys(catSent).length} categories
`; const blob=new Blob([html],{type:'text/html'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='sentiment-report-'+new Date().toISOString().slice(0,10)+'.html';a.click();URL.revokeObjectURL(a.href); toast('Sentiment report exported','ok'); } function generateEntityReport(){ const{items}=collectReportItems(); const now=new Date(); const entityStats={countries:{},orgs:{}}; items.forEach(it=>{ const ent=typeof extractEntities==='function'?extractEntities((it.title||'')+' '+(it.description||'')):{countries:[],orgs:[]}; ent.countries.forEach(c=>{ if(!entityStats.countries[c]) entityStats.countries[c]={count:0,sources:new Set(),sentiment:0,items:[]}; entityStats.countries[c].count++;entityStats.countries[c].sources.add(it.sourceName);entityStats.countries[c].sentiment+=it.sentimentScore; if(entityStats.countries[c].items.length<3) entityStats.countries[c].items.push(it); }); ent.orgs.forEach(o=>{ if(!entityStats.orgs[o]) entityStats.orgs[o]={count:0,sources:new Set(),sentiment:0,items:[]}; entityStats.orgs[o].count++;entityStats.orgs[o].sources.add(it.sourceName);entityStats.orgs[o].sentiment+=it.sentimentScore; if(entityStats.orgs[o].items.length<3) entityStats.orgs[o].items.push(it); }); }); let html=`Entity Intelligence Report - ${now.toLocaleDateString()}

🌐 Entity Intelligence Report

Generated: ${now.toLocaleString()} | Items scanned: ${items.length}

`; // Countries const sortedCountries=Object.entries(entityStats.countries).sort((a,b)=>b[1].count-a[1].count); html+=`

Country/Region Mentions (${sortedCountries.length} detected)

`; sortedCountries.slice(0,30).forEach(([name,data])=>{ const avgSent=data.count?(data.sentiment/data.count).toFixed(2):'0'; const color=parseFloat(avgSent)>=0.5?'#059669':parseFloat(avgSent)<=-0.5?'#e63946':'#666'; const samples=data.items.map(i=>''+esc(i.title.substring(0,60))+'').join('
'); html+=``; }); html+='
EntityMentionsSourcesAvg SentimentSample Headlines
${esc(name)}${data.count}${data.sources.size}${avgSent}${samples}
'; // Organizations const sortedOrgs=Object.entries(entityStats.orgs).sort((a,b)=>b[1].count-a[1].count); html+=`

Organization Mentions (${sortedOrgs.length} detected)

`; sortedOrgs.slice(0,30).forEach(([name,data])=>{ const avgSent=data.count?(data.sentiment/data.count).toFixed(2):'0'; const color=parseFloat(avgSent)>=0.5?'#059669':parseFloat(avgSent)<=-0.5?'#e63946':'#666'; html+=``; }); html+='
EntityMentionsSourcesAvg Sentiment
${esc(name)}${data.count}${data.sources.size}${avgSent}
'; // Co-occurrence matrix (top 10 countries) const topCountries=sortedCountries.slice(0,10).map(([n])=>n); if(topCountries.length>=3){ const cooccur={}; items.forEach(it=>{ const ent=typeof extractEntities==='function'?extractEntities((it.title||'')+' '+(it.description||'')):{countries:[]}; const cs=ent.countries.filter(c=>topCountries.includes(c)); for(let i=0;ic>=2).sort((a,b)=>b[1]-a[1]).slice(0,15); if(pairs.length){ html+=`

Country Co-Occurrence (appearing in same article)

`; pairs.forEach(([pair,count])=>{html+=``}); html+='
PairCo-mentions
${esc(pair)}${count}
'; } } html+=`
Generated by Sync Dashboard v9 Entity Extraction Engine | NER: ${Object.keys(entityStats.countries).length} countries, ${Object.keys(entityStats.orgs).length} organizations detected
`; const blob=new Blob([html],{type:'text/html'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='entity-report-'+new Date().toISOString().slice(0,10)+'.html';a.click();URL.revokeObjectURL(a.href); toast('Entity report exported','ok'); } function generateSourceAudit(){ const now=new Date(); const enabled=state.sources.filter(s=>s.enabled); const errored=state.sources.filter(s=>s.enabled&&s.errorState); const empty=state.sources.filter(s=>s.enabled&&s.lastFetched&&!(state.cache[s.id]||[]).length&&!s.errorState); const never=state.sources.filter(s=>s.enabled&&!s.lastFetched); let html=`Source Audit Report - ${now.toLocaleDateString()}

📋 Source Audit Report

Generated: ${now.toLocaleString()} | Total sources: ${state.sources.length}

`; html+=`

Summary

${enabled.length}
Enabled
${errored.length}
Errored
${empty.length}
Empty
${never.length}
Never Synced
${state.sources.length-enabled.length}
Disabled
`; // Category coverage const catCov={}; state.sources.forEach(s=>{if(!catCov[s.category]) catCov[s.category]={total:0,enabled:0,items:0,errored:0};catCov[s.category].total++;if(s.enabled) catCov[s.category].enabled++;if(s.errorState) catCov[s.category].errored++;catCov[s.category].items+=(state.cache[s.id]||[]).length}); html+=`

Category Coverage

`; Object.entries(catCov).sort((a,b)=>b[1].total-a[1].total).forEach(([cat,data])=>{ const health=data.errored===0?'OK':data.errored>=data.enabled?'FAILING':'PARTIAL'; html+=``; }); html+='
CategorySourcesEnabledErrorsItemsHealth
${esc(cat)}${data.total}${data.enabled}${data.errored}${data.items}${health}
'; // Reliability scores if(state.sourceReliability&&Object.keys(state.sourceReliability).length){ html+=`

Source Reliability Ratings

`; state.sources.filter(s=>state.sourceReliability[s.id]).sort((a,b)=>(state.sourceReliability[a.id]?.score||0)-(state.sourceReliability[b.id]?.score||0)).forEach(s=>{ const r=state.sourceReliability[s.id]; const cls=r.score>=75?'badge-green':r.score>=50?'badge-amber':'badge-red'; html+=``; }); html+='
SourceCategoryScoreNotesLast Reviewed
${esc(s.icon)} ${esc(s.name)}${esc(s.category)}${r.score}/100${esc(r.notes||'')}${r.lastReviewed?new Date(r.lastReviewed).toLocaleDateString():'Never'}
'; } // Errored sources detail if(errored.length){ html+=`

Errored Sources (${errored.length})

`; errored.forEach(s=>{ html+=``; }); html+='
SourceCategoryErrorLast Attempt
${esc(s.icon)} ${esc(s.name)}${esc(s.category)}${esc(s.errorState||'Unknown')}${s.lastFetched?new Date(s.lastFetched).toLocaleString():'Never'}
'; } html+=`
Generated by Sync Dashboard v9 | ${state.sources.length} total sources | ${enabled.length} enabled | ${Object.values(state.cache).reduce((n,a)=>n+(a?.length||0),0)} total items cached
`; const blob=new Blob([html],{type:'text/html'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='source-audit-'+new Date().toISOString().slice(0,10)+'.html';a.click();URL.revokeObjectURL(a.href); toast('Source audit exported','ok'); } function renderMaritimePanel(){ const panel=document.getElementById('maritimePanel');panel.innerHTML=''; const h=document.createElement('h3');h.style.cssText='font-size:.9rem;margin-bottom:.5rem;color:var(--accent)';h.textContent='\u2693 Maritime & Infrastructure Dashboard';panel.appendChild(h); // Tracking tools const trackH=document.createElement('div');trackH.className='prem-section-head';trackH.textContent='\u{1F6F0} Vessel & Flight Tracking';panel.appendChild(trackH); const trackGrid=document.createElement('div');trackGrid.className='links-grid'; [{name:'MarineTraffic',url:'https://www.marinetraffic.com/',icon:'\u{1F6A2}',tag:'Ships'},{name:'VesselFinder',url:'https://www.vesselfinder.com/',icon:'\u{1F6A2}',tag:'Ships'},{name:'FlightRadar24',url:'https://www.flightradar24.com/',icon:'\u2708',tag:'Flights'},{name:'FlightAware Misery Map',url:'https://flightaware.com/miserymap/',icon:'\u2708',tag:'Delays'},{name:'Port of LA',url:'https://www.portoflosangeles.org/',icon:'\u2693',tag:'Port'},{name:'Port of Long Beach',url:'https://www.polb.com/',icon:'\u2693',tag:'Port'},{name:'Port of NY/NJ',url:'https://www.panynj.gov/port/en/index.html',icon:'\u2693',tag:'Port'},{name:'USCG HOMEPORT',url:'https://homeport.uscg.mil/',icon:'\u2693',tag:'USCG'},{name:'NOAA Ship Tracker',url:'https://www.omao.noaa.gov/learn/marine-operations/ships',icon:'\u{1F30A}',tag:'NOAA'},{name:'US Navy Ship Locator',url:'https://news.usni.org/category/fleet-tracker',icon:'\u2693',tag:'Navy'}].forEach(l=>{ const a=document.createElement('a');a.className='link-item';a.href=l.url;a.target='_blank';a.rel='noopener'; a.innerHTML=''+l.icon+''+esc(l.name)+''+esc(l.tag)+''; trackGrid.appendChild(a); }); panel.appendChild(trackGrid); // Infrastructure tools const infraH=document.createElement('div');infraH.className='prem-section-head';infraH.style.marginTop='.8rem';infraH.textContent='\u26A1 Energy & Infrastructure';panel.appendChild(infraH); const infraGrid=document.createElement('div');infraGrid.className='links-grid'; [{name:'Downdetector',url:'https://downdetector.com/',icon:'\u{1F534}',tag:'Outages'},{name:'US Drought Monitor',url:'https://droughtmonitor.unl.edu/',icon:'\u{1F3DC}',tag:'Drought'},{name:'AirNow (Air Quality)',url:'https://www.airnow.gov/',icon:'\u{1F32C}',tag:'AQI'},{name:'IQAir World AQI',url:'https://www.iqair.com/world-air-quality',icon:'\u{1F32C}',tag:'AQI'},{name:'TomTom Traffic',url:'https://www.tomtom.com/traffic-index/',icon:'\u{1F697}',tag:'Traffic'},{name:'Google Traffic (Maps)',url:'https://www.google.com/maps/@39.8,-98.5,5z/data=!5m1!1e1',icon:'\u{1F697}',tag:'Traffic'},{name:'EIA Dashboard',url:'https://www.eia.gov/dashboard/',icon:'\u26A1',tag:'Energy'},{name:'ISO New England',url:'https://www.iso-ne.com/',icon:'\u26A1',tag:'Grid'},{name:'ERCOT (Texas Grid)',url:'https://www.ercot.com/gridmktinfo/dashboards',icon:'\u26A1',tag:'Grid'},{name:'CAISO (CA Grid)',url:'http://www.caiso.com/TodaysOutlook/Pages/default.aspx',icon:'\u26A1',tag:'Grid'},{name:'Internet Outage Map',url:'https://downdetector.com/status/internet/',icon:'\u{1F4E1}',tag:'Internet'}].forEach(l=>{ const a=document.createElement('a');a.className='link-item';a.href=l.url;a.target='_blank';a.rel='noopener'; a.innerHTML=''+l.icon+''+esc(l.name)+''+esc(l.tag)+''; infraGrid.appendChild(a); }); panel.appendChild(infraGrid); // Earth Cams const camH=document.createElement('div');camH.className='prem-section-head';camH.style.marginTop='.8rem';camH.textContent='\u{1F4F7} Earth Cams & Live Views';panel.appendChild(camH); const camGrid=document.createElement('div');camGrid.className='links-grid'; [{name:'EarthCam - Times Square',url:'https://www.earthcam.com/usa/newyork/timessquare/',icon:'\u{1F4F7}',tag:'NYC'},{name:'EarthCam - Abbey Road',url:'https://www.earthcam.com/world/england/london/abbeyroad/',icon:'\u{1F4F7}',tag:'London'},{name:'Explore.org Live Cams',url:'https://explore.org/livecams',icon:'\u{1F4F7}',tag:'Nature'},{name:'Skyline Webcams',url:'https://www.skylinewebcams.com/',icon:'\u{1F4F7}',tag:'Global'},{name:'Windy Webcams',url:'https://www.windy.com/webcams',icon:'\u{1F4F7}',tag:'Global'},{name:'Jackson Hole Town Square',url:'https://www.see.cam/us/wy/jackson-hole',icon:'\u{1F4F7}',tag:'Wyoming'}].forEach(l=>{ const a=document.createElement('a');a.className='link-item';a.href=l.url;a.target='_blank';a.rel='noopener'; a.innerHTML=''+l.icon+''+esc(l.name)+''+esc(l.tag)+''; camGrid.appendChild(a); }); panel.appendChild(camGrid); // TV Guide const tvH=document.createElement('div');tvH.className='prem-section-head';tvH.style.marginTop='.8rem';tvH.textContent='\u{1F4FA} TV Guide & Programming';panel.appendChild(tvH); const tvGrid=document.createElement('div');tvGrid.className='links-grid'; [{name:'TV Guide',url:'https://www.tvguide.com/',icon:'\u{1F4FA}',tag:'Guide'},{name:'JustWatch',url:'https://www.justwatch.com/',icon:'\u{1F3AC}',tag:'Streaming'},{name:'Reelgood',url:'https://reelgood.com/',icon:'\u{1F3AC}',tag:'Streaming'},{name:'TV Passport',url:'https://www.tvpassport.com/',icon:'\u{1F4FA}',tag:'Listings'},{name:'Pluto TV',url:'https://pluto.tv/',icon:'\u{1F4FA}',tag:'Free'}].forEach(l=>{ const a=document.createElement('a');a.className='link-item';a.href=l.url;a.target='_blank';a.rel='noopener'; a.innerHTML=''+l.icon+''+esc(l.name)+''+esc(l.tag)+''; tvGrid.appendChild(a); }); panel.appendChild(tvGrid); // Maritime feed items const feedH=document.createElement('div');feedH.className='prem-section-head';feedH.style.marginTop='.8rem';feedH.textContent='\u{1F4F0} Maritime & Infrastructure News Feed';panel.appendChild(feedH); const mariSources=state.sources.filter(s=>s.enabled&&(s.category==='Maritime'||s.category==='Energy'||s.category==='Infrastructure'||s.category==='Agriculture')); let items=[]; mariSources.forEach(src=>{(state.cache[src.id]||[]).forEach(it=>{items.push({...it,sourceName:src.name,sourceIcon:src.icon})})}); items.sort((a,b)=>new Date(b.pubDate||0)-new Date(a.pubDate||0)); items.slice(0,40).forEach(it=>{ const div=document.createElement('div');div.className='tl-item'; const dt=document.createElement('span');dt.className='tl-date';dt.textContent=it.pubDate?fmtDate(new Date(it.pubDate)):''; const sr=document.createElement('span');sr.className='tl-source';sr.textContent=(it.sourceIcon||'')+' '+it.sourceName; const ct=document.createElement('div');ct.className='tl-content'; const a=document.createElement('a');a.href=it.link;a.target='_blank';a.rel='noopener';a.textContent=it.title;ct.appendChild(a); div.appendChild(dt);div.appendChild(sr);div.appendChild(ct);panel.appendChild(div); }); if(!items.length){const _emptyP=document.createElement('p');_emptyP.style.cssText='text-align:center;color:var(--txt3);padding:1rem';_emptyP.textContent='No maritime/infrastructure items. Sync sources first.';panel.appendChild(_emptyP)} } function renderWeatherMapsPanel(){ const panel=document.getElementById('weatherMapsPanel');panel.innerHTML=''; const h=document.createElement('h3');h.style.cssText='font-size:.9rem;margin-bottom:.5rem;color:var(--accent)';h.textContent='\u{1F321} Fortified Weather & Environment';panel.appendChild(h); const note=document.createElement('div');note.className='prem-note'; note.textContent='Interactive weather maps, wave heights, wind patterns, temperature, air quality, and environmental monitoring. Click any tool to open in a new tab.'; panel.appendChild(note); const sections=[ {title:'\u{1F32A} Weather Maps & Radar',links:[ {name:'Ventusky',url:'https://www.ventusky.com/',icon:'\u{1F32C}',tag:'Interactive'},{name:'Windy.com',url:'https://www.windy.com/',icon:'\u{1F32C}',tag:'Interactive'},{name:'Windy Waves',url:'https://www.windy.com/-Waves-waves?waves',icon:'\u{1F30A}',tag:'Waves'},{name:'Windy Wind',url:'https://www.windy.com/-Wind-wind?wind',icon:'\u{1F32C}',tag:'Wind'},{name:'Windy Temperature',url:'https://www.windy.com/-Temperature-temp?temp',icon:'\u{1F321}',tag:'Temp'},{name:'NOAA Weather',url:'https://www.weather.gov/',icon:'\u{1F327}',tag:'Official'},{name:'Zoom Earth',url:'https://zoom.earth/',icon:'\u{1F30E}',tag:'Satellite'},{name:'NWS Radar',url:'https://radar.weather.gov/',icon:'\u{1F4E1}',tag:'Radar'} ]}, {title:'\u{1F30A} Ocean & Wave Height',links:[ {name:'NOAA Wave Height',url:'https://www.ndbc.noaa.gov/',icon:'\u{1F30A}',tag:'Buoys'},{name:'Surfline',url:'https://www.surfline.com/',icon:'\u{1F3C4}',tag:'Surf'},{name:'Magic Seaweed',url:'https://magicseaweed.com/',icon:'\u{1F30A}',tag:'Surf'},{name:'NOAA Tides',url:'https://tidesandcurrents.noaa.gov/',icon:'\u{1F30A}',tag:'Tides'} ]}, {title:'\u{1F32C} Air Quality & Pollution',links:[ {name:'AirNow (US AQI)',url:'https://www.airnow.gov/',icon:'\u{1F32C}',tag:'US'},{name:'IQAir World AQI',url:'https://www.iqair.com/world-air-quality',icon:'\u{1F32C}',tag:'Global'},{name:'AQICN',url:'https://aqicn.org/map/world/',icon:'\u{1F32C}',tag:'Map'},{name:'PurpleAir Map',url:'https://map.purpleair.com/',icon:'\u{1F32C}',tag:'Sensors'} ]}, {title:'\u{1F525} Wildfire & Severe Weather',links:[ {name:'NIFC Wildfire Map',url:'https://www.nifc.gov/fire-information',icon:'\u{1F525}',tag:'Wildfire'},{name:'NASA FIRMS Fire Map',url:'https://firms.modaps.eosdis.nasa.gov/map/',icon:'\u{1F525}',tag:'Satellite'},{name:'SPC Convective Outlook',url:'https://www.spc.noaa.gov/',icon:'\u{1F329}',tag:'Severe'},{name:'Lightning Map',url:'https://www.lightningmaps.org/',icon:'\u26A1',tag:'Lightning'} ]} ]; sections.forEach(sec=>{ const sh=document.createElement('div');sh.className='prem-section-head';sh.style.marginTop='.5rem';sh.textContent=sec.title;panel.appendChild(sh); const grid=document.createElement('div');grid.className='links-grid'; sec.links.forEach(l=>{ const a=document.createElement('a');a.className='link-item';a.href=l.url;a.target='_blank';a.rel='noopener'; a.innerHTML=''+l.icon+''+esc(l.name)+''+esc(l.tag)+''; grid.appendChild(a); }); panel.appendChild(grid); }); } function renderEconLabPanel(){ const panel=document.getElementById('econLabPanel');panel.innerHTML=''; const h=document.createElement('h3');h.style.cssText='font-size:.9rem;margin-bottom:.5rem;color:var(--accent)';h.textContent='\u{1F9EA} Econ Lab \u2014 Calculators, CPI & Commodity Data';panel.appendChild(h); // ShadowStats CPI section const cpiH=document.createElement('div');cpiH.className='prem-section-head';cpiH.textContent='\u{1F4B5} CPI & Real Dollar Value Calculator';panel.appendChild(cpiH); const cpiNote=document.createElement('div');cpiNote.className='prem-note'; cpiNote.innerHTML='Compare official CPI (BLS methodology) vs alternative calculations (1980s/1990s methodology). ShadowStats.com provides alternative inflation data.'; panel.appendChild(cpiNote); // Calculator const calcDiv=document.createElement('div');calcDiv.style.cssText='background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius-lg);padding:.8rem;margin-bottom:.7rem'; calcDiv.innerHTML=`
`; panel.appendChild(calcDiv); // CPI calculator logic setTimeout(()=>{ document.getElementById('cpiCalcBtn')?.addEventListener('click',()=>{ const amt=parseFloat(document.getElementById('cpiAmount').value)||100; const from=parseInt(document.getElementById('cpiFromYear').value)||2000; const to=parseInt(document.getElementById('cpiToYear').value)||2026; const years=to-from; // Approximate CPI rates const officialRate=0.025; // ~2.5% official CPI avg const alt1980Rate=0.10; // ~10% using 1980s methodology per ShadowStats const alt1990Rate=0.06; // ~6% using 1990s methodology const officialVal=amt*Math.pow(1+officialRate,years); const alt1980Val=amt*Math.pow(1+alt1980Rate,years); const alt1990Val=amt*Math.pow(1+alt1990Rate,years); const officialPower=amt/Math.pow(1+officialRate,years); const alt1980Power=amt/Math.pow(1+alt1980Rate,years); const res=document.getElementById('cpiResult'); res.innerHTML=`
Official CPI (~${(officialRate*100).toFixed(1)}%/yr)
$${amt.toFixed(2)} in ${from} = $${officialVal.toFixed(2)} in ${to}
Purchasing power of $${amt}: $${officialPower.toFixed(2)}
1980s Method (~${(alt1980Rate*100).toFixed(0)}%/yr)
$${amt.toFixed(2)} in ${from} = $${alt1980Val.toFixed(2)} in ${to}
Purchasing power of $${amt}: $${alt1980Power.toFixed(2)}
1990s Method (~${(alt1990Rate*100).toFixed(0)}%/yr)
$${amt.toFixed(2)} in ${from} = $${alt1990Val.toFixed(2)} in ${to}
`; }); },0); // Gold vs S&P section const goldH=document.createElement('div');goldH.className='prem-section-head';goldH.style.marginTop='.8rem';goldH.textContent='\u{1F947} Gold vs Market Comparison';panel.appendChild(goldH); const goldNote=document.createElement('div');goldNote.className='prem-note'; const goldPrice=tickerData['GC=F']?.price||'N/A'; const spPrice=tickerData['^GSPC']?.price||'N/A'; const nasdaqPrice=tickerData['^IXIC']?.price||'N/A'; const goldSPRatio=typeof goldPrice==='number'&&typeof spPrice==='number'?(spPrice/goldPrice).toFixed(2):'N/A'; goldNote.innerHTML=`Current Gold: $${typeof goldPrice==='number'?goldPrice.toLocaleString(undefined,{minimumFractionDigits:2}):goldPrice} | S&P 500: ${typeof spPrice==='number'?spPrice.toLocaleString(undefined,{minimumFractionDigits:2}):spPrice} | NASDAQ: ${typeof nasdaqPrice==='number'?nasdaqPrice.toLocaleString(undefined,{minimumFractionDigits:2}):nasdaqPrice}
S&P/Gold Ratio: ${goldSPRatio} (historical avg ~1.5-2.0, higher = stocks expensive vs gold)`; panel.appendChild(goldNote); // Fuel prices section const fuelH=document.createElement('div');fuelH.className='prem-section-head';fuelH.style.marginTop='.8rem';fuelH.textContent='\u26FD Fuel Price Tracker';panel.appendChild(fuelH); const oilPrice=tickerData['CL=F']?.price||'N/A'; const natGasPrice=tickerData['NG=F']?.price||'N/A'; const fuelNote=document.createElement('div');fuelNote.className='prem-note'; const bblToGal=typeof oilPrice==='number'?(oilPrice/42).toFixed(2):'N/A'; const bblToLiter=typeof oilPrice==='number'?(oilPrice/159).toFixed(3):'N/A'; fuelNote.innerHTML=`Crude Oil (WTI): $${typeof oilPrice==='number'?oilPrice.toFixed(2):oilPrice}/barrel | Per Gallon (raw): $${bblToGal} | Per Liter (raw): $${bblToLiter}
Natural Gas: $${typeof natGasPrice==='number'?natGasPrice.toFixed(2):natGasPrice}/MMBtu`; panel.appendChild(fuelNote); const fuelGrid=document.createElement('div');fuelGrid.className='links-grid'; [{name:'GasBuddy (US)',url:'https://www.gasbuddy.com/',icon:'\u26FD',tag:'US'},{name:'GlobalPetrolPrices',url:'https://www.globalpetrolprices.com/',icon:'\u26FD',tag:'Global'},{name:'AAA Gas Prices',url:'https://gasprices.aaa.com/',icon:'\u26FD',tag:'US'},{name:'Oil Price.com',url:'https://oilprice.com/',icon:'\u{1F6E2}',tag:'Global'}].forEach(l=>{ const a=document.createElement('a');a.className='link-item';a.href=l.url;a.target='_blank';a.rel='noopener'; a.innerHTML=''+l.icon+''+esc(l.name)+''+esc(l.tag)+''; fuelGrid.appendChild(a); }); panel.appendChild(fuelGrid); // Metal exchanges const metalH=document.createElement('div');metalH.className='prem-section-head';metalH.style.marginTop='.8rem';metalH.textContent='\u{1FA99} Metal Exchanges & Commodity Data';panel.appendChild(metalH); const metalGrid=document.createElement('div');metalGrid.className='links-grid'; [{name:'LME (London Metal Exchange)',url:'https://www.lme.com/',icon:'\u{1FA99}',tag:'Metals'},{name:'COMEX (CME)',url:'https://www.cmegroup.com/markets/metals.html',icon:'\u{1FA99}',tag:'Futures'},{name:'Kitco Live',url:'https://www.kitco.com/',icon:'\u{1F947}',tag:'Gold'},{name:'ShadowStats',url:'http://www.shadowstats.com/',icon:'\u{1F4CA}',tag:'Alt CPI'},{name:'FRED (St Louis Fed)',url:'https://fred.stlouisfed.org/',icon:'\u{1F4CA}',tag:'Econ Data'},{name:'Trading Economics',url:'https://tradingeconomics.com/commodities',icon:'\u{1F4CA}',tag:'Global'},{name:'FinViz Futures',url:'https://finviz.com/futures.ashx',icon:'\u{1F4C8}',tag:'Charts'},{name:'Seeking Alpha',url:'https://seekingalpha.com/',icon:'\u{1F4CA}',tag:'Analysis'}].forEach(l=>{ const a=document.createElement('a');a.className='link-item';a.href=l.url;a.target='_blank';a.rel='noopener'; a.innerHTML=''+l.icon+''+esc(l.name)+''+esc(l.tag)+''; metalGrid.appendChild(a); }); panel.appendChild(metalGrid); } function renderReportsPanel(){ const panel=document.getElementById('reportsPanel');panel.innerHTML=''; const h=document.createElement('h3');h.style.cssText='font-size:.9rem;margin-bottom:.5rem;color:var(--accent)';h.textContent='\u{1F4C4} Report Generator';panel.appendChild(h); const note=document.createElement('div');note.className='prem-note'; note.textContent='Generate reports from all available news feeds and categories. Reports include cited links, source attribution, sentiment analysis, timeline, and category breakdown. Customize the time range, filters, and output format below.'; panel.appendChild(note); // Controls row 1: Time & Filters const controls1=document.createElement('div');controls1.style.cssText='display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:.4rem;align-items:flex-end'; controls1.innerHTML=`
`; panel.appendChild(controls1); // Controls row 2: Format & Options const controls2=document.createElement('div');controls2.style.cssText='display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:.4rem;align-items:flex-end'; controls2.innerHTML=`
`; panel.appendChild(controls2); // Controls row 3: Actions const controls3=document.createElement('div');controls3.style.cssText='display:flex;gap:.4rem;flex-wrap:wrap;margin-bottom:.6rem'; controls3.innerHTML=` `; panel.appendChild(controls3); // Controls row 4: v9 Intelligence Reports const controls4=document.createElement('div');controls4.style.cssText='display:flex;gap:.4rem;flex-wrap:wrap;margin-bottom:.6rem;padding-top:.4rem;border-top:1px solid var(--border)'; controls4.innerHTML=` INTEL REPORTS: `; panel.appendChild(controls4); // Populate dropdowns setTimeout(()=>{ const catSel=document.getElementById('reportCats'); if(catSel){[...new Set(state.sources.map(s=>s.category))].sort().forEach(c=>{const o=document.createElement('option');o.value=c;o.textContent=c;catSel.appendChild(o)})} const regSel=document.getElementById('reportRegion'); if(regSel){state.regions.forEach(r=>{const o=document.createElement('option');o.value=r;o.textContent=r;regSel.appendChild(o)})} document.getElementById('genHtmlReport')?.addEventListener('click',()=>generateReport('html')); document.getElementById('genTextReport')?.addEventListener('click',()=>generateReport('text')); document.getElementById('genCsvReport')?.addEventListener('click',()=>generateReport('csv')); document.getElementById('genJsonReport')?.addEventListener('click',()=>generateReport('json')); document.getElementById('previewReport')?.addEventListener('click',()=>generateReport('preview')); document.getElementById('genThreatReport')?.addEventListener('click',()=>generateThreatReport()); document.getElementById('genSentimentReport')?.addEventListener('click',()=>generateSentimentReport()); document.getElementById('genEntityReport')?.addEventListener('click',()=>generateEntityReport()); document.getElementById('genSourceAudit')?.addEventListener('click',()=>generateSourceAudit()); },0); const output=document.createElement('div');output.id='reportOutput';output.style.cssText='margin-top:.6rem;max-height:500px;overflow-y:auto'; panel.appendChild(output); } function getReportDateStr(d,fmt){ if(!d||isNaN(d)) return''; switch(fmt){ case 'iso':return d.toISOString(); case 'us':return`${d.getMonth()+1}/${d.getDate()}/${d.getFullYear()} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; case 'eu':return`${d.getDate()}/${d.getMonth()+1}/${d.getFullYear()} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; case 'relative':return timeAgo(d); default:return d.toLocaleString(); } } // ── Report Analytics Helpers ── const STOP_WORDS=new Set(['the','a','an','and','or','but','in','on','at','to','for','of','with','by','from','is','are','was','were','be','been','being','have','has','had','do','does','did','will','would','could','should','may','might','can','shall','it','its','this','that','these','those','he','she','they','we','you','i','me','my','our','your','his','her','their','what','which','who','whom','how','when','where','why','not','no','nor','so','if','then','than','too','very','just','about','above','after','before','between','into','through','during','out','off','over','under','again','further','once','here','there','all','each','every','both','few','more','most','other','some','such','only','own','same','also','as','up','s','t','don','re','ve','ll','d','m','said','says','new','us','de','la','el']); function extractKeywords(items,topN=25){ const freq={}; items.forEach(it=>{ const text=((it.title||'')+' '+(it.description||'')).toLowerCase(); // Extract words 3+ chars, skip URLs and numbers const words=text.replace(/https?:\/\/\S+/g,'').replace(/[^a-z\s'-]/g,' ').split(/\s+/).filter(w=>w.length>=3&&!STOP_WORDS.has(w)&&!/^\d+$/.test(w)); words.forEach(w=>{freq[w]=(freq[w]||0)+1}); }); return Object.entries(freq).sort((a,b)=>b[1]-a[1]).slice(0,topN); } function extractBigrams(items,topN=15){ const freq={}; items.forEach(it=>{ const text=((it.title||'')).toLowerCase().replace(/[^a-z\s'-]/g,' '); const words=text.split(/\s+/).filter(w=>w.length>=3&&!STOP_WORDS.has(w)); for(let i=0;ic>=2).sort((a,b)=>b[1]-a[1]).slice(0,topN); } function detectTrending(items,days){ if(items.length<4||days<0.1) return[]; const mid=Date.now()-(days*86400000/2); const firstHalf=items.filter(it=>it.pubDate.getTime()it.pubDate.getTime()>=mid); const countWords=(arr)=>{const f={};arr.forEach(it=>{const words=((it.title||'')).toLowerCase().replace(/[^a-z\s]/g,' ').split(/\s+/).filter(w=>w.length>=4&&!STOP_WORDS.has(w));words.forEach(w=>{f[w]=(f[w]||0)+1})});return f}; const early=countWords(firstHalf);const recent=countWords(secondHalf); const trending=[]; Object.entries(recent).forEach(([word,recentCount])=>{ if(recentCount<2) return; const earlyCount=early[word]||0; const velocity=earlyCount>0?(recentCount/earlyCount):recentCount; if(velocity>=1.5||earlyCount===0) trending.push({word,recentCount,earlyCount,velocity}); }); trending.sort((a,b)=>b.velocity-a.velocity); return trending.slice(0,12); } function buildHourDistribution(items){ const hours=new Array(24).fill(0); items.forEach(it=>{if(it.pubDate) hours[it.pubDate.getHours()]++}); return hours; } function findDuplicates(items,threshold=0.55){ const groups=[];const assigned=new Set(); const tokenize=s=>{const t=new Set(s.toLowerCase().replace(/[^a-z0-9\s]/g,'').split(/\s+/).filter(w=>w.length>=3));return t}; const jaccard=(a,b)=>{let inter=0;a.forEach(w=>{if(b.has(w)) inter++});return inter/(a.size+b.size-inter)}; const tokens=items.map(it=>tokenize(it.title||'')); for(let i=0;i=threshold){group.push(j);assigned.add(j)} } if(group.length>=2){assigned.add(i);groups.push(group)} } return groups.map(g=>({count:g.length,title:items[g[0]].title,sources:g.map(i=>items[i].sourceName).filter((v,i,a)=>a.indexOf(v)===i)})); } function matchWatchlistKeywords(items){ const allKw=getAllKeywords(); if(!allKw.length) return[]; const hits=[]; items.forEach(it=>{ const text=(it.title||'')+' '+(it.description||''); const matched=allKw.filter(kw=>kw.test(text)); if(matched.length) hits.push({title:it.title,link:it.link,pubDate:it.pubDate,sourceName:it.sourceName,keywords:matched.map(kw=>kw.source.replace(/\\[bsi]/g,'').replace(/[()]/g,''))}); }); return hits; } function calcReadingLoad(items){ let totalWords=0; items.forEach(it=>{totalWords+=((it.title||'')+' '+(it.description||'')).split(/\s+/).length}); return{totalWords,minutes:Math.ceil(totalWords/200),hours:(totalWords/200/60).toFixed(1)}; } function buildRegionSentimentMatrix(items){ const matrix={}; items.forEach(it=>{ const r=it.region||'Unknown'; if(!matrix[r]) matrix[r]={positive:0,negative:0,neutral:0,total:0}; matrix[r].total++; if(it.sentimentScore>=1) matrix[r].positive++; else if(it.sentimentScore<=-1) matrix[r].negative++; else matrix[r].neutral++; }); return matrix; } function buildSourceDiversity(items){ const catSources={}; items.forEach(it=>{ if(!catSources[it.category]) catSources[it.category]=new Set(); catSources[it.category].add(it.sourceName); }); return Object.entries(catSources).map(([cat,srcs])=>({category:cat,sourceCount:srcs.size,sources:[...srcs]})).sort((a,b)=>b.sourceCount-a.sourceCount); } function collectReportItems(){ const days=parseFloat(document.getElementById('reportRange').value)||7; const catFilter=document.getElementById('reportCats').value; const regFilter=document.getElementById('reportRegion').value; const sentFilter=document.getElementById('reportSentiment').value; const sortMode=document.getElementById('reportSort').value; const maxItems=parseInt(document.getElementById('reportMaxItems').value)||0; const cutoff=Date.now()-days*86400000; let items=[]; state.sources.forEach(src=>{ if(!src.enabled) return; if(catFilter!=='all'&&src.category!==catFilter) return; if(regFilter!=='all'&&src.region!==regFilter) return; (state.cache[src.id]||[]).forEach(it=>{ if(!it.pubDate) return; const d=new Date(it.pubDate); if(isNaN(d)||d.getTime()-1) return; if(sentFilter==='neutral'&&sent.score!==0) return; if(sentFilter==='strong'&&Math.abs(sent.score)<2) return; items.push({title:it.title,link:it.link,pubDate:d,description:it.description||'',sourceName:src.name,category:src.category,region:src.region,sentiment:sent.label,sentimentScore:sent.score}); }); }); // Sort switch(sortMode){ case 'date-asc':items.sort((a,b)=>a.pubDate-b.pubDate);break; case 'source':items.sort((a,b)=>a.sourceName.localeCompare(b.sourceName)||(b.pubDate-a.pubDate));break; case 'category':items.sort((a,b)=>a.category.localeCompare(b.category)||(b.pubDate-a.pubDate));break; case 'sentiment-desc':items.sort((a,b)=>a.sentimentScore-b.sentimentScore);break; case 'sentiment-asc':items.sort((a,b)=>b.sentimentScore-a.sentimentScore);break; default:items.sort((a,b)=>b.pubDate-a.pubDate); } if(maxItems>0) items=items.slice(0,maxItems); return{items,days,catFilter,regFilter,sentFilter}; } function generateReport(format){ const{items,days,catFilter,regFilter,sentFilter}=collectReportItems(); const dateFmt=document.getElementById('reportDateFmt').value; const include=document.getElementById('reportInclude').value; const rangeLabel=days<1?(Math.round(days*24)+'h'):days===1?'Daily':days<=3?days+'d':days===7?'Weekly':days===14?'Bi-weekly':days===30?'Monthly':days===60?'60-Day':days===90?'Quarterly':'Custom'; const now=new Date(); const activeFilters=[]; if(catFilter!=='all') activeFilters.push('Category: '+catFilter); if(regFilter!=='all') activeFilters.push('Region: '+regFilter); if(sentFilter!=='all') activeFilters.push('Sentiment: '+sentFilter); const filterStr=activeFilters.length?activeFilters.join(', '):'None'; // Build basic stats const catCounts={};const regCounts={};const srcCounts={}; const sentCounts={positive:0,negative:0,neutral:0,'very positive':0,'very negative':0}; items.forEach(it=>{ catCounts[it.category]=(catCounts[it.category]||0)+1; regCounts[it.region]=(regCounts[it.region]||0)+1; srcCounts[it.sourceName]=(srcCounts[it.sourceName]||0)+1; if(it.sentiment.includes('very pos')) sentCounts['very positive']++; else if(it.sentiment.includes('pos')) sentCounts.positive++; else if(it.sentiment.includes('very neg')) sentCounts['very negative']++; else if(it.sentiment.includes('neg')) sentCounts.negative++; else sentCounts.neutral++; }); // Advanced analytics const keywords=extractKeywords(items); const bigrams=extractBigrams(items); const trending=detectTrending(items,days); const hourDist=buildHourDistribution(items); const duplicates=findDuplicates(items); const watchlistHits=matchWatchlistKeywords(items); const readingLoad=calcReadingLoad(items); const regionSentiment=buildRegionSentimentMatrix(items); const sourceDiversity=buildSourceDiversity(items); if(format==='csv'){ let csv='Date,Source,Category,Region,Sentiment,Title,Link,Description\n'; items.forEach(it=>{ csv+=`"${getReportDateStr(it.pubDate,dateFmt)}","${it.sourceName.replace(/"/g,'""')}","${it.category}","${it.region}","${it.sentiment}","${(it.title||'').replace(/"/g,'""')}","${it.link}","${(it.description||'').replace(/"/g,'""').substring(0,300)}"\n`; }); const blob=new Blob([csv],{type:'text/csv'}); const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`sync-report-${rangeLabel.toLowerCase()}-${now.toISOString().slice(0,10)}.csv`;a.click(); URL.revokeObjectURL(a.href);toast('CSV exported ('+items.length+' rows)','ok');return; } if(format==='json'){ const data={generated:now.toISOString(),period:rangeLabel,days,filters:{category:catFilter,region:regFilter,sentiment:sentFilter},stats:{total:items.length,categories:catCounts,regions:regCounts,sources:srcCounts,sentiment:sentCounts,readingLoad,hourDistribution:hourDist},analytics:{keywords:keywords.map(([w,c])=>({word:w,count:c})),bigrams:bigrams.map(([b,c])=>({phrase:b,count:c})),trending:trending.map(t=>({word:t.word,recent:t.recentCount,earlier:t.earlyCount,velocity:t.velocity.toFixed(1)})),duplicateStories:duplicates,watchlistHits:watchlistHits.length,regionSentiment,sourceDiversity:sourceDiversity.map(s=>({category:s.category,uniqueSources:s.sourceCount}))},items:items.map(it=>({date:it.pubDate.toISOString(),source:it.sourceName,category:it.category,region:it.region,sentiment:it.sentiment,sentimentScore:it.sentimentScore,title:it.title,link:it.link,description:it.description}))}; const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'}); const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`sync-report-${rangeLabel.toLowerCase()}-${now.toISOString().slice(0,10)}.json`;a.click(); URL.revokeObjectURL(a.href);toast('JSON exported ('+items.length+' items)','ok');return; } if(format==='preview'){ // Show preview in a large modal overlay instead of inline div let out=document.getElementById('reportPreviewModal'); if(!out){ const overlay=document.createElement('div');overlay.id='reportPreviewOverlay';overlay.style.cssText='position:fixed;inset:0;z-index:var(--z-modal,200);background:rgba(0,0,0,.6);backdrop-filter:blur(4px);display:flex;justify-content:center;align-items:center;padding:1rem'; const modal=document.createElement('div');modal.style.cssText='background:var(--bg1);border:1px solid var(--border);border-radius:16px;width:100%;max-width:700px;max-height:85vh;overflow-y:auto;box-shadow:0 24px 80px rgba(0,0,0,.5);padding:1rem 1.2rem;position:relative'; modal.id='reportPreviewModal'; const closeBtn=document.createElement('button');closeBtn.style.cssText='position:sticky;top:0;float:right;background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:.3rem .6rem;font-size:.76rem;cursor:pointer;color:var(--txt);z-index:1'; closeBtn.textContent='\u2716 Close Preview';closeBtn.addEventListener('click',()=>overlay.remove()); overlay.addEventListener('click',e=>{if(e.target===overlay) overlay.remove()}); modal.appendChild(closeBtn);overlay.appendChild(modal);document.body.appendChild(overlay); out=modal; } else { const overlay=document.getElementById('reportPreviewOverlay'); if(overlay) overlay.style.display='flex'; out.innerHTML=''; const closeBtn=document.createElement('button');closeBtn.style.cssText='position:sticky;top:0;float:right;background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:.3rem .6rem;font-size:.76rem;cursor:pointer;color:var(--txt);z-index:1'; closeBtn.textContent='\u2716 Close Preview';closeBtn.addEventListener('click',()=>{const o=document.getElementById('reportPreviewOverlay');if(o) o.style.display='none'}); out.appendChild(closeBtn); } const info=document.createElement('div');info.style.cssText='font-size:.76rem;margin-bottom:.5rem;color:var(--txt2)'; info.textContent=`${rangeLabel} report: ${items.length} items | Filters: ${filterStr}`; out.appendChild(info); // Sentiment bar const totalSent=sentCounts.positive+sentCounts['very positive']+sentCounts.negative+sentCounts['very negative']+sentCounts.neutral; if(totalSent){ const bar=document.createElement('div');bar.style.cssText='display:flex;height:12px;border-radius:6px;overflow:hidden;margin-bottom:.5rem;border:1px solid var(--border)'; const posPct=((sentCounts.positive+sentCounts['very positive'])/totalSent*100).toFixed(0); const negPct=((sentCounts.negative+sentCounts['very negative'])/totalSent*100).toFixed(0); const neuPct=(sentCounts.neutral/totalSent*100).toFixed(0); bar.innerHTML=`
`; out.appendChild(bar); const legend=document.createElement('div');legend.style.cssText='font-size:.66rem;color:var(--txt3);margin-bottom:.5rem;display:flex;gap:.6rem'; legend.innerHTML=`\u25B2 Positive: ${posPct}%Neutral: ${neuPct}%\u25BC Negative: ${negPct}%`; out.appendChild(legend); } // Reading load const loadDiv=document.createElement('div');loadDiv.style.cssText='font-size:.7rem;color:var(--txt3);margin-bottom:.5rem'; loadDiv.textContent=`Est. reading load: ~${readingLoad.minutes} min (${readingLoad.totalWords.toLocaleString()} words)`; out.appendChild(loadDiv); // Hour distribution heatmap const maxHour=Math.max(...hourDist,1); if(items.length>=5){ const heatH=document.createElement('div');heatH.style.cssText='font-weight:600;font-size:.74rem;color:var(--accent);margin-bottom:.3rem';heatH.textContent='Activity by Hour (UTC)';out.appendChild(heatH); const heatWrap=document.createElement('div');heatWrap.style.cssText='display:flex;align-items:flex-end;gap:2px;height:40px;margin-bottom:.5rem'; hourDist.forEach((c,h)=>{ const bar=document.createElement('div');bar.title=h+':00 \u2014 '+c+' items'; const pct=Math.max(2,(c/maxHour)*100); bar.style.cssText=`flex:1;height:${pct}%;background:${c>maxHour*0.7?'var(--red)':c>maxHour*0.4?'var(--amber)':'var(--accent)'};border-radius:2px 2px 0 0;min-width:4px;cursor:default;transition:opacity .15s`; heatWrap.appendChild(bar); }); out.appendChild(heatWrap); const heatLabels=document.createElement('div');heatLabels.style.cssText='display:flex;gap:2px;font-size:.5rem;color:var(--txt3);margin-bottom:.5rem'; [0,3,6,9,12,15,18,21].forEach(h=>{const s=document.createElement('span');s.style.cssText='flex:3;text-align:center';s.textContent=h+'h';heatLabels.appendChild(s)}); out.appendChild(heatLabels); } // Keywords if(keywords.length){ const kwH=document.createElement('div');kwH.style.cssText='font-weight:600;font-size:.74rem;color:var(--accent);margin-bottom:.3rem';kwH.textContent='Top Keywords';out.appendChild(kwH); const kwWrap=document.createElement('div');kwWrap.style.cssText='display:flex;flex-wrap:wrap;gap:.25rem;margin-bottom:.5rem'; keywords.slice(0,20).forEach(([word,count])=>{ const chip=document.createElement('span');chip.style.cssText='font-size:.66rem;padding:.12rem .35rem;border-radius:8px;background:var(--bg2);border:1px solid var(--border);color:var(--txt2);cursor:default'; chip.textContent=word+' ('+count+')';chip.title=count+' mentions';kwWrap.appendChild(chip); }); out.appendChild(kwWrap); } // Trending topics if(trending.length){ const trH=document.createElement('div');trH.style.cssText='font-weight:600;font-size:.74rem;color:var(--red);margin-bottom:.3rem';trH.textContent='\u{1F525} Trending (accelerating)';out.appendChild(trH); trending.slice(0,8).forEach(t=>{ const row=document.createElement('div');row.style.cssText='display:flex;align-items:center;gap:.4rem;font-size:.72rem;padding:.12rem 0'; row.innerHTML=`\u25B2${esc(t.word)}${t.earlyCount}\u2192${t.recentCount}${t.velocity===Infinity?'NEW':t.velocity.toFixed(1)+'x'}`; out.appendChild(row); }); } // Bigrams if(bigrams.length){ const biH=document.createElement('div');biH.style.cssText='font-weight:600;font-size:.74rem;color:var(--accent);margin-top:.4rem;margin-bottom:.3rem';biH.textContent='Key Phrases';out.appendChild(biH); const biWrap=document.createElement('div');biWrap.style.cssText='display:flex;flex-wrap:wrap;gap:.25rem;margin-bottom:.5rem'; bigrams.slice(0,12).forEach(([phrase,count])=>{ const chip=document.createElement('span');chip.style.cssText='font-size:.66rem;padding:.12rem .35rem;border-radius:8px;background:var(--accent-dim);border:1px solid var(--accent);color:var(--accent);cursor:default'; chip.textContent='"'+phrase+'" ('+count+')';biWrap.appendChild(chip); }); out.appendChild(biWrap); } // Duplicate stories if(duplicates.length){ const dupH=document.createElement('div');dupH.style.cssText='font-weight:600;font-size:.74rem;color:var(--purple);margin-bottom:.3rem';dupH.textContent='\u{1F501} Related/Duplicate Stories ('+duplicates.length+' clusters)';out.appendChild(dupH); duplicates.slice(0,8).forEach(d=>{ const row=document.createElement('div');row.style.cssText='font-size:.72rem;padding:.2rem 0;border-bottom:1px solid var(--bg2)'; row.innerHTML=`${esc(d.title)} \u2014 ${d.count} versions across ${d.sources.join(', ')}`; out.appendChild(row); }); } // Watchlist hits if(watchlistHits.length){ const wlH=document.createElement('div');wlH.style.cssText='font-weight:600;font-size:.74rem;color:var(--amber);margin-top:.4rem;margin-bottom:.3rem';wlH.textContent='\u{1F514} Watchlist Keyword Hits ('+watchlistHits.length+')';out.appendChild(wlH); watchlistHits.slice(0,10).forEach(h=>{ const row=document.createElement('div');row.style.cssText='font-size:.72rem;padding:.2rem 0;border-bottom:1px solid var(--bg2)'; row.innerHTML=`[${esc(h.keywords.join(', '))}] ${esc(h.title)} \u2014 ${esc(h.sourceName)}`; out.appendChild(row); }); } // Region x Sentiment const regSentEntries=Object.entries(regionSentiment); if(regSentEntries.length>1){ const rsH=document.createElement('div');rsH.style.cssText='font-weight:600;font-size:.74rem;color:var(--accent);margin-top:.4rem;margin-bottom:.3rem';rsH.textContent='Region x Sentiment';out.appendChild(rsH); regSentEntries.sort((a,b)=>b[1].total-a[1].total).forEach(([reg,data])=>{ const row=document.createElement('div');row.style.cssText='display:flex;align-items:center;gap:.3rem;font-size:.7rem;padding:.15rem 0'; const posPct=data.total?(data.positive/data.total*100).toFixed(0):0; const negPct=data.total?(data.negative/data.total*100).toFixed(0):0; row.innerHTML=`${esc(reg)}${data.total}
${posPct}%+${negPct}%-`; out.appendChild(row); }); } // Correlation flags const correlations=detectCorrelations(); const corrEntries=Object.entries(correlations); if(corrEntries.length){ const corrH=document.createElement('div');corrH.style.cssText='font-weight:600;font-size:.74rem;color:var(--red);margin-top:.4rem;margin-bottom:.3rem';corrH.textContent='🔗 Cross-Category Correlations (3+ categories)';out.appendChild(corrH); corrEntries.sort((a,b)=>b[1].categories.length-a[1].categories.length).forEach(([entity,data])=>{ const row=document.createElement('div');row.style.cssText='font-size:.72rem;padding:.2rem 0;border-bottom:1px solid var(--bg2)'; row.innerHTML=`${esc(entity)} (${data.count} mentions across ${data.categories.length} categories)
${data.categories.join(', ')}`; out.appendChild(row); }); } // Source diversity if(sourceDiversity.length>1){ const sdH=document.createElement('div');sdH.style.cssText='font-weight:600;font-size:.74rem;color:var(--accent);margin-top:.4rem;margin-bottom:.3rem';sdH.textContent='Source Diversity by Category';out.appendChild(sdH); sourceDiversity.slice(0,10).forEach(s=>{ const row=document.createElement('div');row.style.cssText='display:flex;align-items:center;gap:.4rem;font-size:.72rem;padding:.12rem 0'; row.innerHTML=`${esc(s.category)}${s.sourceCount} sources`; out.appendChild(row); }); } // Top sources const topSrc=Object.entries(srcCounts).sort((a,b)=>b[1]-a[1]).slice(0,10); if(topSrc.length){ const srcH=document.createElement('div');srcH.style.cssText='font-weight:600;font-size:.74rem;color:var(--accent);margin-top:.4rem;margin-bottom:.3rem';srcH.textContent='Top Sources';out.appendChild(srcH); topSrc.forEach(([name,count])=>{ const row=document.createElement('div');row.style.cssText='display:flex;align-items:center;gap:.4rem;font-size:.72rem;padding:.15rem 0'; const pct=(count/items.length*100).toFixed(0); row.innerHTML=`${esc(name)}${count}
`; out.appendChild(row); }); } // Preview items const pvH=document.createElement('div');pvH.style.cssText='font-weight:600;font-size:.74rem;color:var(--accent);margin-top:.5rem;margin-bottom:.3rem;border-top:1px solid var(--border);padding-top:.4rem'; pvH.textContent='Items Preview (first 50)';out.appendChild(pvH); items.slice(0,50).forEach(it=>{ const div=document.createElement('div');div.className='tl-item'; const dt=document.createElement('span');dt.className='tl-date';dt.textContent=getReportDateStr(it.pubDate,dateFmt); const sr=document.createElement('span');sr.className='tl-source';sr.textContent=it.sourceName; const ct=document.createElement('div');ct.className='tl-content'; const a=document.createElement('a');a.href=it.link;a.target='_blank';a.rel='noopener';a.textContent=it.title;ct.appendChild(a); if(it.sentiment!=='neutral'){const sb=document.createElement('span');sb.className='sent-badge '+(it.sentiment.includes('pos')?'sent-pos':'sent-neg');sb.textContent=it.sentiment;ct.appendChild(sb)} if(include!=='headlines'&&it.description){const p=document.createElement('p');p.textContent=it.description.substring(0,160);ct.appendChild(p)} div.appendChild(dt);div.appendChild(sr);div.appendChild(ct);out.appendChild(div); }); toast('Preview generated','ok');return; } if(format==='html'){ let html=`Sync Dashboard ${rangeLabel} Report - ${now.toLocaleDateString()} `; html+=`

\u{1F4CA} Sync Dashboard ${rangeLabel} Report

`; html+=`
Generated: ${getReportDateStr(now,dateFmt)}
Period: Last ${days<1?Math.round(days*24)+' hour(s)':days+' day(s)'}
Total Items: ${items.length}
Active Sources: ${state.sources.filter(s=>s.enabled).length}
Filters: ${filterStr}
`; // Sentiment bar const totalSent=Object.values(sentCounts).reduce((a,b)=>a+b,0); if(totalSent){ const posPct=((sentCounts.positive+sentCounts['very positive'])/totalSent*100).toFixed(1); const negPct=((sentCounts.negative+sentCounts['very negative'])/totalSent*100).toFixed(1); html+=`

\u{1F4CA} Sentiment Analysis

`; html+=`
`; html+=`
\u25B2\u25B2 Very Positive: ${sentCounts['very positive']} | \u25B2 Positive: ${sentCounts.positive} | Neutral: ${sentCounts.neutral} | \u25BC Negative: ${sentCounts.negative} | \u25BC\u25BC Very Negative: ${sentCounts['very negative']}
`; } // Category breakdown html+='

\u{1F4CB} Category Breakdown

'; Object.entries(catCounts).sort((a,b)=>b[1]-a[1]).forEach(([cat,count])=>{html+=``}); html+='
CategoryItems%
${cat}${count}${(count/items.length*100).toFixed(1)}%
'; // Region breakdown html+='

\u{1F310} Region Breakdown

'; Object.entries(regCounts).sort((a,b)=>b[1]-a[1]).forEach(([reg,count])=>{html+=``}); html+='
RegionItems%
${reg}${count}${(count/items.length*100).toFixed(1)}%
'; // Top sources html+='

\u{1F4F0} Top Sources

'; Object.entries(srcCounts).sort((a,b)=>b[1]-a[1]).slice(0,20).forEach(([src,count])=>{html+=``}); html+='
SourceItems
${src}${count}
'; // Reading load html+=`
Est. Reading Load: ~${readingLoad.minutes} min (${readingLoad.totalWords.toLocaleString()} words, ${readingLoad.hours} hours)
`; // Hour distribution if(items.length>=5){ const maxH=Math.max(...hourDist,1); html+='

\u{1F552} Activity by Hour

'; hourDist.forEach((c,h)=>{const pct=Math.max(2,(c/maxH)*100);html+=`
`}); html+='
'; for(let h=0;h<24;h+=3) html+=`${h}h`; html+='
'; } // Keywords if(keywords.length){ html+='

\u{1F50D} Top Keywords

'; keywords.forEach(([w,c])=>{html+=``}); html+='
KeywordMentions
${w}${c}
'; } // Key phrases if(bigrams.length){ html+='

\u{1F4AC} Key Phrases

'; bigrams.forEach(([b,c])=>{html+=``}); html+='
PhraseCount
"${b}"${c}
'; } // Trending if(trending.length){ html+='

\u{1F525} Trending Topics (accelerating)

'; trending.forEach(t=>{html+=``}); html+='
TopicEarlierRecentVelocity
${t.word}${t.earlyCount}${t.recentCount}${t.velocity===Infinity?'NEW':t.velocity.toFixed(1)+'x'}
'; } // Duplicate stories if(duplicates.length){ html+='

\u{1F501} Related/Duplicate Stories ('+duplicates.length+' clusters)

'; duplicates.forEach(d=>{html+=`
${d.title} \u2014 ${d.count} versions across: ${d.sources.join(', ')}
`}); } // Watchlist hits if(watchlistHits.length){ html+='

\u{1F514} Watchlist Keyword Hits ('+watchlistHits.length+')

'; watchlistHits.forEach(h=>{html+=`
[${h.keywords.join(', ')}] ${h.title} \u2014 ${h.sourceName}
`}); } // Region x Sentiment if(Object.keys(regionSentiment).length>1){ html+='

\u{1F310} Region \u00D7 Sentiment

'; Object.entries(regionSentiment).sort((a,b)=>b[1].total-a[1].total).forEach(([reg,d])=>{ const ratio=d.negative>0?(d.positive/d.negative).toFixed(1):(d.positive>0?'\u221E':'--'); html+=``; }); html+='
RegionTotalPositiveNeutralNegativeRatio
${reg}${d.total}${d.positive}${d.neutral}${d.negative}${ratio}
'; } // Source diversity if(sourceDiversity.length>1){ html+='

\u{1F4CA} Source Diversity by Category

'; sourceDiversity.forEach(s=>{html+=``}); html+='
CategoryUnique SourcesSources
${s.category}${s.sourceCount}${s.sources.slice(0,5).join(', ')}${s.sources.length>5?' +'+( s.sources.length-5)+' more':''}
'; } // Timeline html+='

\u{1F4C5} Timeline

'; items.forEach(it=>{ const sentClass=it.sentiment.includes('pos')?'sent-pos':it.sentiment.includes('neg')?'sent-neg':''; html+=`
${it.title}`; if(include!=='headlines'&&it.description) html+=`
${it.description.substring(0,300)}
`; html+=`
${getReportDateStr(it.pubDate,dateFmt)} | ${it.sourceName} | ${it.category} | ${it.region} | ${it.sentiment}${include==='full'?' | '+it.link:''}
`; }); html+=`
Generated by Sync Dashboard \u2022 ${now.toISOString()}
${items.length} items across ${Object.keys(srcCounts).length} sources
`; const blob=new Blob([html],{type:'text/html'}); const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`sync-report-${rangeLabel.toLowerCase()}-${now.toISOString().slice(0,10)}.html`;a.click(); URL.revokeObjectURL(a.href);toast('HTML report downloaded ('+items.length+' items)','ok'); }else{ let text=`SYNC DASHBOARD ${rangeLabel.toUpperCase()} REPORT\n${'='.repeat(60)}\nGenerated: ${getReportDateStr(now,dateFmt)}\nPeriod: Last ${days<1?Math.round(days*24)+' hour(s)':days+' day(s)'}\nTotal Items: ${items.length}\nActive Sources: ${state.sources.filter(s=>s.enabled).length}\nFilters: ${filterStr}\n\n`; text+='SENTIMENT ANALYSIS\n'+'-'.repeat(40)+'\n'; text+=` Very Positive: ${sentCounts['very positive']}\n Positive: ${sentCounts.positive}\n Neutral: ${sentCounts.neutral}\n Negative: ${sentCounts.negative}\n Very Negative: ${sentCounts['very negative']}\n\n`; text+='CATEGORY BREAKDOWN\n'+'-'.repeat(40)+'\n'; Object.entries(catCounts).sort((a,b)=>b[1]-a[1]).forEach(([cat,count])=>{text+=` ${cat}: ${count} (${(count/items.length*100).toFixed(1)}%)\n`}); text+='\nREGION BREAKDOWN\n'+'-'.repeat(40)+'\n'; Object.entries(regCounts).sort((a,b)=>b[1]-a[1]).forEach(([reg,count])=>{text+=` ${reg}: ${count}\n`}); text+='\nTOP SOURCES\n'+'-'.repeat(40)+'\n'; Object.entries(srcCounts).sort((a,b)=>b[1]-a[1]).slice(0,20).forEach(([src,count])=>{text+=` ${src}: ${count}\n`}); text+=`\nREADING LOAD\n`+'-'.repeat(40)+`\n ~${readingLoad.minutes} min (${readingLoad.totalWords.toLocaleString()} words, ${readingLoad.hours} hours)\n`; // Hour distribution if(items.length>=5){ text+='\nACTIVITY BY HOUR\n'+'-'.repeat(40)+'\n'; const maxH=Math.max(...hourDist,1); hourDist.forEach((c,h)=>{const bar='\u2588'.repeat(Math.round(c/maxH*20));text+=` ${String(h).padStart(2,'0')}:00 ${bar} ${c}\n`}); } // Keywords if(keywords.length){ text+='\nTOP KEYWORDS\n'+'-'.repeat(40)+'\n'; keywords.forEach(([w,c])=>{text+=` ${w}: ${c}\n`}); } if(bigrams.length){ text+='\nKEY PHRASES\n'+'-'.repeat(40)+'\n'; bigrams.forEach(([b,c])=>{text+=` "${b}": ${c}\n`}); } // Trending if(trending.length){ text+='\nTRENDING TOPICS (accelerating)\n'+'-'.repeat(40)+'\n'; trending.forEach(t=>{text+=` ${t.word}: ${t.earlyCount} -> ${t.recentCount} (${t.velocity===Infinity?'NEW':t.velocity.toFixed(1)+'x'})\n`}); } // Duplicates if(duplicates.length){ text+='\nRELATED/DUPLICATE STORIES ('+duplicates.length+' clusters)\n'+'-'.repeat(40)+'\n'; duplicates.forEach(d=>{text+=` "${d.title}" -- ${d.count} versions across: ${d.sources.join(', ')}\n`}); } // Watchlist if(watchlistHits.length){ text+='\nWATCHLIST KEYWORD HITS ('+watchlistHits.length+')\n'+'-'.repeat(40)+'\n'; watchlistHits.forEach(h=>{text+=` [${h.keywords.join(', ')}] ${h.title}\n ${h.link} -- ${h.sourceName}\n`}); } // Region x Sentiment if(Object.keys(regionSentiment).length>1){ text+='\nREGION x SENTIMENT\n'+'-'.repeat(40)+'\n'; text+=' '+['Region','Total','Pos','Neut','Neg','Ratio'].map(s=>s.padEnd(12)).join('')+'\n'; Object.entries(regionSentiment).sort((a,b)=>b[1].total-a[1].total).forEach(([reg,d])=>{ const ratio=d.negative>0?(d.positive/d.negative).toFixed(1):(d.positive>0?'Inf':'--'); text+=' '+[reg,d.total,d.positive,d.neutral,d.negative,ratio].map(s=>String(s).padEnd(12)).join('')+'\n'; }); } // Source diversity if(sourceDiversity.length>1){ text+='\nSOURCE DIVERSITY BY CATEGORY\n'+'-'.repeat(40)+'\n'; sourceDiversity.forEach(s=>{text+=` ${s.category}: ${s.sourceCount} unique sources\n`}); } text+='\n'+'='.repeat(60)+'\nTIMELINE\n'+'='.repeat(60)+'\n'; items.forEach(it=>{ text+=`\n[${getReportDateStr(it.pubDate,dateFmt)}] ${it.sourceName} (${it.category} / ${it.region})\n ${it.title}\n ${it.link}\n Sentiment: ${it.sentiment}`; if(include!=='headlines'&&it.description) text+=`\n ${it.description.substring(0,300)}`; text+='\n'; }); text+=`\n${'='.repeat(60)}\nGenerated by Sync Dashboard | ${now.toISOString()}\n${items.length} items across ${Object.keys(srcCounts).length} sources\n`; const blob=new Blob([text],{type:'text/plain'}); const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`sync-report-${rangeLabel.toLowerCase()}-${now.toISOString().slice(0,10)}.txt`;a.click(); URL.revokeObjectURL(a.href);toast('Text report downloaded ('+items.length+' items)','ok'); } } // ═══════════════════════════════════════════════════════════════════════════════ // GEOINT: Geospatial Intelligence Panel (inspired by WorldView / bilawal.ai) // ═══════════════════════════════════════════════════════════════════════════════ let geointMap=null;let geointMarkers=[];let geointLayers={}; const GEOINT_POIS=[ // Major US Military Installations {name:'Pentagon',lat:38.871,lon:-77.056,cat:'military',icon:'\u{1FA96}'}, {name:'Norfolk Naval Station',lat:36.946,lon:-76.303,cat:'military',icon:'\u2693'}, {name:'San Diego Naval Base',lat:32.684,lon:-117.148,cat:'military',icon:'\u2693'}, {name:'Pearl Harbor',lat:21.353,lon:-157.974,cat:'military',icon:'\u2693'}, {name:'Yokosuka Naval Base',lat:35.283,lon:139.654,cat:'military',icon:'\u2693'}, {name:'Ramstein AB',lat:49.437,lon:-7.600,cat:'military',icon:'\u2708'}, {name:'Diego Garcia',lat:-7.316,lon:72.411,cat:'military',icon:'\u2693'}, {name:'Camp Humphreys (Korea)',lat:36.963,lon:127.031,cat:'military',icon:'\u{1FA96}'}, {name:'Guam Naval Base',lat:13.444,lon:144.794,cat:'military',icon:'\u2693'}, {name:'Bahrain 5th Fleet',lat:26.189,lon:50.565,cat:'military',icon:'\u2693'}, // Major Global Ports {name:'Port of Shanghai',lat:31.365,lon:121.606,cat:'port',icon:'\u{1F6A2}'}, {name:'Port of Singapore',lat:1.264,lon:103.821,cat:'port',icon:'\u{1F6A2}'}, {name:'Port of Rotterdam',lat:51.953,lon:4.145,cat:'port',icon:'\u{1F6A2}'}, {name:'Port of Los Angeles',lat:33.729,lon:-118.262,cat:'port',icon:'\u{1F6A2}'}, {name:'Port of Hamburg',lat:53.533,lon:9.973,cat:'port',icon:'\u{1F6A2}'}, {name:'Port of Busan',lat:35.096,lon:129.036,cat:'port',icon:'\u{1F6A2}'}, {name:'Suez Canal',lat:30.457,lon:32.349,cat:'chokepoint',icon:'\u26A0'}, {name:'Strait of Hormuz',lat:26.567,lon:56.250,cat:'chokepoint',icon:'\u26A0'}, {name:'Strait of Malacca',lat:2.5,lon:101.0,cat:'chokepoint',icon:'\u26A0'}, {name:'Panama Canal',lat:9.08,lon:-79.68,cat:'chokepoint',icon:'\u26A0'}, {name:'Bab el-Mandeb',lat:12.583,lon:43.333,cat:'chokepoint',icon:'\u26A0'}, {name:'Taiwan Strait',lat:24.0,lon:119.5,cat:'chokepoint',icon:'\u26A0'}, // Key Infrastructure {name:'Cape Canaveral',lat:28.396,lon:-80.605,cat:'space',icon:'\u{1F680}'}, {name:'Vandenberg SFB',lat:34.742,lon:-120.574,cat:'space',icon:'\u{1F680}'}, {name:'Baikonur Cosmodrome',lat:45.965,lon:63.305,cat:'space',icon:'\u{1F680}'}, {name:'Jiuquan Launch Center',lat:40.958,lon:100.291,cat:'space',icon:'\u{1F680}'}, {name:'Satish Dhawan (India)',lat:13.720,lon:80.230,cat:'space',icon:'\u{1F680}'}, ]; const GEOINT_CAT_COLORS={military:'#f87171',port:'#38bdf8',chokepoint:'#fbbf24',space:'#a78bfa'}; function renderGeointPanel(){ const panel=document.getElementById('geointPanel'); // Only rebuild if empty (preserve map state) if(panel.dataset.init==='1'){return} panel.dataset.init='1';panel.innerHTML=''; const h=document.createElement('h3');h.style.cssText='font-size:.9rem;margin-bottom:.3rem;color:var(--accent)'; h.innerHTML='\u{1F30E} GEOINT \u2014 Geospatial Intelligence Command Inspired by WorldView'; panel.appendChild(h); const note=document.createElement('div');note.className='prem-note'; note.innerHTML='Fuse open-source intelligence feeds onto an interactive map. Toggle data layers, click markers for details, and use the OSINT tools below for deep analysis. Concept: ADS-B flights, maritime AIS, satellite orbits, GPS jamming zones, and infrastructure overlaid on a single view.'; panel.appendChild(note); // Map layer toggle controls const controls=document.createElement('div');controls.className='geoint-controls'; const layerDefs=[ {id:'military',label:'\u{1FA96} Military Bases',color:'#f87171'}, {id:'port',label:'\u{1F6A2} Major Ports',color:'#38bdf8'}, {id:'chokepoint',label:'\u26A0 Chokepoints',color:'#fbbf24'}, {id:'space',label:'\u{1F680} Launch Sites',color:'#a78bfa'}, ]; layerDefs.forEach(ld=>{ const chip=document.createElement('span');chip.className='geoint-layer-chip active';chip.dataset.layer=ld.id; chip.innerHTML=ld.label;chip.style.borderColor=ld.color; chip.addEventListener('click',()=>{chip.classList.toggle('active');toggleGeointLayer(ld.id,chip.classList.contains('active'))}); controls.appendChild(chip); }); // Tile switcher const tileSel=document.createElement('select');tileSel.className='sort-select';tileSel.style.cssText='font-size:.68rem;margin-left:auto'; tileSel.innerHTML=''; tileSel.addEventListener('change',()=>switchGeointTiles(tileSel.value)); controls.appendChild(tileSel); panel.appendChild(controls); // Map container const mapDiv=document.createElement('div');mapDiv.id='geointMapContainer';mapDiv.className='geoint-map'; panel.appendChild(mapDiv); // Initialize Leaflet map setTimeout(()=>{initGeointMap()},100); // ── Live Data Feeds (iframes/links) ── const feedsSection=document.createElement('div');feedsSection.className='geoint-section'; feedsSection.innerHTML='
\u{1F4E1} Live OSINT Data Feeds
'; const feedGrid=document.createElement('div');feedGrid.className='links-grid'; [ {name:'ADS-B Exchange (flights)',url:'https://globe.adsbexchange.com/',icon:'\u2708',tag:'ADS-B'}, {name:'FlightRadar24',url:'https://www.flightradar24.com/',icon:'\u2708',tag:'Flights'}, {name:'MarineTraffic (vessels)',url:'https://www.marinetraffic.com/',icon:'\u{1F6A2}',tag:'AIS'}, {name:'VesselFinder',url:'https://www.vesselfinder.com/',icon:'\u{1F6A2}',tag:'AIS'}, {name:'N2YO Satellite Tracker',url:'https://www.n2yo.com/',icon:'\u{1F6F0}',tag:'Satellites'}, {name:'SatelliteMap.space',url:'https://satellitemap.space/',icon:'\u{1F6F0}',tag:'Orbits'}, {name:'Celestrak (NORAD TLE)',url:'https://celestrak.org/',icon:'\u{1F6F0}',tag:'Orbital Data'}, {name:'GPSJam (jamming map)',url:'https://gpsjam.org/',icon:'\u{1F4E1}',tag:'GPS/EW'}, {name:'USNI Fleet Tracker',url:'https://news.usni.org/category/fleet-tracker',icon:'\u2693',tag:'Navy'}, {name:'NATO Shipping Centre',url:'https://shipping.nato.int/',icon:'\u{1F30D}',tag:'NATO'}, {name:'Submarine Cable Map',url:'https://www.submarinecablemap.com/',icon:'\u{1F310}',tag:'Infra'}, {name:'Nuclear Threat Initiative',url:'https://www.nti.org/analysis/articles/cns-north-korea-missile-test-database/',icon:'\u2622',tag:'Nuclear'}, ].forEach(l=>{ const a=document.createElement('a');a.className='link-item';a.href=l.url;a.target='_blank';a.rel='noopener'; a.innerHTML=''+l.icon+''+esc(l.name)+''+esc(l.tag)+''; feedGrid.appendChild(a); }); feedsSection.appendChild(feedGrid);panel.appendChild(feedsSection); // ── Map Overlays & Analysis ── const overlaySection=document.createElement('div');overlaySection.className='geoint-section'; overlaySection.innerHTML='
\u{1F5FA} Map Overlays & Situational Awareness
'; const overlayGrid=document.createElement('div');overlayGrid.className='links-grid'; [ {name:'Windy.com (weather overlay)',url:'https://www.windy.com/',icon:'\u{1F32C}',tag:'Weather'}, {name:'Zoom Earth (satellite)',url:'https://zoom.earth/',icon:'\u{1F30E}',tag:'Satellite'}, {name:'NASA FIRMS (fire)',url:'https://firms.modaps.eosdis.nasa.gov/map/',icon:'\u{1F525}',tag:'Wildfire'}, {name:'GDACS Disasters',url:'https://www.gdacs.org/default.aspx',icon:'\u26A0',tag:'Disasters'}, {name:'ACLED Conflict Data',url:'https://acleddata.com/dashboard/',icon:'\u{1F4A5}',tag:'Conflict'}, {name:'Liveuamap',url:'https://liveuamap.com/',icon:'\u{1F4CD}',tag:'Conflict'}, {name:'Sentinel Hub EO Browser',url:'https://apps.sentinel-hub.com/eo-browser/',icon:'\u{1F6F0}',tag:'Imagery'}, {name:'Planet Labs Explorer',url:'https://www.planet.com/explorer/',icon:'\u{1F6F0}',tag:'Imagery'}, {name:'Maxar Open Data',url:'https://www.maxar.com/open-data',icon:'\u{1F6F0}',tag:'Imagery'}, {name:'Google Earth Web',url:'https://earth.google.com/web/',icon:'\u{1F30E}',tag:'3D Globe'}, {name:'OpenAerialMap',url:'https://map.openaerialmap.org/',icon:'\u{1F4F7}',tag:'Aerial'}, {name:'OSINT Map Tools',url:'https://osintframework.com/',icon:'\u{1F50E}',tag:'OSINT'}, ].forEach(l=>{ const a=document.createElement('a');a.className='link-item';a.href=l.url;a.target='_blank';a.rel='noopener'; a.innerHTML=''+l.icon+''+esc(l.name)+''+esc(l.tag)+''; overlayGrid.appendChild(a); }); overlaySection.appendChild(overlayGrid);panel.appendChild(overlaySection); // ── Geospatial Analysis Tools ── const toolsSection=document.createElement('div');toolsSection.className='geoint-section'; toolsSection.innerHTML='
\u{1F6E0} Geospatial Analysis Platforms
'; const toolsGrid=document.createElement('div');toolsGrid.className='links-grid'; [ {name:'Bellingcat Online Toolkit',url:'https://docs.google.com/spreadsheets/d/18rtqh8EG2q1xBo2cLNyhIDuK9jrPGwYr9DI2UncoqJQ/',icon:'\u{1F50E}',tag:'OSINT'}, {name:'Overpass Turbo (OSM queries)',url:'https://overpass-turbo.eu/',icon:'\u{1F5FA}',tag:'OSM'}, {name:'Mapillary (street-level)',url:'https://www.mapillary.com/app',icon:'\u{1F4F7}',tag:'Street'}, {name:'Wikimapia',url:'https://wikimapia.org/',icon:'\u{1F5FA}',tag:'Annotated'}, {name:'QGIS Cloud',url:'https://qgiscloud.com/',icon:'\u{1F5FA}',tag:'GIS'}, {name:'Kepler.gl (data viz)',url:'https://kepler.gl/',icon:'\u{1F4CA}',tag:'Viz'}, {name:'Deck.gl (WebGL maps)',url:'https://deck.gl/',icon:'\u{1F4CA}',tag:'WebGL'}, {name:'CesiumJS (3D globe)',url:'https://cesium.com/cesiumjs/',icon:'\u{1F30E}',tag:'3D'}, {name:'ShadowMap (sun/shadow)',url:'https://shadowmap.org/',icon:'\u2600',tag:'Shadow'}, {name:'TimeZoneDB',url:'https://timezonedb.com/',icon:'\u23F0',tag:'Zones'}, {name:'Copernicus Open Access',url:'https://dataspace.copernicus.eu/',icon:'\u{1F6F0}',tag:'EU Sat'}, {name:'USGS EarthExplorer',url:'https://earthexplorer.usgs.gov/',icon:'\u{1F30E}',tag:'Imagery'}, ].forEach(l=>{ const a=document.createElement('a');a.className='link-item';a.href=l.url;a.target='_blank';a.rel='noopener'; a.innerHTML=''+l.icon+''+esc(l.name)+''+esc(l.tag)+''; toolsGrid.appendChild(a); }); toolsSection.appendChild(toolsGrid);panel.appendChild(toolsSection); // ── Embedded Live Map (ADS-B) ── const embedSection=document.createElement('div');embedSection.className='geoint-section'; embedSection.innerHTML='
\u2708 Live Flight Tracker (ADS-B Exchange)
'; const embedNote=document.createElement('div');embedNote.style.cssText='font-size:.68rem;color:var(--txt3);margin-bottom:.4rem'; embedNote.textContent='Embedded live view of global air traffic from ADS-B Exchange. Click aircraft for details.'; embedSection.appendChild(embedNote); const embedFrame=document.createElement('iframe');embedFrame.className='geoint-embed';embedFrame.src='https://globe.adsbexchange.com/'; embedFrame.setAttribute('loading','lazy');embedFrame.setAttribute('allowfullscreen','');embedFrame.style.cssText='width:100%;height:450px;border:1px solid var(--border);border-radius:var(--radius-lg)'; embedSection.appendChild(embedFrame); panel.appendChild(embedSection); // ── Live Vessel Tracking (direct links — most vessel trackers block iframe embedding) ── const marineSection=document.createElement('div');marineSection.className='geoint-section'; marineSection.innerHTML='
\u{1F6A2} Live Vessel Tracking
'; const marineNote=document.createElement('div');marineNote.className='prem-note'; marineNote.textContent='Real-time AIS vessel position maps. These services block iframe embedding for security, so each opens in a new tab. Click any tracker below.'; marineSection.appendChild(marineNote); const vesselGrid=document.createElement('div');vesselGrid.className='links-grid'; [{name:'MarineTraffic',url:'https://www.marinetraffic.com/',icon:'\u{1F6A2}',tag:'Live AIS',desc:'Industry standard. 160K+ vessels.'}, {name:'VesselFinder',url:'https://www.vesselfinder.com/',icon:'\u{1F6A2}',tag:'Live AIS',desc:'Free real-time ship tracking.'}, {name:'MyShipTracking',url:'https://www.myshiptracking.com/',icon:'\u{1F6A2}',tag:'Live AIS',desc:'Global vessel positions & routes.'}, {name:'ShipMap.org',url:'https://www.shipmap.org/',icon:'\u{1F30D}',tag:'Visualization',desc:'Beautiful animated global shipping routes.'}, {name:'PortWatch (IMF)',url:'https://portwatch.imf.org/',icon:'\u{1F4CA}',tag:'Port Data',desc:'IMF port activity & trade disruption monitor.'}, {name:'USNI Fleet Tracker',url:'https://news.usni.org/category/fleet-tracker',icon:'\u2693',tag:'US Navy',desc:'Weekly positions of US Navy carrier groups.'}, {name:'NATO Shipping Centre',url:'https://shipping.nato.int/',icon:'\u{1F30D}',tag:'NATO',desc:'Allied maritime situational awareness.'}, {name:'Submarine Cable Map',url:'https://www.submarinecablemap.com/',icon:'\u{1F310}',tag:'Infrastructure',desc:'Undersea fiber optic cable routes.'} ].forEach(l=>{ const a=document.createElement('a');a.className='link-item';a.href=l.url;a.target='_blank';a.rel='noopener'; a.innerHTML=`${l.icon}${esc(l.name)}${esc(l.tag)}`; a.title=l.desc;vesselGrid.appendChild(a); }); marineSection.appendChild(vesselGrid); panel.appendChild(marineSection); } const GEOINT_TILES={ dark:'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', satellite:'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', street:'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', topo:'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png' }; function initGeointMap(){ const container=document.getElementById('geointMapContainer'); if(!container||typeof L==='undefined') return; if(geointMap){geointMap.remove();geointMap=null} geointMap=L.map(container,{zoomControl:true,attributionControl:true}).setView([25,0],2); geointLayers.tiles=L.tileLayer(GEOINT_TILES.dark,{ maxZoom:18,attribution:'© OSM | CARTO' }).addTo(geointMap); // Add POI markers by category const catGroups={}; GEOINT_POIS.forEach(poi=>{ if(!catGroups[poi.cat]) catGroups[poi.cat]=L.layerGroup(); const color=GEOINT_CAT_COLORS[poi.cat]||'#38bdf8'; const marker=L.circleMarker([poi.lat,poi.lon],{radius:6,fillColor:color,color:'#fff',weight:1.5,opacity:0.9,fillOpacity:0.8}); marker.bindPopup(`${poi.icon} ${poi.name}
${poi.cat.toUpperCase()} | ${poi.lat.toFixed(2)}, ${poi.lon.toFixed(2)}
Open in Google Maps`); marker.bindTooltip(poi.icon+' '+poi.name,{direction:'top',offset:[0,-8]}); catGroups[poi.cat].addLayer(marker); }); Object.entries(catGroups).forEach(([cat,group])=>{geointLayers[cat]=group;group.addTo(geointMap)}); // Fix map rendering after panel becomes visible setTimeout(()=>{geointMap.invalidateSize()},300); } function toggleGeointLayer(layerId,show){ if(!geointMap||!geointLayers[layerId]) return; if(show) geointMap.addLayer(geointLayers[layerId]); else geointMap.removeLayer(geointLayers[layerId]); } function switchGeointTiles(tileId){ if(!geointMap||!GEOINT_TILES[tileId]) return; if(geointLayers.tiles) geointMap.removeLayer(geointLayers.tiles); const attribs={dark:'CARTO',satellite:'Esri',street:'OSM',topo:'OpenTopoMap'}; geointLayers.tiles=L.tileLayer(GEOINT_TILES[tileId],{maxZoom:18,attribution:'© '+attribs[tileId]}).addTo(geointMap); } function renderAll(){ if(document.hidden) return; // Update view tab badges invalidateNewCounts(); updateViewBadges(); // Render critical bars immediately, defer non-critical to idle time renderCatPills();renderRegPills(); const _idle=window.requestIdleCallback||((fn)=>setTimeout(fn,16)); _idle(()=>{renderHealthBar();renderStatusBar();renderStatsBar();renderHotNews();renderQotdBar();if(typeof updateThreatBar==='function') updateThreatBar();if(typeof checkAlertRules==='function') checkAlertRules()}); // Only render the active view panel (biggest perf win) const v=getActiveView(); if(v==='cards') renderCards(); else if(v==='timeline') renderTimeline(); else if(v==='bookmarks') renderBookmarks(); else if(v==='junk') renderJunkPanel(); else if(v==='video') renderVideoPanel(); else if(v==='social') renderSocialPanel(); else if(v==='sbx') renderSbxPanel(); else if(v==='college') renderCollegePanel(); else if(v==='list') renderListView(); else if(v==='sources') renderSourcesPanel(); else if(v==='politics') renderPoliticsPanel(); else if(v==='holidays') renderHolidaysPanel(); else if(v==='maritime') renderMaritimePanel(); else if(v==='weather-maps') renderWeatherMapsPanel(); else if(v==='econ-lab') renderEconLabPanel(); else if(v==='reports') renderReportsPanel(); else if(v==='geoint') renderGeointPanel(); else if(v==='intel-dash'&&typeof renderIntelDash==='function') renderIntelDash(); else if(v==='premium'&&typeof renderPremiumPanel==='function') renderPremiumPanel(); else if(v==='links'&&typeof renderLinksPanel==='function') renderLinksPanel(); } function rgnCls(r){return'region-'+(r||'Global').replace(/[\s\/]+/g,'-')} // ── Sources Manager Panel ── function renderSourcesPanel(){ const panel=document.getElementById('sourcesPanel');panel.innerHTML=''; // State for this panel const srcSearch=panel._search||''; const srcFilter=panel._filter||'all'; const srcViewMode=panel._viewMode||'grouped'; // Toolbar const toolbar=document.createElement('div');toolbar.className='src-toolbar'; const total=state.sources.length; // Single-pass counting (replaces 3 separate .filter() loops) let enabled=0,errored=0,withContent=0,emptyS=0,neverS=0,defaultS=0,customS=0; state.sources.forEach(s=>{if(s.enabled) enabled++;if(s.enabled&&s.errorState) errored++;if((state.cache[s.id]||[]).length>0) withContent++;if(s.enabled&&!s.errorState&&s.lastFetched&&!(state.cache[s.id]||[]).length) emptyS++;if(!s.lastFetched) neverS++;if(s.isDefault) defaultS++;else customS++}); toolbar.innerHTML=`

\u2699 Source Manager

${total} total \u2022 ${enabled} active \u2022 ${errored} errors \u2022 ${withContent} with content`; const toolRight=document.createElement('div');toolRight.className='src-toolbar-right'; // Search const searchInput=document.createElement('input');searchInput.type='search';searchInput.className='src-search'; searchInput.placeholder='Search sources...';searchInput.value=srcSearch; let _srcSearchDb;searchInput.addEventListener('input',()=>{clearTimeout(_srcSearchDb);_srcSearchDb=setTimeout(()=>{panel._search=searchInput.value;renderSourcesPanel()},250)}); toolRight.appendChild(searchInput); // View mode toggle const vmToggle=document.createElement('div');vmToggle.className='src-view-toggle'; ['grouped','flat'].forEach(m=>{ const btn=document.createElement('button');btn.textContent=m==='grouped'?'\u25A4 Grouped':'\u2630 Flat'; btn.className=srcViewMode===m?'active':''; btn.addEventListener('click',()=>{panel._viewMode=m;renderSourcesPanel()}); vmToggle.appendChild(btn); }); toolRight.appendChild(vmToggle); toolbar.appendChild(toolRight);panel.appendChild(toolbar); // Bulk actions bar const bulkBar=document.createElement('div');bulkBar.style.cssText='display:flex;gap:.3rem;margin-bottom:.5rem;flex-wrap:wrap'; const mkBulk=(label,style,fn)=>{const b=document.createElement('button');b.className='btn btn-sm';b.style.cssText='font-size:.64rem;'+style;b.textContent=label;b.addEventListener('click',fn);bulkBar.appendChild(b)}; mkBulk('\u2713 Enable All','color:var(--green)',()=>{state.sources.forEach(s=>s.enabled=true);saveState();renderAll();toast('All sources enabled','ok')}); mkBulk('\u2717 Disable All','color:var(--red)',()=>{state.sources.forEach(s=>s.enabled=false);saveState();renderAll();toast('All sources disabled','warn')}); mkBulk('\u2713 Enable OK Only','color:var(--green)',()=>{state.sources.forEach(s=>{s.enabled=!s.errorState});saveState();renderAll();toast('Enabled non-errored sources','ok')}); mkBulk('\u2717 Disable Errored','color:var(--amber)',()=>{state.sources.forEach(s=>{if(s.errorState) s.enabled=false});saveState();renderAll();toast('Disabled errored sources','warn')}); mkBulk('\u2717 Disable Empty','color:var(--amber)',()=>{state.sources.forEach(s=>{if(s.enabled&&!s.errorState&&s.lastFetched&&!(state.cache[s.id]||[]).length) s.enabled=false});saveState();renderAll();toast('Disabled empty sources','warn')}); mkBulk('\u21BB Sync All Enabled','background:var(--accent2);color:#fff',()=>syncAll()); panel.appendChild(bulkBar); // Filter pills const filterWrap=document.createElement('div');filterWrap.className='src-filter-pills'; const filters=[ {key:'all',label:'All',cls:'',count:total}, {key:'active',label:'Active',cls:'',count:enabled}, {key:'disabled',label:'Disabled',cls:'sf-disabled',count:total-enabled}, {key:'errors',label:'Errors',cls:'sf-errors',count:errored}, {key:'content',label:'Has Content',cls:'sf-content',count:withContent}, {key:'empty',label:'Empty',cls:'sf-empty',count:emptyS}, {key:'never',label:'Never Synced',cls:'',count:neverS}, {key:'default',label:'Built-in',cls:'',count:defaultS}, {key:'custom',label:'Custom',cls:'',count:customS} ]; filters.forEach(f=>{ const pill=document.createElement('span');pill.className='src-filter-pill '+f.cls+(srcFilter===f.key?' active':''); pill.textContent=f.label+(f.count?' ('+f.count+')':''); pill.addEventListener('click',()=>{panel._filter=f.key;renderSourcesPanel()}); filterWrap.appendChild(pill); }); panel.appendChild(filterWrap); // Filter sources let sources=[...state.sources]; if(srcSearch){const q=srcSearch.toLowerCase();sources=sources.filter(s=>s.name.toLowerCase().includes(q)||s.category.toLowerCase().includes(q)||(s.region||'').toLowerCase().includes(q)||(s.url||'').toLowerCase().includes(q))} if(srcFilter==='active') sources=sources.filter(s=>s.enabled); else if(srcFilter==='disabled') sources=sources.filter(s=>!s.enabled); else if(srcFilter==='errors') sources=sources.filter(s=>s.enabled&&s.errorState); else if(srcFilter==='content') sources=sources.filter(s=>(state.cache[s.id]||[]).length>0); else if(srcFilter==='empty') sources=sources.filter(s=>s.enabled&&!s.errorState&&s.lastFetched&&!(state.cache[s.id]||[]).length); else if(srcFilter==='never') sources=sources.filter(s=>!s.lastFetched); else if(srcFilter==='default') sources=sources.filter(s=>s.isDefault); else if(srcFilter==='custom') sources=sources.filter(s=>!s.isDefault); // Results count const resultInfo=document.createElement('div');resultInfo.style.cssText='font-size:.68rem;color:var(--txt3);margin-bottom:.4rem'; resultInfo.textContent='Showing '+sources.length+' of '+total+' sources'; panel.appendChild(resultInfo); // Build source row helper function buildRow(src){ const row=document.createElement('div');row.className='src-row'; // Status dot const statusCls=!src.enabled?'sr-off':src.errorState?'sr-err':((state.cache[src.id]||[]).length>0?'sr-ok':'sr-empty'); row.innerHTML=``; const icon=document.createElement('span');icon.className='sr-icon';icon.textContent=src.icon||'\u{1F310}';row.appendChild(icon); const name=document.createElement('span');name.className='sr-name';name.title=src.name+' \u2014 '+src.url;name.textContent=src.name;row.appendChild(name); const region=document.createElement('span');region.className='sr-region '+rgnCls(src.region);region.textContent=src.region||'US';row.appendChild(region); const items=document.createElement('span');items.className='sr-items';items.textContent=(state.cache[src.id]||[]).length+' items';row.appendChild(items); const sync=document.createElement('span');sync.className='sr-sync';sync.textContent=src.lastFetched?timeAgo(new Date(src.lastFetched)):'Never';row.appendChild(sync); if(src.errorState){const err=document.createElement('span');err.className='sr-error';err.title=src.errorState;err.textContent='\u26A0 '+src.errorState;row.appendChild(err)} const actions=document.createElement('span');actions.className='sr-actions'; // Toggle button const toggleBtn=document.createElement('button');toggleBtn.className='btn btn-sm'; toggleBtn.style.cssText=src.enabled?'color:var(--red)':'color:var(--green)'; toggleBtn.textContent=src.enabled?'Disable':'Enable'; toggleBtn.addEventListener('click',()=>{toggleSource(src.id);renderSourcesPanel()}); actions.appendChild(toggleBtn); // Sync button const syncBtn=document.createElement('button');syncBtn.className='btn btn-sm';syncBtn.textContent='\u21BB';syncBtn.title='Sync this source'; syncBtn.addEventListener('click',async()=>{syncBtn.textContent='\u23F3';await syncOne(src.id);renderSourcesPanel()}); actions.appendChild(syncBtn); // Edit button const editBtn=document.createElement('button');editBtn.className='btn btn-sm';editBtn.textContent='\u270E';editBtn.title='Edit source'; editBtn.addEventListener('click',()=>openEditModal(src.id)); actions.appendChild(editBtn); // Category move const catSel=document.createElement('select');catSel.className='sort-select';catSel.style.cssText='font-size:.62rem;padding:.1rem .2rem;max-width:85px'; catSel.innerHTML=''; const cats=[...new Set(state.sources.map(s=>s.category))].sort(); cats.forEach(c=>{if(c!==src.category){const o=document.createElement('option');o.value=c;o.textContent=c;catSel.appendChild(o)}}); catSel.addEventListener('change',()=>{if(!catSel.value) return;editSource(src.id,{category:catSel.value});toast(src.name+' \u2192 '+catSel.value,'ok');renderSourcesPanel()}); actions.appendChild(catSel); row.appendChild(actions); return row; } if(srcViewMode==='grouped'){ // Group by category const catOrder=[];const catMap={}; sources.forEach(src=>{const c=src.category||'Uncategorized';if(!catMap[c]){catMap[c]=[];catOrder.push(c)}catMap[c].push(src)}); catOrder.sort(); catOrder.forEach(catName=>{ const group=document.createElement('div');group.className='src-group'; const activeInGroup=catMap[catName].filter(s=>s.enabled).length; const errInGroup=catMap[catName].filter(s=>s.errorState).length; const head=document.createElement('div');head.className='src-group-head'; head.innerHTML=`\u25BC${esc(catName)}${catMap[catName].length} sources \u2022 ${activeInGroup} active${errInGroup?' \u2022 '+errInGroup+' errors':''}`; const sgActions=document.createElement('span');sgActions.className='sg-actions'; const enAll=document.createElement('button');enAll.className='btn btn-sm';enAll.style.cssText='font-size:.6rem;color:var(--green);padding:.12rem .3rem';enAll.textContent='\u2713 All';enAll.title='Enable all in this group'; enAll.addEventListener('click',e=>{e.stopPropagation();catMap[catName].forEach(s=>s.enabled=true);saveState();renderAll();toast(catName+': all enabled','ok')}); const disAll=document.createElement('button');disAll.className='btn btn-sm';disAll.style.cssText='font-size:.6rem;color:var(--red);padding:.12rem .3rem';disAll.textContent='\u2717 All';disAll.title='Disable all in this group'; disAll.addEventListener('click',e=>{e.stopPropagation();catMap[catName].forEach(s=>s.enabled=false);saveState();renderAll();toast(catName+': all disabled','warn')}); sgActions.appendChild(enAll);sgActions.appendChild(disAll); head.appendChild(sgActions); head.addEventListener('click',()=>{head.classList.toggle('collapsed')}); group.appendChild(head); const body=document.createElement('div');body.className='src-group-body'; catMap[catName].forEach(src=>body.appendChild(buildRow(src))); group.appendChild(body);panel.appendChild(group); }); }else{ // Flat list const list=document.createElement('div');list.style.cssText='background:var(--bg1);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden'; sources.forEach(src=>list.appendChild(buildRow(src))); if(!sources.length) list.innerHTML='

No sources match your filter.

'; panel.appendChild(list); } } // ── Health Bar ── function renderHealthBar(){ const bar=document.getElementById('healthBar');bar.innerHTML=''; const ok=state.sources.filter(s=>s.enabled&&!s.errorState&&s.lastFetched&&(state.cache[s.id]||[]).length>0).length; const empty=state.sources.filter(s=>s.enabled&&!s.errorState&&s.lastFetched&&!(state.cache[s.id]||[]).length).length; const failed=state.sources.filter(s=>s.enabled&&s.errorState).length; const disabled=state.sources.filter(s=>!s.enabled).length; const never=state.sources.filter(s=>s.enabled&&!s.lastFetched&&!s.errorState).length; const total=state.sources.length; bar.innerHTML=`Health:`; const mkChip=(cls,label,count,action)=>{const c=document.createElement('span');c.className='health-chip '+cls+(action?' clickable':'');c.innerHTML=`${count} ${label}`;if(action) c.addEventListener('click',action);bar.appendChild(c)}; mkChip('health-ok',`OK / ${total}`,ok); if(empty) mkChip('health-never','Empty',empty,()=>{document.querySelector('[data-status="empty"]')?.click();toast(`Showing ${empty} empty sources`,'info')}); mkChip('health-err','Failed',failed,failed?()=>{state.sources.forEach(s=>{if(s.errorState) s.enabled=false});saveState();renderAll();toast(`Disabled ${failed} failed sources`,'warn')}:null); mkChip('health-off','Disabled',disabled,()=>{document.querySelector('[data-status="all"]')?.click()}); if(never) mkChip('health-never','Pending',never); } // ── Health Dashboard Panel ── function renderHealthPanel(){ const panel=document.getElementById('healthDashPanel');if(!panel) return; const ok=state.sources.filter(s=>s.enabled&&!s.errorState&&s.lastFetched&&(state.cache[s.id]||[]).length>0); const empty=state.sources.filter(s=>s.enabled&&!s.errorState&&s.lastFetched&&!(state.cache[s.id]||[]).length); const failed=state.sources.filter(s=>s.enabled&&s.errorState); const disabled=state.sources.filter(s=>!s.enabled); const pending=state.sources.filter(s=>s.enabled&&!s.lastFetched&&!s.errorState); const total=state.sources.length; const pctOk=total?Math.round(ok.length/total*100):0; const pctFail=total?Math.round(failed.length/total*100):0; // Category breakdown const catMap={}; state.sources.forEach(s=>{ if(!catMap[s.category]) catMap[s.category]={ok:0,fail:0,empty:0,disabled:0,pending:0,total:0}; catMap[s.category].total++; if(!s.enabled) catMap[s.category].disabled++; else if(s.errorState) catMap[s.category].fail++; else if(!s.lastFetched) catMap[s.category].pending++; else if(!(state.cache[s.id]||[]).length) catMap[s.category].empty++; else catMap[s.category].ok++; }); let catRows=''; Object.keys(catMap).sort().forEach(cat=>{ const c=catMap[cat]; const pct=c.total?Math.round(c.ok/c.total*100):0; catRows+=` ${esc(cat)} ${c.ok} ${c.fail} ${c.empty} ${c.disabled} ${c.total}
${pct}%
`; }); // Failed sources detail let failRows=''; failed.forEach(s=>{ const ago=s.lastFetched?timeAgo(new Date(s.lastFetched)):'Never'; failRows+=` ${esc(s.icon||'')} ${esc(s.name)} ${esc(s.category)} ${esc(s.errorState||'Error')} ${ago} `; }); // Empty sources detail let emptyRows=''; empty.forEach(s=>{ const ago=s.lastFetched?timeAgo(new Date(s.lastFetched)):'Never'; emptyRows+=` ${esc(s.icon||'')} ${esc(s.name)} ${esc(s.category)} ${ago} `; }); panel.innerHTML=`

💚 Source Health Dashboard

${ok.length}
Healthy
${failed.length}
Failed
${empty.length}
Empty
${disabled.length}
Disabled
${pending.length}
Pending
${total}
Total
${pctOk}% OK
${failed.length?`

❌ Failed Sources (${failed.length})

${failRows}
SourceCategoryErrorLast SyncActions
`:''} ${empty.length?`

⚠ Empty Sources (${empty.length})

${emptyRows}
SourceCategoryLast SyncActions
`:''}

📊 Category Breakdown

${catRows}
CategoryOKFailedEmptyDisabledTotalHealth
`; } // ── Compact List View ── function renderListView(){ const lv=document.getElementById('listView');lv.innerHTML=''; const search=(document.getElementById('searchInput').value||'').toLowerCase(); const cat=getCat(),reg=getReg(),stat=getStatus(); let html=''; state.sources.forEach(src=>{ if(cat!=='all'&&src.category!==cat) return;if(reg!=='all'&&src.region!==reg) return; if(stat==='failed'&&!src.errorState) return;if(stat==='ok'&&(src.errorState||(!!(src.lastFetched)&&!(state.cache[src.id]||[]).length))) return; if(stat==='empty'){const items=(state.cache[src.id]||[]);if(src.errorState||!src.lastFetched||items.length>0) return} if(stat==='content'){const items=(state.cache[src.id]||[]);if(!items.length) return} if(search&&!src.name.toLowerCase().includes(search)&&!src.category.toLowerCase().includes(search)){const items=state.cache[src.id]||[];const hasMatch=items.some(it=>(it.title||'').toLowerCase().includes(search)||(it.description||'').toLowerCase().includes(search));if(!hasMatch) return} const statusCls=!src.enabled?'lt-off':src.errorState?'lt-err':'lt-ok'; const statusTxt=!src.enabled?'Off':src.errorState?'Err':'OK'; const itemCount=(state.cache[src.id]||[]).length; const lastSync=src.lastFetched?timeAgo(new Date(src.lastFetched)):'Never'; html+=``; html+=``; html+=``; html+=``; html+=``; html+=``; }); html+='
StatusSourceCategoryRegionItemsLast SyncActions
${esc(src.icon)} ${esc(src.name)}${esc(src.category)}${esc(src.region)}${itemCount}${lastSync}
'; lv.innerHTML=html; } // Track which items have already shown the glow animation (bounded to prevent memory growth) const _glowedItems=new Set(); function markGlowed(link){_glowedItems.add(link);if(_glowedItems.size>2000){const arr=[..._glowedItems];_glowedItems.clear();arr.slice(-1000).forEach(l=>_glowedItems.add(l))}} // Shared lazy-load observer for cards (one instance, reused across renders) let _lazyCardObs; if(typeof IntersectionObserver!=='undefined'){ _lazyCardObs=new IntersectionObserver((entries)=>{ entries.forEach(entry=>{ if(entry.isIntersecting&&entry.target._lazyRender){ entry.target._lazyRender(); delete entry.target._lazyRender; _lazyCardObs.unobserve(entry.target); } }); },{rootMargin:'200px'}); } function renderCards(){ // Disconnect previous lazy observations before rebuilding grid if(_lazyCardObs) _lazyCardObs.disconnect(); const g=document.getElementById('cardGrid');g.innerHTML='';const searchRaw=document.getElementById('searchInput').value||'';const search=searchRaw.toLowerCase();const searchExpr=parseSearchExpr(searchRaw);const cat=getCat(),reg=getReg(),stat=getStatus();let totalSearchHits=0; // Single-pass pill counts (replaces 3 separate .filter() loops) let failedCount=0,emptyCount=0,contentCount=0; state.sources.forEach(s=>{if(!s.enabled) return;if(s.errorState){failedCount++;return}const items=(state.cache[s.id]||[]);if(items.length>0){contentCount++}else if(s.lastFetched){emptyCount++}}); const failPill=document.querySelector('[data-status="failed"]'); const emptyPill=document.querySelector('[data-status="empty"]'); if(failPill) failPill.textContent=`Errors${failedCount?` (${failedCount})`:''}`; if(emptyPill) emptyPill.textContent=`Empty${emptyCount?` (${emptyCount})`:''}`; const contentPill=document.querySelector('[data-status="content"]'); if(contentPill) contentPill.textContent=`Has Content${contentCount?` (${contentCount})`:''}`; // Group sources by category for section headers const filtered=state.sources.filter(src=>{ if(cat!=='all'&&src.category!==cat) return false;if(reg!=='all'&&src.region!==reg) return false; if(stat==='failed'&&!src.errorState) return false; if(stat==='ok'&&(src.errorState||(!!(src.lastFetched)&&!(state.cache[src.id]||[]).length))) return false; if(stat==='empty'){const items=(state.cache[src.id]||[]);if(src.errorState||!src.lastFetched||items.length>0) return false} if(stat==='content'){const items=(state.cache[src.id]||[]);if(!items.length) return false} return true; }); // Sort const sortMode=document.getElementById('cardSort')?.value||'default'; if(sortMode==='name') filtered.sort((a,b)=>a.name.localeCompare(b.name)); else if(sortMode==='name-desc') filtered.sort((a,b)=>b.name.localeCompare(a.name)); else if(sortMode==='items-desc') filtered.sort((a,b)=>(state.cache[b.id]||[]).length-(state.cache[a.id]||[]).length); else if(sortMode==='items-asc') filtered.sort((a,b)=>(state.cache[a.id]||[]).length-(state.cache[b.id]||[]).length); else if(sortMode==='recent'){const _tsCache=new Map();const _ts=s=>{if(!_tsCache.has(s.id)) _tsCache.set(s.id,new Date(s.lastFetched||0).getTime());return _tsCache.get(s.id)};filtered.sort((a,b)=>_ts(b)-_ts(a))} else if(sortMode==='errors') filtered.sort((a,b)=>(b.errorState?1:0)-(a.errorState?1:0)); else if(sortMode==='new') filtered.sort((a,b)=>newCount(b.id)-newCount(a.id)); const CAT_PRIORITY={'Breaking':0,'News':1,'Cybersecurity':2,'Regulatory':3,'Central Banks':4,'Defense':5,'Law Enforcement':6,'Government':7,'Fund Flows':8,'Politics':9,'Energy':10,'Maritime':11,'Agriculture':12,'Insurance':13,'Sports':14,'College':15,'YouTube':16,'Social':17,'Social - X':17,'Social - Truth':17,'Social - Facebook':17,'Social - Instagram':17,'Social - Pinterest':17,'Junk Media':99}; const catOrder=[];const catMap={}; filtered.forEach(src=>{ const c=src.category||'Uncategorized'; if(!catMap[c]){catMap[c]=[];catOrder.push(c)} catMap[c].push(src); }); catOrder.sort((a,b)=>(CAT_PRIORITY[a]??50)-(CAT_PRIORITY[b]??50)); // Populate jump dropdown const jumpSel=document.getElementById('jumpCategory'); jumpSel.innerHTML=''; catOrder.forEach(c=>{const o=document.createElement('option');o.value='cat-'+c.replace(/\s+/g,'-');o.textContent=c+' ('+catMap[c].length+')';jumpSel.appendChild(o)}); // Render grouped catOrder.forEach(catName=>{ // Category header const hdr=document.createElement('div');hdr.className='cat-group-header'+(collapsedCats.has(catName)?' collapsed':'');hdr.setAttribute('role','button');hdr.setAttribute('aria-expanded',String(!collapsedCats.has(catName))); hdr.id='cat-'+catName.replace(/\s+/g,'-'); hdr.innerHTML=`${esc(catName)}${catMap[catName].length} sources\u25BC`; hdr.addEventListener('click',()=>{ if(collapsedCats.has(catName)) collapsedCats.delete(catName);else collapsedCats.add(catName); hdr.classList.toggle('collapsed'); // Toggle visibility of cards in this category hdr.nextElementSibling&&g.querySelectorAll(`.card[data-cat="${CSS.escape(catName)}"]`).forEach(c=>c.style.display=collapsedCats.has(catName)?'none':''); }); const catFrag=document.createDocumentFragment(); catMap[catName].forEach(src=>{ const card=document.createElement('div');card.className='card'+(src.enabled?'':' disabled')+(collapsedCards.has(src.id)?' collapsed':'');card.dataset.id=src.id;card.dataset.cat=src.category||'Uncategorized';card.draggable=true; if(collapsedCats.has(src.category)) card.style.display='none'; const nc=newCount(src.id); const _cardBodyId='cb-'+src.id; const head=document.createElement('div');head.className='card-head';head.setAttribute('role','button');head.setAttribute('aria-controls',_cardBodyId);head.tabIndex=0; const _rel=(state.sourceReliability||{})[src.id];const _relScore=_rel?_rel.score:50;const _relColor=_relScore>=75?'var(--green)':_relScore>=50?'var(--amber)':_relScore>=25?'var(--red)':'var(--txt3)'; head.innerHTML=`\u25BC${esc(src.icon||'\u{1F310}')}${esc(src.name)}${nc?`${nc} new`:''}${_rel?`${_relScore}`:''}${esc(src.region||'US')}${esc(src.category)}
${src.category==='Junk Media'?``:``}${src.isDefault?'':``}
`; head.setAttribute('aria-expanded',String(!collapsedCards.has(src.id))); const _toggleCard=()=>{if(collapsedCards.has(src.id)) collapsedCards.delete(src.id);else collapsedCards.add(src.id);card.classList.toggle('collapsed');head.setAttribute('aria-expanded',String(!collapsedCards.has(src.id)))}; head.addEventListener('click',e=>{if(e.target.closest('.card-menu')) return;_toggleCard()}); head.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();_toggleCard()}}); card.appendChild(head); if(src.errorState){const err=document.createElement('div');err.className='card-error';err.textContent='\u26A0 '+src.errorState;card.appendChild(err)} const body=document.createElement('div');body.className='card-body';body.id=_cardBodyId; const items=state.cache[src.id]||[];let vis=0; // Lazy-render: defer feed item rendering until card scrolls into view let _bodyRendered=false; const _renderBody=()=>{ if(_bodyRendered) return;_bodyRendered=true; body.innerHTML=''; items.forEach(it=>{ // Skip placeholder items from failed social bridges if((it.description||'').startsWith('Direct link')&&it.title===src.name) return; if(searchExpr){const txt=(it.title||'')+' '+(it.description||'');if(!searchMatches(searchExpr,txt)) return}vis++;totalSearchHits++; const _isNewItem=isNew(it); const _shouldGlow=_isNewItem&&!_glowedItems.has(it.link); const div=document.createElement('div');div.className='feed-item'+(_isNewItem?' new-item':'')+(_shouldGlow?' fresh-glow':''); if(_shouldGlow) markGlowed(it.link); div.setAttribute('role','article'); const td=document.createElement('div');td.className='feed-item-title';td.dataset.link=it.link; const star=document.createElement('span');star.className='feed-item-star'+(isBM(it.link)?' starred':'');star.textContent=isBM(it.link)?'\u2605':'\u2606';star.dataset.bmLink=it.link;star.dataset.bmTitle=it.title||'';star.dataset.bmPubDate=it.pubDate||'';star.dataset.bmSource=it.source||src.name;td.appendChild(star); const lk=document.createElement('a');lk.href=(it.linkStatus==='wayback'&&it.waybackLink)?it.waybackLink:it.link;lk.target='_blank';lk.rel='noopener'; if(searchExpr){lk.innerHTML=highlightText(it.title||'(untitled)',searchExpr)}else{lk.textContent=it.title||'(untitled)'}td.appendChild(lk); if(it.linkStatus==='wayback'||isPW(it.link)){const wb=document.createElement('span');wb.className='wb-badge';wb.textContent='WB';wb.dataset.wbLink=it.link;td.appendChild(wb)} const cpBtn=document.createElement('button');cpBtn.className='share-btn';cpBtn.textContent='\u{1F4CB}';cpBtn.title='Copy link';cpBtn.dataset.cpLink=it.link;cpBtn.dataset.cpTitle=it.title||'';td.appendChild(cpBtn); div.appendChild(td); const meta=document.createElement('div');meta.className='feed-item-meta';if(it.pubDate){const d=new Date(it.pubDate);if(!isNaN(d)) meta.textContent=timeAgo(d)} const rt=estReadTime(it.description||it.title);const rtWords=((it.description||it.title)||'').split(/\s+/).length;if(rt&&rtWords>200){const rtBadge=document.createElement('span');rtBadge.className='read-time';rtBadge.textContent=rt;meta.appendChild(rtBadge)} div.appendChild(meta); if(it.description){const desc=document.createElement('div');desc.className='feed-item-desc';if(searchExpr){desc.innerHTML=highlightText(it.description.substring(0,180),searchExpr)}else{desc.textContent=it.description.substring(0,180)}div.appendChild(desc)} body.appendChild(div); }); }; // end _renderBody // For search mode or first 12 cards, render immediately (above fold) const _cardIdx=catFrag.childNodes.length; if(searchExpr||_cardIdx<12){_renderBody()} else{ // Lazy-render: use shared IntersectionObserver to render when card enters viewport body.innerHTML='
Scroll to load...
'; card._lazyRender=_renderBody; _lazyCardObs.observe(card); } card.appendChild(body); if(searchExpr&&vis===0){return} const _itemCount=items.filter(it=>!((it.description||'').startsWith('Direct link')&&it.title===src.name)).length; const foot=document.createElement('div');foot.className='card-foot';foot.innerHTML=`${_bodyRendered?vis:_itemCount} item${(_bodyRendered?vis:_itemCount)!==1?'s':''}${buildSparklineSVG(src.id)}${src.lastFetched?timeAgo(new Date(src.lastFetched)):'Not synced'}`; if(searchExpr&&vis>0) card.classList.add('search-match-card'); card.appendChild(foot);catFrag.appendChild(card); }); if(!searchExpr||catFrag.childNodes.length>0){g.appendChild(hdr);while(catFrag.firstChild) g.appendChild(catFrag.firstChild)} }); // Update search stats const statsEl=document.getElementById('searchStats'); if(searchExpr&&searchRaw){statsEl.style.display='';statsEl.textContent=totalSearchHits+' match'+(totalSearchHits!==1?'es':'')} else{statsEl.style.display='none'} // Rebuild alpha sidebar after cards render if(typeof window._afterRenderCards==='function') window._afterRenderCards(); } function renderTimeline(){ const tl=document.getElementById('timelineView');tl.innerHTML='';const searchRaw=document.getElementById('searchInput').value||'';const searchExpr=parseSearchExpr(searchRaw);const cat=getCat(),reg=getReg(); const _tlFutureLimit=Date.now()+2*3600000; let all=[];state.sources.forEach(src=>{if(!src.enabled) return;if(cat!=='all'&&src.category!==cat) return;if(reg!=='all'&&src.region!==reg) return;(state.cache[src.id]||[]).forEach(it=>{if((it.description||'').startsWith('Direct link')&&it.title===src.name) return;if(it.pubDate){const pd=new Date(it.pubDate).getTime();if(pd>_tlFutureLimit) return}if(searchExpr){const txt=(it.title||'')+' '+(it.description||'');if(!searchMatches(searchExpr,txt)) return}all.push({...it,_sn:src.name,_si:src.icon})})}); all.sort((a,b)=>new Date(b.pubDate||0)-new Date(a.pubDate||0)); all.slice(0,200).forEach(it=>{const div=document.createElement('div');div.className='tl-item'+(isNew(it)?' new-item':'');const dt=document.createElement('span');dt.className='tl-date';dt.textContent=it.pubDate?fmtDate(new Date(it.pubDate)):'';const sr=document.createElement('span');sr.className='tl-source';sr.textContent=(it._si||it.sourceIcon||'')+' '+(it._sn||it.sourceName);const ct=document.createElement('div');ct.className='tl-content';const star=document.createElement('span');star.className='feed-item-star'+(isBM(it.link)?' starred':'');star.textContent=isBM(it.link)?'\u2605':'\u2606';star.style.cssText='margin-right:.3rem;cursor:pointer';star.addEventListener('click',()=>toggleBM(it));ct.appendChild(star);const a=document.createElement('a');a.href=(it.linkStatus==='wayback'&&it.waybackLink)?it.waybackLink:it.link;a.target='_blank';a.rel='noopener';if(searchExpr){a.innerHTML=highlightText(it.title,searchExpr)}else{a.textContent=it.title}ct.appendChild(a);if(it.linkStatus==='wayback'||isPW(it.link)){const wb=document.createElement('span');wb.className='wb-badge';wb.textContent=' WB';ct.appendChild(wb)}const tlCp=document.createElement('button');tlCp.className='share-btn';tlCp.textContent='\u{1F4CB}';tlCp.title='Copy link';tlCp.addEventListener('click',e=>{e.stopPropagation();copyLink(it.link,it.title,tlCp)});ct.appendChild(tlCp);if(it.description){const p=document.createElement('p');if(searchExpr){p.innerHTML=highlightText(it.description.substring(0,140),searchExpr)}else{p.textContent=it.description.substring(0,140)}ct.appendChild(p)}div.appendChild(dt);div.appendChild(sr);div.appendChild(ct);tl.appendChild(div)}); if(!all.length) tl.innerHTML='

No items. Sync first.

'; } function renderBookmarks(){ const p=document.getElementById('bookmarksPanel');p.innerHTML=''; if(!state.bookmarks.length){p.innerHTML='

No bookmarks. Star items to save.

';return} const h=document.createElement('h3');h.style.cssText='font-size:.88rem;margin-bottom:.5rem;color:var(--txt2)';h.textContent=`${state.bookmarks.length} Saved`;p.appendChild(h); [...state.bookmarks].reverse().forEach(bm=>{const div=document.createElement('div');div.className='bm-item';const a=document.createElement('a');a.href=isPW(bm.link)?wbUrl(bm.link):bm.link;a.target='_blank';a.rel='noopener';a.textContent=bm.title;const meta=document.createElement('span');meta.className='bm-meta';meta.textContent=(bm.source?bm.source+' \u2022 ':'')+timeAgo(new Date(bm.savedAt||bm.pubDate));const rm=document.createElement('span');rm.className='bm-remove';rm.textContent='\u2716';rm.addEventListener('click',()=>toggleBM(bm));const bmCp=document.createElement('button');bmCp.className='share-btn';bmCp.textContent='\u{1F4CB}';bmCp.title='Copy link';bmCp.addEventListener('click',e=>{e.stopPropagation();copyLink(bm.link,bm.title,bmCp)});div.appendChild(a);div.appendChild(bmCp);div.appendChild(meta);div.appendChild(rm);p.appendChild(div)}); } function renderStatusBar(){ const en=state.sources.filter(s=>s.enabled);const total=Object.values(state.cache).reduce((n,a)=>n+(a?.length||0),0); document.getElementById('statusSources').textContent=`${en.length}/${state.sources.length} sources`; document.getElementById('statusItems').textContent=`${total} items`; document.getElementById('statusSync').textContent=state.settings.lastGlobalSync?'Synced '+timeAgo(new Date(state.settings.lastGlobalSync)):'Never synced'; const badge=document.getElementById('sourceCountBadge'); if(badge) badge.textContent=`(${en.length} active / ${state.sources.length} total)`; } function renderStatsBar(){ const bar=document.getElementById('statsBar');bar.innerHTML='';const rc={};let tn=0; state.sources.forEach(src=>{if(!src.enabled) return;const r=src.region||'US';rc[r]=(rc[r]||0)+(state.cache[src.id]?.length||0);tn+=newCount(src.id)}); if(tn){const c=document.createElement('span');c.className='stat-chip';c.style.cursor='pointer';c.title='Mark all read';c.innerHTML=` ${tn} new`;c.addEventListener('click',markAllSeen);bar.appendChild(c)} Object.entries(rc).sort((a,b)=>b[1]-a[1]).slice(0,8).forEach(([r,c])=>{const ch=document.createElement('span');ch.className='stat-chip';ch.innerHTML=` ${esc(r)} ${c}`;bar.appendChild(ch)}); const bm=document.createElement('span');bm.className='stat-chip';bm.innerHTML=` ${state.bookmarks.length} saved`;bar.appendChild(bm); } const REGION_COLORS={'US':'var(--region-us)','Europe':'var(--region-eu)','Asia-Pacific':'var(--region-ap)','Latin America':'var(--region-la)','Middle East / Africa':'var(--region-me)','Global':'var(--region-global)'}; function renderCatPills(){const c=document.getElementById('categoryPills');const catCounts={};state.sources.forEach(s=>{if(!catCounts[s.category]) catCounts[s.category]=0;if(s.enabled) catCounts[s.category]++});const cats=Object.keys(catCounts).sort();const a=getCat();c.innerHTML='';mkPill(c,'cat','all','All',a==='all');cats.forEach(x=>mkPill(c,'cat',x,x,a===x,catCounts[x]))} function renderRegPills(){const c=document.getElementById('regionPills');const regCounts={},regExists=new Set();state.sources.forEach(s=>{if(s.region) regExists.add(s.region);if(s.enabled&&s.region){regCounts[s.region]=(regCounts[s.region]||0)+1}});const a=getReg();c.innerHTML='';mkPill(c,'region','all','All',a==='all');state.regions.forEach(r=>{if((regCounts[r]||0)||regExists.has(r)) mkPill(c,'region',r,r,a===r,regCounts[r]||0)})} function mkPill(c,dataKey,v,l,a,n){const p=document.createElement('span');p.className='pill'+(dataKey==='region'?' region-pill':'')+(a?' active':'');p.dataset[dataKey]=v;p.textContent=l;if(n!=null){const s=document.createElement('span');s.className='pill-count';s.textContent=`(${n})`;p.appendChild(s)}c.appendChild(p)} function updateStatus(m,l){document.getElementById('statusText').textContent=m;const dot=document.getElementById('statusDot');dot.className='dot '+({ok:'dot-ok',warn:'dot-warn',err:'dot-err'}[l]||'dot-ok');dot.setAttribute('title',{ok:'OK',warn:'Warning',err:'Error'}[l]||'OK')} function toast(m,t='info'){const c=document.getElementById('toastContainer');const e=document.createElement('div');e.className='toast toast-'+(t||'info'); // OPSEC: sanitize URLs from error messages const clean=typeof m==='string'?m.replace(/https?:\/\/[^\s)]+/g,'[URL]'):m; e.textContent=clean;c.appendChild(e);setTimeout(()=>{e.style.animation='toastOut .3s forwards';setTimeout(()=>e.remove(),300)},3000)} function esc(s){return(s||'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''')} function safeUrl(u){if(!u) return'#';const s=(u||'').trim().toLowerCase();if(s.startsWith('javascript:')||s.startsWith('data:')||s.startsWith('vbscript:')) return'#';return u} // ── Search highlighting & expression parsing ── function parseSearchExpr(raw){ if(!raw) return null; raw=raw.trim(); // Regex mode: /pattern/flags if(raw.startsWith('/')&&raw.lastIndexOf('/')>0){ const lastSlash=raw.lastIndexOf('/'); const pattern=raw.substring(1,lastSlash); const flags=raw.substring(lastSlash+1)||'i'; try{if(pattern.length>100) return null;return{regex:new RegExp(pattern,flags),type:'regex'}}catch{return null} } // OR mode: term1 | term2 | term3 if(raw.includes('|')){ const terms=raw.split('|').map(t=>t.trim()).filter(Boolean); if(terms.length){const pattern=terms.map(t=>t.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join('|');return{regex:new RegExp('('+pattern+')','gi'),type:'or',terms}} } // AND mode: term1 + term2 if(raw.includes('+')){ const terms=raw.split('+').map(t=>t.trim()).filter(Boolean); if(terms.length>1) return{terms,type:'and',regex:new RegExp('('+terms.map(t=>t.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join('|')+')','gi')} } // Plain text (case insensitive) const escaped=raw.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'); return{regex:new RegExp('('+escaped+')','gi'),type:'plain'}; } function searchMatches(expr,text){ if(!expr||!text) return false; if(expr.type==='and') return expr.terms.every(t=>text.toLowerCase().includes(t.toLowerCase())); expr.regex.lastIndex=0; return expr.regex.test(text); } function highlightText(text,expr){ if(!expr||!text||!expr.regex) return esc(text); // Reset regex lastIndex expr.regex.lastIndex=0; return esc(text).replace(new RegExp(expr.regex.source,expr.regex.flags),m=>''+m+''); } async function copyLink(url,title,btn){ // Try native share sheet first (mobile) if(navigator.share){ try{await navigator.share({title:title||'',url});toast('Shared!','ok');return}catch(e){if(e.name==='AbortError') return} } const text=title?title+'\n'+url:url; navigator.clipboard.writeText(text).then(()=>{ if(btn){btn.textContent='\u2713';btn.classList.add('copied');setTimeout(()=>{btn.textContent='\u{1F4CB}';btn.classList.remove('copied')},1500)} toast('Link copied!','ok'); }).catch(()=>{ // Fallback for older browsers / non-HTTPS const ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');ta.remove(); if(btn){btn.textContent='\u2713';btn.classList.add('copied');setTimeout(()=>{btn.textContent='\u{1F4CB}';btn.classList.remove('copied')},1500)} toast('Link copied!','ok'); }); } const MONTHS=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; function timeAgo(d){if(!d||isNaN(d)) return'';const s=Math.floor((Date.now()-d.getTime())/1000);if(s<0) return'just now';if(s<60) return'just now';if(s<3600) return Math.floor(s/60)+'m ago';if(s<86400) return Math.floor(s/3600)+'h ago';return Math.floor(s/86400)+'d ago'} function fmtDate(d){if(!d||isNaN(d)) return'';const yr=d.getFullYear()!==new Date().getFullYear()?' '+d.getFullYear():'';return`${MONTHS[d.getMonth()]} ${d.getDate()}${yr}, ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`} let _modalTrigger=null; function openModal(id){ _modalTrigger=document.activeElement; const m=document.getElementById(id);m.classList.add('open'); // Focus first focusable element inside modal const focusable=m.querySelector('input:not([type=hidden]),select,textarea,button:not([data-close])'); if(focusable) setTimeout(()=>focusable.focus(),50); // Trap focus within modal m._trapFocus=e=>{ if(e.key!=='Tab') return; const els=m.querySelectorAll('input,select,textarea,button,[tabindex]:not([tabindex="-1"])'); if(!els.length) return; const first=els[0],last=els[els.length-1]; if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus()} else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus()} }; m.addEventListener('keydown',m._trapFocus); } function closeModal(id){ const m=document.getElementById(id);m.classList.remove('open'); if(m._trapFocus){m.removeEventListener('keydown',m._trapFocus);m._trapFocus=null} if(_modalTrigger){try{_modalTrigger.focus()}catch(e){}_modalTrigger=null} } // ═══════════════════════════════════════════════════════════════════════════════ // L: Region Manager // ═══════════════════════════════════════════════════════════════════════════════ function openRegionMgr(){ const body=document.getElementById('regionModalBody');body.innerHTML=''; state.regions.forEach(r=>{const cnt=state.sources.filter(s=>s.region===r);const en=cnt.filter(s=>s.enabled).length;const div=document.createElement('div');div.style.cssText='display:flex;align-items:center;gap:.4rem;padding:.35rem 0;border-bottom:1px solid var(--border)';div.innerHTML=`${esc(r)}${en}/${cnt.length}`; const on=document.createElement('button');on.className='btn btn-sm';on.textContent='Enable';on.addEventListener('click',()=>{bulkToggleRegion(r,true);openRegionMgr()}); const off=document.createElement('button');off.className='btn btn-sm';off.textContent='Disable';off.addEventListener('click',()=>{bulkToggleRegion(r,false);openRegionMgr()}); div.appendChild(on);div.appendChild(off); if(!BUILT_IN_REGIONS.includes(r)){const del=document.createElement('button');del.className='btn btn-sm btn-danger';del.textContent='\u2716';del.addEventListener('click',()=>{state.regions=state.regions.filter(x=>x!==r);state.sources.forEach(s=>{if(s.region===r) s.region='Global'});saveState();openRegionMgr();renderAll()});div.appendChild(del)} body.appendChild(div)}); const addDiv=document.createElement('div');addDiv.style.cssText='margin-top:.6rem;display:flex;gap:.35rem';const inp=document.createElement('input');inp.placeholder='New region...';inp.style.flex='1';const addBtn=document.createElement('button');addBtn.className='btn btn-accent btn-sm';addBtn.textContent='+ Add';addBtn.addEventListener('click',()=>{const v=inp.value.trim();if(v&&!state.regions.includes(v)){state.regions.push(v);saveState();openRegionMgr();renderAll()}});addDiv.appendChild(inp);addDiv.appendChild(addBtn);body.appendChild(addDiv);openModal('regionModal'); } function popRegionSel(){const sel=document.getElementById('srcRegion');sel.innerHTML='';state.regions.forEach(r=>{const o=document.createElement('option');o.value=r;o.textContent=r;sel.appendChild(o)})} // ═══════════════════════════════════════════════════════════════════════════════ // M: Delete Manager // ═══════════════════════════════════════════════════════════════════════════════ function openDeleteMgr(){ const body=document.getElementById('deleteModalBody');body.innerHTML=''; const groups={};state.sources.forEach(s=>{const k=s.region||'US';if(!groups[k]) groups[k]=[];groups[k].push(s)}); Object.keys(groups).sort().forEach(region=>{ const hdr=document.createElement('div');hdr.style.cssText='font-weight:700;font-size:.8rem;margin-top:.6rem;margin-bottom:.25rem;color:var(--accent)';hdr.textContent=region;body.appendChild(hdr); groups[region].forEach(s=>{ const div=document.createElement('div');div.className='del-item'; div.innerHTML=`${esc(s.category)}${s.enabled?'ON':'OFF'}`; body.appendChild(div); }); }); openModal('deleteModal'); } // ═══════════════════════════════════════════════════════════════════════════════ // N: Event Handlers & Init // ═══════════════════════════════════════════════════════════════════════════════ document.querySelectorAll('[data-close]').forEach(b=>b.addEventListener('click',()=>b.closest('.modal-overlay').classList.remove('open'))); document.addEventListener('click',e=>{if(!e.target.closest('.card-menu')) document.querySelectorAll('.card-dropdown.open').forEach(d=>d.classList.remove('open'))}); document.getElementById('cardGrid').addEventListener('click',e=>{ const mb=e.target.closest('.card-menu-btn');if(mb){const dd=mb.nextElementSibling;document.querySelectorAll('.card-dropdown.open').forEach(d=>{if(d!==dd) d.classList.remove('open')});dd.classList.toggle('open');return} const ab=e.target.closest('[data-action]');if(!ab) return;const act=ab.dataset.action,id=ab.dataset.id; document.querySelectorAll('.card-dropdown.open').forEach(d=>d.classList.remove('open')); switch(act){case 'sync':syncOne(id);break;case 'toggle':toggleSource(id);break;case 'edit':openEditModal(id);break;case 'delete':showConfirm('Delete?','Cannot undo.',()=>deleteSource(id));break;case 'mark-seen':markSeen(id);saveState();renderAll();toast('Marked read','ok');break;case 'wayback-all':(state.cache[id]||[]).slice(0,10).forEach(it=>window.open(wbUrl(it.link),'_blank'));break;case 'promote':{const s=state.sources.find(x=>x.id===id);if(s){editSource(id,{category:'News'});toast(s.name+' promoted to News','ok')}break}case 'demote':{const s=state.sources.find(x=>x.id===id);if(s){editSource(id,{category:'Junk Media'});toast(s.name+' moved to Junk Media','ok')}break}} }); document.getElementById('btnSync').addEventListener('click',()=>syncAll()); document.getElementById('btnAdd').addEventListener('click',()=>{editingSourceId=null;popRegionSel();document.getElementById('sourceModalTitle').textContent='Add Data Source';['srcName','srcUrl','srcCategory','srcIcon'].forEach(id=>document.getElementById(id).value='');document.getElementById('srcType').value='rss';document.getElementById('srcRegion').value='US';document.getElementById('srcEnabled').checked=true;openModal('sourceModal')}); document.getElementById('btnSaveSource').addEventListener('click',()=>{const name=document.getElementById('srcName').value.trim(),url=document.getElementById('srcUrl').value.trim();if(!name||!url){toast('Name and URL required','err');return}const cfg={name,url,type:document.getElementById('srcType').value,category:document.getElementById('srcCategory').value.trim()||'Custom',region:document.getElementById('srcRegion').value||'US',icon:document.getElementById('srcIcon').value.trim()||'\u{1F310}',enabled:document.getElementById('srcEnabled').checked};if(editingSourceId) editSource(editingSourceId,cfg);else addSource(cfg);closeModal('sourceModal')}); function openEditModal(id){const s=state.sources.find(x=>x.id===id);if(!s) return;editingSourceId=id;popRegionSel();document.getElementById('sourceModalTitle').textContent='Edit Source';document.getElementById('srcName').value=s.name;document.getElementById('srcUrl').value=s.url;document.getElementById('srcType').value=s.type;document.getElementById('srcCategory').value=s.category;document.getElementById('srcIcon').value=s.icon;document.getElementById('srcRegion').value=s.region||'US';document.getElementById('srcEnabled').checked=s.enabled;openModal('sourceModal')} document.getElementById('btnRegions').addEventListener('click',openRegionMgr); document.getElementById('btnDelete').addEventListener('click',openDeleteMgr); // ── Test URL feature ── let lastTestedUrl=''; document.getElementById('btnTestUrl').addEventListener('click',()=>{ document.getElementById('testUrlInput').value=''; document.getElementById('testUrlResult').innerHTML=''; document.getElementById('btnTestAddSource').style.display='none'; openModal('testUrlModal'); }); document.getElementById('btnRunTest').addEventListener('click',async()=>{ const url=document.getElementById('testUrlInput').value.trim(); if(!url){toast('Enter a URL','err');return} lastTestedUrl=url; const res=document.getElementById('testUrlResult'); res.innerHTML='
Testing...
'; const results=[]; // Test all CORS proxies const proxyNames=['allorigins','codetabs','corsproxy.io']; for(let i=0;i{ const color=r.status==='OK'?'var(--green)':'var(--red)'; html+=`${r.proxy}${r.status}${r.count||r.error||'--'}`; if(r.status==='OK'&&r.items&&(!bestItems||r.items.length>bestItems.length)) bestItems=r.items; }); html+=''; if(bestItems&&bestItems.length){ html+='
Preview (first 5 items):
'; bestItems.slice(0,5).forEach(it=>{ html+=`
${esc(it.title)}
${esc(it.pubDate||'')} ${esc((it.description||'').substring(0,100))}
`; }); document.getElementById('btnTestAddSource').style.display=''; } res.innerHTML=html; }); document.getElementById('btnTestAddSource').addEventListener('click',()=>{ closeModal('testUrlModal'); editingSourceId=null;popRegionSel(); document.getElementById('sourceModalTitle').textContent='Add Data Source'; document.getElementById('srcUrl').value=lastTestedUrl; document.getElementById('srcName').value='';document.getElementById('srcCategory').value=''; document.getElementById('srcIcon').value='';document.getElementById('srcType').value='rss'; document.getElementById('srcRegion').value='US';document.getElementById('srcEnabled').checked=true; openModal('sourceModal'); }); document.getElementById('btnDeleteSelected').addEventListener('click',()=>{const cbs=document.querySelectorAll('.del-cb:checked');if(!cbs.length){toast('Nothing selected','warn');return}showConfirm(`Delete ${cbs.length} sources?`,'Cannot undo.',()=>{cbs.forEach(cb=>{const id=cb.dataset.id;const i=state.sources.findIndex(x=>x.id===id);if(i>=0){state.sources.splice(i,1);delete state.cache[id]}});saveState();renderAll();closeModal('deleteModal');toast(`Deleted ${cbs.length} sources`,'warn')})}); document.getElementById('btnTheme').addEventListener('click',toggleTheme); // ── Notification toggle ── document.getElementById('btnNotify').addEventListener('click',async()=>{ if(notificationsEnabled){notificationsEnabled=false;toast('Notifications disabled','info');document.getElementById('btnNotify').style.color=''} else{const ok=await enableNotifications();if(ok){toast('Notifications enabled! You\'ll be alerted on keyword matches & high-alert items.','ok');document.getElementById('btnNotify').style.color='var(--green)'}} }); // ── Sound toggle ── document.getElementById('btnSound').addEventListener('click',()=>{ soundEnabled=!soundEnabled; document.getElementById('btnSound').textContent=soundEnabled?'🔊':'🔈'; document.getElementById('btnSound').style.color=soundEnabled?'var(--green)':''; if(soundEnabled) playAlertSound(); // test chime toast(soundEnabled?'Sound alerts on':'Sound alerts off','info'); }); // ── Focus mode ── document.getElementById('btnFocus').addEventListener('click',toggleFocusMode); // ── Intel Notes ── document.getElementById('btnNotes').addEventListener('click',()=>{renderNotesList();openModal('notesModal')}); document.getElementById('btnAddNote').addEventListener('click',()=>{ const inp=document.getElementById('noteInput');const text=inp.value.trim();if(!text) return; const notes=loadNotes();notes.unshift({text,ts:new Date().toISOString()});saveNotes(notes);inp.value='';renderNotesList();toast('Note added','ok'); }); document.getElementById('noteInput').addEventListener('keydown',e=>{if(e.key==='Enter') document.getElementById('btnAddNote').click()}); document.getElementById('btnExportNotes').addEventListener('click',()=>{ const notes=loadNotes();if(!notes.length){toast('No notes','warn');return} let text='INTEL NOTES EXPORT\n'+'='.repeat(40)+'\n\n'; notes.forEach(n=>{text+=`[${new Date(n.ts).toLocaleString()}] ${n.text}\n\n`}); const blob=new Blob([text],{type:'text/plain'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='intel-notes-'+new Date().toISOString().slice(0,10)+'.txt';a.click();URL.revokeObjectURL(a.href);toast('Notes exported','ok'); }); document.getElementById('btnClearNotes').addEventListener('click',()=>{if(!confirm('Clear all notes?')) return;saveNotes([]);renderNotesList();toast('Notes cleared','warn')}); // ── OPML Import/Export ── document.getElementById('btnExportOPML').addEventListener('click',()=>{ let opml='\n\nSync Dashboard Feeds'+new Date().toISOString()+'\n\n'; const cats={};state.sources.forEach(s=>{if(s.type!=='rss') return;const c=s.category||'Uncategorized';if(!cats[c]) cats[c]=[];cats[c].push(s)}); Object.entries(cats).sort((a,b)=>a[0].localeCompare(b[0])).forEach(([cat,sources])=>{ opml+=' \n'; sources.forEach(s=>{opml+=' \n'}); opml+=' \n'; }); opml+='\n'; const blob=new Blob([opml],{type:'text/xml'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='sync-dashboard-feeds.opml';a.click();URL.revokeObjectURL(a.href);toast('OPML exported ('+state.sources.filter(s=>s.type==='rss').length+' feeds)','ok'); }); document.getElementById('btnImportOPML').addEventListener('click',()=>{ const inp=document.createElement('input');inp.type='file';inp.accept='.opml,.xml'; inp.addEventListener('change',e=>{ const f=e.target.files[0];if(!f) return; const r=new FileReader();r.onload=()=>{ try{ const doc=new DOMParser().parseFromString(r.result,'text/xml'); let added=0; doc.querySelectorAll('outline[xmlUrl]').forEach(el=>{ const url=el.getAttribute('xmlUrl');const name=el.getAttribute('text')||el.getAttribute('title')||url; if(!url||state.sources.some(s=>s.url===url)) return; const parent=el.parentElement;const cat=parent?.getAttribute('text')||parent?.getAttribute('title')||'Imported'; addSource({name,url,type:'rss',category:cat,region:'Global',icon:'🌐'});added++; }); toast('Imported '+added+' feeds from OPML','ok'); }catch(e){toast('OPML parse error: '+e.message,'err')} };r.readAsText(f); });inp.click(); }); // ── Source Health Export ── document.getElementById('btnHealthExport').addEventListener('click',exportSourceHealth); // ── Settings popover: toggle UI bars ── // ── Quick collapse all bars (click title) ── let barsCollapsed=false; document.getElementById('headerTitle').addEventListener('click',()=>{ barsCollapsed=!barsCollapsed; ['tickerStrip','infoStrip','hotStrip','fwBar','debtBar','statsBar','wotdBar','qotdBar','otdStrip','weatherBar','countdownBar','energyBar','earthCamsBar','sessionBar','healthBar','statusBarSection','filterBar'].forEach(id=>{ const el=document.getElementById(id); if(el){ if(barsCollapsed) el.dataset.wasVisible=el.style.display!=='none'?'1':'0'; el.style.display=barsCollapsed?'none':(el.dataset.wasVisible==='0'?'none':''); } }); toast(barsCollapsed?'Bars collapsed — click title to expand':'Bars expanded','info'); }); // ── More dropdown ── document.getElementById('btnMore').addEventListener('click',e=>{ e.stopPropagation(); document.getElementById('moreDropdown').classList.toggle('open'); }); document.addEventListener('click',e=>{if(!e.target.closest('.action-menu')) document.getElementById('moreDropdown').classList.remove('open')}); // Close more dropdown when any button inside is clicked document.getElementById('moreDropdown').addEventListener('click',()=>{document.getElementById('moreDropdown').classList.remove('open')}); document.getElementById('btnSettings').addEventListener('click',e=>{ e.stopPropagation(); const pop=document.getElementById('settingsPopover'); pop.style.display=pop.style.display==='none'?'block':'none'; }); // ── Bar reorder: Show/Hide All + Reset ── document.getElementById('btnShowAllBars')?.addEventListener('click',()=>{ document.querySelectorAll('.ui-toggle').forEach(cb=>{cb.checked=true;const el=document.getElementById(cb.dataset.target);if(el) el.style.display=''}); if(!state.settings.uiBars) state.settings.uiBars={}; document.querySelectorAll('.ui-toggle').forEach(cb=>{state.settings.uiBars[cb.dataset.target]=true}); saveState();toast('All sections shown','ok'); }); document.getElementById('btnHideAllBars')?.addEventListener('click',()=>{ document.querySelectorAll('.ui-toggle').forEach(cb=>{cb.checked=false;const el=document.getElementById(cb.dataset.target);if(el) el.style.display='none'}); if(!state.settings.uiBars) state.settings.uiBars={}; document.querySelectorAll('.ui-toggle').forEach(cb=>{state.settings.uiBars[cb.dataset.target]=false}); saveState();toast('All sections hidden','ok'); }); document.getElementById('btnNewsOnlyMode')?.addEventListener('click',()=>{ // Show only: threat bar, hot news, filter bar. Hide everything else. const keepVisible=new Set(['threatBar','hotStrip','filterBar']); if(!state.settings.uiBars) state.settings.uiBars={}; document.querySelectorAll('.ui-toggle').forEach(cb=>{ const show=keepVisible.has(cb.dataset.target); cb.checked=show; const el=document.getElementById(cb.dataset.target); if(el) el.style.display=show?'':'none'; state.settings.uiBars[cb.dataset.target]=show; }); saveState();toast('News-only mode: showing threat + hot news + filters','ok'); }); document.getElementById('btnResetBarOrder')?.addEventListener('click',()=>{ if(state.settings.barOrder) delete state.settings.barOrder; saveState();location.reload(); }); // ── Bar reorder: Up/Down buttons ── document.getElementById('barSortList')?.addEventListener('click',e=>{ const mvBtn=e.target.closest('.bar-mv'); if(!mvBtn) return; const item=mvBtn.closest('.bar-sort-item'); const list=document.getElementById('barSortList'); const dir=mvBtn.dataset.dir; if(dir==='up'&&item.previousElementSibling){list.insertBefore(item,item.previousElementSibling)} else if(dir==='down'&&item.nextElementSibling){list.insertBefore(item.nextElementSibling,item)} // Apply new order to DOM applyBarOrder(); }); function applyBarOrder(){ const header=document.querySelector('.header'); const items=[...document.querySelectorAll('#barSortList .bar-sort-item')]; const order=items.map(i=>i.dataset.bar); // Save order if(!state.settings.barOrder) state.settings.barOrder=[]; state.settings.barOrder=order; saveState(); // Reorder actual DOM elements const filterBar=document.getElementById('filterBar'); order.forEach(barId=>{ const el=document.getElementById(barId); if(el&&filterBar) filterBar.parentNode.insertBefore(el,filterBar); }); } // ── Bar reorder: Drag and drop ── (function(){ const list=document.getElementById('barSortList'); if(!list) return; let dragItem=null; list.addEventListener('dragstart',e=>{ const item=e.target.closest('.bar-sort-item'); if(!item) return; dragItem=item;item.classList.add('dragging'); e.dataTransfer.effectAllowed='move'; }); list.addEventListener('dragover',e=>{ e.preventDefault(); const item=e.target.closest('.bar-sort-item'); if(item&&item!==dragItem){list.querySelectorAll('.drag-over').forEach(i=>i.classList.remove('drag-over'));item.classList.add('drag-over')} }); list.addEventListener('drop',e=>{ e.preventDefault(); const target=e.target.closest('.bar-sort-item'); if(target&&dragItem&&target!==dragItem){list.insertBefore(dragItem,target)} list.querySelectorAll('.drag-over').forEach(i=>i.classList.remove('drag-over')); if(dragItem){dragItem.classList.remove('dragging');dragItem=null} applyBarOrder(); }); list.addEventListener('dragend',()=>{if(dragItem){dragItem.classList.remove('dragging');dragItem=null}list.querySelectorAll('.drag-over').forEach(i=>i.classList.remove('drag-over'))}); // Make items draggable list.querySelectorAll('.bar-sort-item').forEach(item=>item.draggable=true); })(); // ── Restore saved bar order on load ── function restoreBarOrder(){ const order=state.settings.barOrder; if(!order||!order.length) return; const filterBar=document.getElementById('filterBar'); order.forEach(barId=>{ const el=document.getElementById(barId); if(el&&filterBar) filterBar.parentNode.insertBefore(el,filterBar); }); // Also reorder the settings list to match const list=document.getElementById('barSortList'); if(!list) return; order.forEach(barId=>{ const item=list.querySelector(`[data-bar="${barId}"]`); if(item) list.appendChild(item); }); } // ── Offline detection ── window.addEventListener('online',()=>{document.getElementById('offlineIndicator').style.display='none';toast('Back online','ok')}); window.addEventListener('offline',()=>{document.getElementById('offlineIndicator').style.display='';toast('You are offline — cached data shown','warn')}); if(!navigator.onLine) document.getElementById('offlineIndicator').style.display=''; document.addEventListener('click',e=>{if(!e.target.closest('#settingsPopover')&&!e.target.closest('#btnSettings')) document.getElementById('settingsPopover').style.display='none'}); // Map toggle IDs to element IDs const UI_BAR_MAP={tickerStrip:'tickerStrip',infoStrip:'infoStrip',hotStrip:'hotStrip',fwBar:'fwBar',debtBar:'debtBar',statsBar:'statsBar'}; function applyUiToggles(){ if(!state.settings.uiBars) state.settings.uiBars={}; document.querySelectorAll('.ui-toggle').forEach(cb=>{ const key=cb.dataset.target; const visible=state.settings.uiBars[key]!==false; // default visible cb.checked=visible; const el=document.getElementById(key); if(el) el.style.display=visible?'':'none'; }); } document.querySelectorAll('.ui-toggle').forEach(cb=>{ cb.addEventListener('change',()=>{ const key=cb.dataset.target; if(!state.settings.uiBars) state.settings.uiBars={}; state.settings.uiBars[key]=cb.checked; const el=document.getElementById(key); if(el) el.style.display=cb.checked?'':'none'; saveState(); }); }); document.getElementById('btnExport').addEventListener('click',exportData); // Export config only (no cache — much smaller file) document.getElementById('btnExportConfig').addEventListener('click',()=>{ const p={sources:state.sources,settings:state.settings,bookmarks:state.bookmarks,regions:state.regions,exportDate:new Date().toISOString(),version:state.version}; const b=new Blob([JSON.stringify(p,null,2)],{type:'application/json'}); const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=`sync-dash-config-${new Date().toISOString().slice(0,10)}.json`;a.click();URL.revokeObjectURL(a.href); toast('Config exported (no cache)','ok'); }); // Disable all sources document.getElementById('btnDisableAll').addEventListener('click',()=>{ state.sources.forEach(s=>s.enabled=false);saveState();renderAll();toast('All sources disabled','warn'); }); // Enable core only document.getElementById('btnEnableCore').addEventListener('click',()=>{ state.sources.forEach(s=>s.enabled=CORE_CATEGORIES.has(s.category)); saveState();renderAll(); const n=state.sources.filter(s=>s.enabled).length; toast(`Enabled ${n} core sources, disabled rest`,'ok'); }); // Disable all failed document.getElementById('btnDisableFailed').addEventListener('click',()=>{ const failed=state.sources.filter(s=>s.errorState); failed.forEach(s=>s.enabled=false);saveState();renderAll(); toast(`Disabled ${failed.length} failed sources`,'warn'); }); document.getElementById('btnDisableEmpty').addEventListener('click',()=>{ const empty=state.sources.filter(s=>s.enabled&&!s.errorState&&s.lastFetched&&!(state.cache[s.id]||[]).length); empty.forEach(s=>s.enabled=false);saveState();renderAll(); toast(`Disabled ${empty.length} empty sources`,'warn'); }); // Hot news time selector document.getElementById('hotTimeSelect').addEventListener('change',e=>{ state.settings.hotNewsMinutes=parseInt(e.target.value)||60;saveState();renderHotNews(); }); // Custom keyword tracker document.getElementById('btnKeywords').addEventListener('click',()=>{renderKwList();openModal('keywordModal')}); document.getElementById('btnApiKeys').addEventListener('click',()=>{renderApiKeyModal();openModal('apiKeyModal')}); document.getElementById('btnAddKw').addEventListener('click',()=>{ const v=document.getElementById('kwInput').value.trim();if(!v) return; if(!state.settings.customKeywords) state.settings.customKeywords=[]; if(!state.settings.customKeywords.includes(v)){state.settings.customKeywords.push(v);saveState();renderKwList();renderSbxPanel();toast(`Keyword added: ${v}`,'ok')} document.getElementById('kwInput').value=''; }); function renderKwList(){ const list=document.getElementById('kwList');list.innerHTML=''; (state.settings.customKeywords||[]).forEach((kw,i)=>{ const div=document.createElement('div');div.style.cssText='display:flex;align-items:center;gap:.4rem;padding:.25rem 0;border-bottom:1px solid var(--border);font-size:.78rem'; div.innerHTML=`${esc(kw)}`; const rm=document.createElement('button');rm.className='btn btn-sm btn-danger';rm.textContent='\u2716';rm.style.fontSize='.64rem'; rm.addEventListener('click',()=>{state.settings.customKeywords.splice(i,1);saveState();renderKwList();renderSbxPanel()}); div.appendChild(rm);list.appendChild(div); }); if(!(state.settings.customKeywords||[]).length) list.innerHTML='

No custom keywords. Built-in SBX/missile defense keywords are always active.

'; } document.getElementById('btnImport').addEventListener('click',()=>{document.getElementById('importText').value='';document.getElementById('importFile').value='';document.getElementById('importMerge').checked=false;openModal('importModal')}); document.getElementById('importFile').addEventListener('change',e=>{const f=e.target.files[0];if(!f) return;const r=new FileReader();r.onload=()=>{document.getElementById('importText').value=r.result};r.readAsText(f)}); document.getElementById('btnDoImport').addEventListener('click',()=>{const j=document.getElementById('importText').value.trim();if(!j){toast('No data','err');return}importData(j,document.getElementById('importMerge').checked);closeModal('importModal')}); function showConfirm(t,m,cb){document.getElementById('confirmTitle').textContent=t;document.getElementById('confirmMsg').textContent=m;confirmCallback=cb;openModal('confirmModal')} document.getElementById('btnConfirmOk').addEventListener('click',()=>{closeModal('confirmModal');if(confirmCallback){confirmCallback();confirmCallback=null}}); let searchDb=null; document.getElementById('searchInput').addEventListener('input',()=>{clearTimeout(searchDb);searchDb=setTimeout(()=>{requestAnimationFrame(()=>{renderCards();renderTimeline()})},250)}); document.getElementById('categoryPills').addEventListener('click',e=>{const p=e.target.closest('.pill');if(!p) return;document.querySelectorAll('#categoryPills .pill').forEach(x=>x.classList.remove('active'));p.classList.add('active');const av=getActiveView();if(av==='cards') renderCards();else if(av==='timeline') renderTimeline();}); document.getElementById('regionPills').addEventListener('click',e=>{const p=e.target.closest('.pill');if(!p) return;document.querySelectorAll('#regionPills .pill').forEach(x=>x.classList.remove('active'));p.classList.add('active');const av=getActiveView();if(av==='cards') renderCards();else if(av==='timeline') renderTimeline();}); document.getElementById('statusPills').addEventListener('click',e=>{const p=e.target.closest('.pill');if(!p) return;document.querySelectorAll('#statusPills .pill').forEach(x=>x.classList.remove('active'));p.classList.add('active');renderCards()}); document.getElementById('btnResetFilters').addEventListener('click',()=>{ document.querySelectorAll('#categoryPills .pill').forEach(p=>p.classList.remove('active')); document.querySelectorAll('#regionPills .pill').forEach(p=>p.classList.remove('active')); document.querySelectorAll('#statusPills .pill').forEach(p=>p.classList.remove('active')); const allCat=document.querySelector('#categoryPills .pill[data-cat="all"]');if(allCat) allCat.classList.add('active'); const allReg=document.querySelector('#regionPills .pill[data-region="all"]');if(allReg) allReg.classList.add('active'); const allStat=document.querySelector('#statusPills .pill[data-status="all"]');if(allStat) allStat.classList.add('active'); document.getElementById('searchInput').value=''; document.getElementById('cardSort').value='default'; collapsedCards.clear();collapsedCats.clear();allCardsFolded=false; renderCards();renderTimeline(); toast('Filters reset to defaults','ok'); }); document.getElementById('refreshSelect').addEventListener('change',e=>{state.settings.refreshIntervalMinutes=parseFloat(e.target.value)||0;saveState();startAutoRefresh()}); // Ticker scroll speed function applyTickerSpeed(s){ const track=document.getElementById('tickerTrack'); if(s==0){track.style.animationPlayState='paused'} else{track.style.animationPlayState='running';document.getElementById('tickerStrip').style.setProperty('--ticker-speed',s+'s')} state.settings.tickerSpeed=s;saveState(); } document.getElementById('tickerSpeed').addEventListener('change',e=>applyTickerSpeed(parseInt(e.target.value))); // ── Ticker grab-to-scroll (mouse drag) ── (function(){ const strip=document.getElementById('tickerStrip'); const track=document.getElementById('tickerTrack'); let isDragging=false,startX=0,scrollLeft=0,animWasRunning=true; function onMouseMove(e){ const dx=e.pageX-startX; track.style.transform=`translateX(${scrollLeft+dx}px)`; } function onMouseUp(){ isDragging=false;strip.classList.remove('dragging'); track.style.animation='';track.style.transform=''; document.removeEventListener('mousemove',onMouseMove); document.removeEventListener('mouseup',onMouseUp); } strip.addEventListener('mousedown',e=>{ isDragging=true;startX=e.pageX; const style=getComputedStyle(track); const matrix=new DOMMatrixReadOnly(style.transform); scrollLeft=matrix.m41; animWasRunning=style.animationPlayState!=='paused'; strip.classList.add('dragging'); track.style.animation='none'; track.style.transform=`translateX(${scrollLeft}px)`; e.preventDefault(); document.addEventListener('mousemove',onMouseMove); document.addEventListener('mouseup',onMouseUp); }); // Touch support strip.addEventListener('touchstart',e=>{ isDragging=true;startX=e.touches[0].pageX; const style=getComputedStyle(track); const matrix=new DOMMatrixReadOnly(style.transform); scrollLeft=matrix.m41; strip.classList.add('dragging'); track.style.animation='none'; track.style.transform=`translateX(${scrollLeft}px)`; },{passive:true}); strip.addEventListener('touchmove',e=>{ if(!isDragging) return; const dx=e.touches[0].pageX-startX; track.style.transform=`translateX(${scrollLeft+dx}px)`; },{passive:true}); strip.addEventListener('touchend',()=>{ if(!isDragging) return; isDragging=false;strip.classList.remove('dragging'); track.style.animation='';track.style.transform=''; }); })(); // ── Data refresh rate (ticker + FX) ── let dataRefreshTimer=null; function startDataRefresh(){ clearInterval(dataRefreshTimer); const secs=parseInt(state.settings.dataRefreshRate)||60; document.getElementById('dataRefreshRate').value=String(secs); dataRefreshTimer=setInterval(()=>{if(!document.hidden) fetchAllQuotes().then(renderFWComm)},secs*1000); } document.getElementById('dataRefreshRate').addEventListener('change',e=>{ state.settings.dataRefreshRate=parseInt(e.target.value)||60; saveState();startDataRefresh(); toast(`Data refresh: every ${e.target.value}s`,'info'); }); // ── Refresh All button (news + ticker + FX in one click) ── document.getElementById('btnRefreshAll').addEventListener('click',async()=>{ toast('Refreshing everything...','info'); await Promise.all([fetchAllQuotes().then(renderFWComm),syncAll()]); renderInfoStrip(); toast('All data refreshed','ok'); }); // ── Drag and drop cards ── let dragSrcId=null; const grid=document.getElementById('cardGrid'); grid.addEventListener('dragstart',e=>{ const card=e.target.closest('.card');if(!card) return; dragSrcId=card.dataset.id;card.classList.add('dragging'); e.dataTransfer.effectAllowed='move';e.dataTransfer.setData('text/plain',dragSrcId); }); grid.addEventListener('dragend',e=>{ const card=e.target.closest('.card');if(card) card.classList.remove('dragging'); grid.querySelectorAll('.drag-over').forEach(c=>c.classList.remove('drag-over')); }); grid.addEventListener('dragover',e=>{ e.preventDefault();e.dataTransfer.dropEffect='move'; const card=e.target.closest('.card'); grid.querySelectorAll('.drag-over').forEach(c=>c.classList.remove('drag-over')); if(card&&card.dataset.id!==dragSrcId) card.classList.add('drag-over'); }); grid.addEventListener('dragleave',e=>{ const card=e.target.closest('.card');if(card) card.classList.remove('drag-over'); }); grid.addEventListener('drop',e=>{ e.preventDefault(); const targetCard=e.target.closest('.card');if(!targetCard) return; const targetId=targetCard.dataset.id; if(!dragSrcId||dragSrcId===targetId) return; const srcIdx=state.sources.findIndex(s=>s.id===dragSrcId); const tgtIdx=state.sources.findIndex(s=>s.id===targetId); if(srcIdx<0||tgtIdx<0) return; const [moved]=state.sources.splice(srcIdx,1); state.sources.splice(tgtIdx,0,moved); saveState();renderCards(); grid.querySelectorAll('.drag-over').forEach(c=>c.classList.remove('drag-over')); toast('Card reordered','info'); }); // ── View toggle with cached panel refs + View Transitions API ── const _panelMap={cards:'cardGrid',timeline:'timelineView',bookmarks:'bookmarksPanel',junk:'junkPanel',video:'videoPanel',social:'socialPanel',sbx:'sbxPanel',college:'collegePanel',premium:'premiumPanel',links:'linksPanel',list:'listView',sources:'sourcesPanel',health:'healthDashPanel','time-converter':'timeConverterPanel',politics:'politicsPanel',holidays:'holidaysPanel','intel-dash':'intelDashPanel',maritime:'maritimePanel','weather-maps':'weatherMapsPanel','econ-lab':'econLabPanel',reports:'reportsPanel',geoint:'geointPanel'}; const _panelEls={};Object.entries(_panelMap).forEach(([k,id])=>{_panelEls[k]=document.getElementById(id)}); const _viewRenderers={cards:renderCards,timeline:renderTimeline,bookmarks:renderBookmarks,junk:renderJunkPanel,video:renderVideoPanel,social:renderSocialPanel,sbx:renderSbxPanel,college:renderCollegePanel,list:renderListView,sources:renderSourcesPanel,health:renderHealthPanel,'time-converter':renderTimeConverterPanel,premium:renderPremiumPanel,links:renderLinksPanel,politics:renderPoliticsPanel,holidays:renderHolidaysPanel,'intel-dash':()=>renderIntelDash(),maritime:renderMaritimePanel,'weather-maps':renderWeatherMapsPanel,'econ-lab':renderEconLabPanel,reports:renderReportsPanel,geoint:()=>{if(!window._geointInitialized){window._geointInitialized=true;try{renderGeointPanel()}catch(e){console.warn('GEOINT init failed:',e)}}setTimeout(()=>{if(typeof geointMap!=='undefined'&&geointMap) geointMap.invalidateSize()},400)}}; let _currentView='cards'; function switchView(v){ _currentView=v; // Persist active view so renderAll doesn't lose it try{sessionStorage.setItem('syncDash_activeView',v)}catch(e){} // Non-Chromium transition fallback if(!document.startViewTransition){ const panels=document.querySelectorAll('.grid,.timeline,.premium-panel,.bookmarks-panel,.junk-panel,.sbx-panel,.list-view,.sources-panel'); panels.forEach(p=>p.classList.add('view-fade-in')); setTimeout(()=>panels.forEach(p=>p.classList.remove('view-fade-in')),200); } document.querySelectorAll('.view-toggle button').forEach(b=>b.classList.remove('active')); const activeBtn=document.querySelector(`.view-toggle button[data-view="${v}"]`);if(activeBtn) activeBtn.classList.add('active'); // Also activate group menu buttons document.querySelectorAll('.vt-group-menu button[data-view]').forEach(b=>b.classList.toggle('active',b.dataset.view===v)); Object.entries(_panelEls).forEach(([key,el])=>{if(!el) return;if(key==='cards') el.classList.toggle('hidden',v!=='cards');else el.classList.toggle('active',v===key)}); if(_viewRenderers[v]) _viewRenderers[v](); const sn=document.getElementById('scrollNav');if(sn&&v!=='cards'&&v!=='timeline'&&v!=='list') sn.classList.remove('visible'); const as=document.getElementById('alphaSidebar');if(as) as.classList.toggle('visible',v==='cards'); // Update group highlights document.querySelectorAll('.vt-group').forEach(g=>{ const hasActive=g.querySelector('.vt-group-menu button.active'); g.classList.toggle('has-active',!!hasActive); }); const contentArea=document.querySelector('.filter-bar');if(contentArea) contentArea.scrollIntoView({behavior:'smooth',block:'start'}); } // Only attach click handlers to TOP-LEVEL view buttons (not group toggles or group menu items) document.querySelectorAll('.view-toggle > button[data-view]').forEach(btn=>{btn.addEventListener('click',()=>{ const v=btn.dataset.view;if(!v) return; if(document.startViewTransition){document.startViewTransition(()=>switchView(v))}else{switchView(v)} })}); // Home button – reset to default cards view with no filters document.getElementById('btnHome')?.addEventListener('click',()=>{ // Clear category filters document.querySelectorAll('#categoryPills .pill').forEach(p=>p.classList.remove('active')); // Clear search const searchBox=document.getElementById('searchBox');if(searchBox){searchBox.value='';searchBox.dispatchEvent(new Event('input'))} // Clear region filter const regionSel=document.getElementById('regionFilter');if(regionSel) regionSel.value='all'; // Switch to cards view if(document.startViewTransition){document.startViewTransition(()=>switchView('cards'))}else{switchView('cards')} // Scroll to top window.scrollTo({top:0,behavior:'smooth'}); }); // View toggle scroll fade indicators {const vt=document.querySelector('.view-toggle'); function updateVtScroll(){ if(!vt) return; vt.classList.toggle('scroll-left',vt.scrollLeft>8); vt.classList.toggle('scroll-right',vt.scrollLeft{ toggle.addEventListener('click',e=>{ e.stopPropagation(); const menu=toggle.nextElementSibling; const isOpen=menu.classList.contains('open'); // Close all other group menus document.querySelectorAll('.vt-group-menu.open').forEach(m=>m.classList.remove('open')); if(!isOpen) menu.classList.add('open'); toggle.setAttribute('aria-expanded',String(!isOpen)); }); }); // Group menu buttons trigger view switch and close menu document.querySelectorAll('.vt-group-menu button[data-view]').forEach(btn=>{ btn.addEventListener('click',()=>{ const v=btn.dataset.view; closeAllGroupMenus(); if(document.startViewTransition){document.startViewTransition(()=>switchView(v))}else{switchView(v)} // Highlight group parent if child is active document.querySelectorAll('.vt-group').forEach(g=>{ const hasActive=g.querySelector('.vt-group-menu button.active'); g.classList.toggle('has-active',!!hasActive); }); }); }); // Close group menus on outside click + sync aria-expanded function closeAllGroupMenus(){ document.querySelectorAll('.vt-group-menu.open').forEach(m=>{m.classList.remove('open');const toggle=m.previousElementSibling;if(toggle) toggle.setAttribute('aria-expanded','false')}); } document.addEventListener('click',e=>{if(!e.target.closest('.vt-group')) closeAllGroupMenus()}); // (switchView group highlights + transition fallback integrated directly into switchView function) // Close group menus on Escape document.addEventListener('keydown',e=>{if(e.key==='Escape') closeAllGroupMenus()}); // Source jump auto-dismiss on mobile touch document.getElementById('sourceJumpResults').addEventListener('touchstart',e=>{ const item=e.target.closest('.sj-item'); if(item){setTimeout(()=>document.getElementById('sourceJumpResults').classList.remove('open'),300)} },{passive:true}); // (View toggles remain enabled during sync for navigation) document.addEventListener('keydown',e=>{if(e.target.matches('input,textarea,select')) return;const h=document.getElementById('kbdHelp');const viewBtns=document.querySelectorAll('.view-toggle button[data-view]');switch(e.key){case '?':h.classList.toggle('open');break;case 's':case 'S':e.preventDefault();syncAll();break;case '/':e.preventDefault();document.getElementById('searchInput').focus();break;case 't':case 'T':toggleTheme();break;case 'n':case 'N':document.getElementById('btnAdd').click();break;case 'g':case 'G':e.preventDefault();document.getElementById('sourceJumpInput').focus();break;case 'w':case 'W':{const el=document.getElementById('wotdBar');el.style.display=el.style.display==='none'?'':'none';break}case 'h':case 'H':{const el=document.getElementById('otdStrip');el.style.display=el.style.display==='none'?'':'none';break}case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8':case '9':{const _kbViews=['cards','timeline','bookmarks','list','sources','reports','video','social','geoint'];const vk=_kbViews[parseInt(e.key)-1];if(vk) switchView(vk);break}case 'Escape':if(focusMode){toggleFocusMode();return}document.querySelectorAll('.modal-overlay.open').forEach(m=>m.classList.remove('open'));h.classList.remove('open');document.getElementById('settingsPopover').style.display='none';document.getElementById('moreDropdown').classList.remove('open');document.querySelectorAll('.card-dropdown.open').forEach(d=>d.classList.remove('open'));document.getElementById('searchInput').blur();break}}); // Close kbd help on overlay click document.getElementById('kbdHelp').addEventListener('click',e=>{if(e.target.id==='kbdHelp') e.target.classList.remove('open')}); // ── Scroll nav (IntersectionObserver instead of scroll listener) ── {const sentinel=document.createElement('div');sentinel.style.height='1px';sentinel.setAttribute('aria-hidden','true'); const filterBar=document.querySelector('.filter-bar'); if(filterBar){filterBar.after(sentinel); const navObs=new IntersectionObserver(([entry])=>{ const nav=document.getElementById('scrollNav'); const v=getActiveView(); const showNav=v==='cards'||v==='timeline'||v==='list'; nav.classList.toggle('visible',!entry.isIntersecting&&showNav); }); navObs.observe(sentinel)}} document.getElementById('btnScrollTop').addEventListener('click',()=>window.scrollTo({top:0})); // Scroll progress indicator window.addEventListener('scroll',()=>{const sp=document.getElementById('scrollProgress');if(sp){const pct=window.scrollY/(document.documentElement.scrollHeight-window.innerHeight)*100;sp.style.width=Math.min(pct,100)+'%'}},{passive:true}); document.getElementById('jumpCategory').addEventListener('change',e=>{ if(!e.target.value) return; const el=document.getElementById(e.target.value); if(el) el.scrollIntoView({behavior:'smooth',block:'start'}); e.target.value=''; }); // ── Source Quick-Jump Navigator ── { const sjInput=document.getElementById('sourceJumpInput'); const sjResults=document.getElementById('sourceJumpResults'); let sjIdx=-1; function sjSearch(q){ sjResults.innerHTML='';sjIdx=-1; if(!q||q.length<1){sjResults.classList.remove('open');return} const lower=q.toLowerCase(); const matches=state.sources.filter(s=>{ return s.name.toLowerCase().includes(lower)|| (s.category||'').toLowerCase().includes(lower)|| (s.icon||'').includes(q); }).slice(0,15); if(!matches.length){ sjResults.innerHTML='
No matching sources
'; sjResults.classList.add('open');return; } matches.forEach((src,i)=>{ const div=document.createElement('div');div.className='sj-item';div.dataset.sjId=src.id; const itemCount=(state.cache[src.id]||[]).length; div.innerHTML=`${esc(src.icon||'\u{1F310}')}${esc(src.name)}${esc(src.category||'')}${itemCount}`; div.addEventListener('click',()=>sjJumpTo(src.id)); div.addEventListener('mouseenter',()=>{sjIdx=i;sjHighlight()}); sjResults.appendChild(div); }); sjResults.classList.add('open'); } function sjHighlight(){ sjResults.querySelectorAll('.sj-item').forEach((el,i)=>el.classList.toggle('sj-active',i===sjIdx)); } function sjJumpTo(id){ sjResults.classList.remove('open');sjInput.value='';sjInput.blur(); // Find the card in the DOM const card=document.querySelector(`.card[data-id="${CSS.escape(id)}"]`); if(card){ // Uncollapse the card if collapsed if(card.classList.contains('collapsed')){ collapsedCards.delete(id);card.classList.remove('collapsed'); card.querySelector('.card-head')?.setAttribute('aria-expanded','true'); } // Uncollapse its category if hidden if(card.style.display==='none'){ const catName=card.dataset.cat; if(catName) collapsedCats.delete(catName); card.style.display=''; const hdr=document.getElementById('cat-'+catName?.replace(/\\s+/g,'-')); if(hdr) hdr.classList.remove('collapsed'); } card.scrollIntoView({behavior:'smooth',block:'center'}); // Flash highlight card.style.transition='box-shadow .2s'; card.style.boxShadow='0 0 0 3px var(--accent),0 4px 20px rgba(56,189,248,.3)'; setTimeout(()=>{card.style.boxShadow='';},2000); } else { // Card not in DOM — might be filtered out. Switch to All category and retry toast('Source not visible — check category/region filters','info'); } } sjInput.addEventListener('input',()=>sjSearch(sjInput.value.trim())); sjInput.addEventListener('focus',()=>{if(sjInput.value.trim()) sjSearch(sjInput.value.trim())}); sjInput.addEventListener('keydown',e=>{ const items=sjResults.querySelectorAll('.sj-item'); if(e.key==='ArrowDown'){e.preventDefault();sjIdx=Math.min(sjIdx+1,items.length-1);sjHighlight();items[sjIdx]?.scrollIntoView({block:'nearest'})} else if(e.key==='ArrowUp'){e.preventDefault();sjIdx=Math.max(sjIdx-1,0);sjHighlight();items[sjIdx]?.scrollIntoView({block:'nearest'})} else if(e.key==='Enter'){e.preventDefault();if(sjIdx>=0&&items[sjIdx]) items[sjIdx].click();else if(items.length===1) items[0].click()} else if(e.key==='Escape'){sjResults.classList.remove('open');sjInput.blur()} }); document.addEventListener('click',e=>{if(!e.target.closest('.source-jump')) sjResults.classList.remove('open')}); } // ── Floating Alphabetical Sidebar ── { const sidebar=document.getElementById('alphaSidebar'); function buildAlphaSidebar(){ sidebar.innerHTML=''; const v=getActiveView(); if(v!=='cards'){sidebar.classList.remove('visible');return} sidebar.classList.add('visible'); // Collect first letters of visible source names const letterSources={}; state.sources.forEach(src=>{ const firstChar=(src.name||'?')[0].toUpperCase(); const letter=/[A-Z]/.test(firstChar)?firstChar:'#'; if(!letterSources[letter]) letterSources[letter]=[]; letterSources[letter].push(src); }); const letters='#ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); letters.forEach(l=>{ const has=!!letterSources[l]; const el=document.createElement('div');el.className='alpha-letter'+(has?' has-items':''); el.textContent=l;el.title=has?letterSources[l].length+' sources':'No sources'; if(has){ el.addEventListener('click',()=>{ // Find the first card that starts with this letter const src=letterSources[l][0]; const card=document.querySelector(`.card[data-id="${CSS.escape(src.id)}"]`); if(card){ card.scrollIntoView({behavior:'smooth',block:'start'}); card.style.boxShadow='0 0 0 3px var(--accent),0 4px 20px rgba(56,189,248,.3)'; setTimeout(()=>{card.style.boxShadow=''},1500); } }); } sidebar.appendChild(el); }); } // Rebuild on view switch and render const origSwitchView=window.switchView||switchView; const _origRenderCards=renderCards; const _alphaDebounce={timer:null}; function scheduleAlphaRebuild(){clearTimeout(_alphaDebounce.timer);_alphaDebounce.timer=setTimeout(buildAlphaSidebar,200)} // Hook into renderCards to update sidebar const _hookedRenderCards=renderCards; window._afterRenderCards=scheduleAlphaRebuild; // Initial build after a short delay setTimeout(buildAlphaSidebar,1500); // Also rebuild on view switch document.querySelectorAll('.view-toggle button').forEach(btn=>{ btn.addEventListener('click',()=>setTimeout(buildAlphaSidebar,100)); }); } document.getElementById('btnCollapseAll').addEventListener('click',()=>{ allCardsFolded=!allCardsFolded; if(allCardsFolded){ state.sources.forEach(s=>collapsedCards.add(s.id)); document.getElementById('btnCollapseAll').innerHTML='\u25BC Unfold All'; }else{ collapsedCards.clear();collapsedCats.clear(); document.getElementById('btnCollapseAll').innerHTML='\u25B6 Fold All'; } renderCards(); }); // ── Sort handler ── document.getElementById('cardSort').addEventListener('change',()=>renderCards()); // ── Density toggle ── document.querySelectorAll('.density-toggle button').forEach(btn=>{btn.addEventListener('click',()=>{ document.querySelectorAll('.density-toggle button').forEach(b=>b.classList.remove('active'));btn.classList.add('active'); document.getElementById('cardGrid').classList.toggle('compact-mode',btn.dataset.density==='compact'); })}); // ── Mark all read ── document.getElementById('btnMarkAllRead').addEventListener('click',()=>{ const count=state.sources.reduce((n,s)=>n+newCount(s.id),0); if(!count){toast('Nothing to mark','info');return} state.sources.forEach(s=>markSeen(s.id));saveState();renderAll();toast(`Marked ${count} items as read`,'ok'); }); // ── Delegated click handlers for card grid (replaces ~10K per-item listeners) ── document.getElementById('cardGrid').addEventListener('click',e=>{ // Star / bookmark toggle (delegated) const star=e.target.closest('.feed-item-star[data-bm-link]'); if(star){e.stopPropagation();const it={link:star.dataset.bmLink,title:star.dataset.bmTitle,pubDate:star.dataset.bmPubDate,source:star.dataset.bmSource};toggleBM(it);return} // Wayback badge click (delegated) const wb=e.target.closest('.wb-badge[data-wb-link]'); if(wb){e.stopPropagation();window.open(wbUrl(wb.dataset.wbLink),'_blank');return} // Copy/share button (delegated) const cp=e.target.closest('.share-btn[data-cp-link]'); if(cp){e.stopPropagation();copyLink(cp.dataset.cpLink,cp.dataset.cpTitle,cp);return} // Category badge filter const catBadge=e.target.closest('.card-cat.clickable'); if(catBadge){e.stopPropagation();const cat=catBadge.dataset.filterCat; const pills=document.querySelectorAll('#categoryPills .pill'); pills.forEach(p=>p.classList.remove('active')); const match=[...pills].find(p=>p.dataset.cat===cat); if(match){match.classList.add('active')}else{pills[0]?.classList.add('active')} renderCards();renderTimeline();return} // Region badge filter const regBadge=e.target.closest('.card-region.clickable'); if(regBadge){e.stopPropagation();const reg=regBadge.dataset.filterRegion; const pills=document.querySelectorAll('#regionPills .pill'); pills.forEach(p=>p.classList.remove('active')); const match=[...pills].find(p=>p.dataset.region===reg); if(match){match.classList.add('active')}else{pills[0]?.classList.add('active')} renderCards();renderTimeline();return} }); // Timestamp refresh moved to managed interval in DOMContentLoaded init block function refreshTimestamps(){ document.querySelectorAll('.card-foot span[data-ts]').forEach(el=>{ const d=new Date(el.dataset.ts);if(!isNaN(d)) el.textContent=timeAgo(d); }); } // ── Filter bar Fold/Unfold/Fold Groups buttons ── document.getElementById('btnCollapseCards').addEventListener('click',()=>{ state.sources.forEach(s=>collapsedCards.add(s.id)); allCardsFolded=true;document.getElementById('btnCollapseAll').innerHTML='\u25BC Unfold All'; renderCards();toast('All cards collapsed','info'); }); document.getElementById('btnExpandCards').addEventListener('click',()=>{ collapsedCards.clear();collapsedCats.clear(); allCardsFolded=false;document.getElementById('btnCollapseAll').innerHTML='\u25B6 Fold All'; renderCards();toast('All cards expanded','info'); }); document.getElementById('btnCollapseCats').addEventListener('click',()=>{ const cats=[...new Set(state.sources.map(s=>s.category||'Uncategorized'))]; if(collapsedCats.size>=cats.length){collapsedCats.clear();toast('All groups expanded','info')} else{cats.forEach(c=>collapsedCats.add(c));toast('All groups collapsed','info')} renderCards(); }); // ── Countdown modal save handler ── document.getElementById('btnSaveCd').addEventListener('click',()=>{ const name=document.getElementById('cdName').value.trim(); const date=document.getElementById('cdDate').value; if(!name||!date){toast('Name and date required','err');return} if(!state.countdowns) state.countdowns=[]; state.countdowns.push({name,date});saveState();renderCountdownBar();closeModal('countdownModal');toast('Countdown added','ok'); }); // ── Weather city save handler ── document.getElementById('btnAddWeatherCity').addEventListener('click',()=>{ document.getElementById('weatherCityInput').value=''; renderWeatherCityList(); openModal('weatherCityModal'); }); function renderWeatherCityList(){ const list=document.getElementById('weatherCityList');list.innerHTML=''; if(!state.settings.weatherCities) return; state.settings.weatherCities.forEach((wc,i)=>{ const div=document.createElement('div');div.style.cssText='display:flex;align-items:center;gap:.4rem;padding:.25rem 0;border-bottom:1px solid var(--border);font-size:.78rem'; div.innerHTML=`${esc(wc.name)}`; const rm=document.createElement('button');rm.className='btn btn-sm btn-danger';rm.textContent='\u2716';rm.style.fontSize='.64rem'; rm.addEventListener('click',()=>{state.settings.weatherCities.splice(i,1);saveState();renderWeatherCityList();fetchWeather()}); div.appendChild(rm);list.appendChild(div); }); if(!state.settings.weatherCities.length) list.innerHTML='

No cities configured.

'; } document.getElementById('btnSaveWeatherCity').addEventListener('click',async()=>{ const city=document.getElementById('weatherCityInput').value.trim(); if(!city){toast('Enter a city name','err');return} if(!state.settings.weatherCities) state.settings.weatherCities=[]; if(state.settings.weatherCities.length>=6){toast('Max 6 cities','warn');return} let geoResult=null; // Primary: Open-Meteo geocoding try{ const r=await fetchWithTimeout('https://geocoding-api.open-meteo.com/v1/search?name='+encodeURIComponent(city)+'&count=1',8000); if(!r.ok) throw new Error('HTTP '+r.status); const d=await r.json(); if(d.results&&d.results.length){ const loc=d.results[0]; geoResult={name:loc.name+(loc.admin1?', '+loc.admin1:'')+(loc.country?', '+loc.country:''),lat:loc.latitude,lon:loc.longitude}; } }catch(e){console.warn('Geocoding primary failed:',e)} // Fallback 1: Nominatim (OpenStreetMap) if(!geoResult){ try{ const r=await fetchWithTimeout('https://nominatim.openstreetmap.org/search?q='+encodeURIComponent(city)+'&format=json&limit=1&accept-language=en',8000); if(!r.ok) throw new Error('HTTP '+r.status); const d=await r.json(); if(d&&d.length){ const loc=d[0]; const parts=loc.display_name.split(',').map(s=>s.trim()); const name=parts.slice(0,Math.min(3,parts.length)).join(', '); geoResult={name,lat:parseFloat(loc.lat),lon:parseFloat(loc.lon)}; } }catch(e){console.warn('Geocoding fallback1 (Nominatim) failed:',e)} } // Fallback 2: Open-Meteo with language param if(!geoResult){ try{ const r=await fetchWithTimeout('https://geocoding-api.open-meteo.com/v1/search?name='+encodeURIComponent(city)+'&count=1&language=en',8000); if(!r.ok) throw new Error('HTTP '+r.status); const d=await r.json(); if(d.results&&d.results.length){ const loc=d.results[0]; geoResult={name:loc.name+(loc.country?', '+loc.country:''),lat:loc.latitude,lon:loc.longitude}; } }catch(e){console.warn('Geocoding fallback2 failed:',e)} } if(!geoResult){toast('City not found — all geocoding services failed','err');return} if(state.settings.weatherCities.some(w=>w.name===geoResult.name)){toast('City already added','warn');return} state.settings.weatherCities.push(geoResult); saveState();document.getElementById('weatherCityInput').value='';renderWeatherCityList();fetchWeather();toast('Added '+geoResult.name,'ok'); }); // ── Clock Manager ── document.getElementById('btnManageClocks').addEventListener('click',()=>{ renderClockList();openModal('clockModal'); }); function renderClockList(){ const list=document.getElementById('clockList');list.innerHTML=''; const clocks=getClocks(); clocks.forEach((c,i)=>{ const div=document.createElement('div'); div.style.cssText='display:flex;align-items:center;gap:.4rem;padding:.25rem 0;border-bottom:1px solid var(--border);font-size:.78rem'; const now=new Date();const h=parseInt(now.toLocaleTimeString('en-US',{timeZone:c.tz,hour:'numeric',hour12:false}));const dnIcon=getDayNightIcon(h);const tNow=now.toLocaleTimeString('en-US',{timeZone:c.tz,hour:'2-digit',minute:'2-digit',hour12:false}); div.innerHTML=`${dnIcon} ${c.flag||'\u{1F310}'} ${esc(c.city)} ${tNow} ${esc(c.tz)}`; const rm=document.createElement('button');rm.className='btn btn-sm btn-danger';rm.textContent='\u2716';rm.style.fontSize='.64rem'; rm.addEventListener('click',()=>{ if(!state.settings.clocks) state.settings.clocks=JSON.parse(JSON.stringify(DEFAULT_CLOCKS)); state.settings.clocks.splice(i,1);saveState();renderClockList();renderInfoStrip(); }); div.appendChild(rm);list.appendChild(div); }); if(!clocks.length) list.innerHTML='

No clocks configured.

'; } document.getElementById('btnSaveClock').addEventListener('click',()=>{ const cityInput=document.getElementById('clockCityInput').value.trim(); const tz=document.getElementById('clockTzSelect').value; const open=parseFloat(document.getElementById('clockOpenInput').value)||9; const close=parseFloat(document.getElementById('clockCloseInput').value)||17; if(!tz){toast('Select a timezone','err');return} const city=cityInput||tz.split('/').pop().replace(/_/g,' '); if(!state.settings.clocks) state.settings.clocks=JSON.parse(JSON.stringify(DEFAULT_CLOCKS)); if(state.settings.clocks.length>=20){toast('Max 20 clocks','warn');return} if(state.settings.clocks.some(c=>c.tz===tz)){toast('Timezone already added','warn');return} const flag=CLOCK_FLAGS[tz]||'\u{1F310}'; state.settings.clocks.push({city,tz,open,close,flag}); saveState();document.getElementById('clockCityInput').value='';renderClockList();renderInfoStrip();toast('Added '+city,'ok'); }); document.getElementById('btnResetClocks').addEventListener('click',()=>{ state.settings.clocks=JSON.parse(JSON.stringify(DEFAULT_CLOCKS)); saveState();renderClockList();renderInfoStrip();toast('Clocks reset to defaults','ok'); }); // ── Time & Date Converter Panel ── function renderTimeConverterPanel(){ const panel=document.getElementById('timeConverterPanel');if(!panel) return; const clocks=getClocks(); const now=new Date(); // Build current times table let clockRows=''; clocks.forEach(c=>{ const tStr=now.toLocaleTimeString('en-US',{timeZone:c.tz,hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:true}); const dStr=now.toLocaleDateString('en-US',{timeZone:c.tz,weekday:'short',year:'numeric',month:'short',day:'numeric'}); const h=parseInt(now.toLocaleTimeString('en-US',{timeZone:c.tz,hour:'numeric',hour12:false})); const m=parseInt(now.toLocaleTimeString('en-US',{timeZone:c.tz,minute:'numeric'})); const hm=h+m/60; const day=now.toLocaleDateString('en-US',{timeZone:c.tz,weekday:'short'}); const isWeekend=day==='Sat'||day==='Sun'; let mktStatus='Closed';let mktColor='var(--red)'; if(!isWeekend&&hm>=c.open&&hm=c.open-1&&hm=c.close&&hm{if(c.tz==='UTC'||c.tz==='Etc/UTC'||c.tz==='Etc/GMT') return false;try{const jan=new Date(now.getFullYear(),0,1);const janOff=jan.toLocaleTimeString('en-US',{timeZone:c.tz,timeZoneName:'shortOffset'}).split('GMT')[1];const nowOff=now.toLocaleTimeString('en-US',{timeZone:c.tz,timeZoneName:'shortOffset'}).split('GMT')[1];return janOff!==nowOff}catch{return false}})(); const dstBadge=isDST?'DST':''; const dnIcon=getDayNightIcon(h); clockRows+=` ${dnIcon} ${c.flag||'\u{1F310}'} ${esc(c.city)}${dstBadge} ${tStr} ${dStr} ${offset} ${mktStatus} `; }); // Build timezone select options for converter let tzOptions=''; clocks.forEach(c=>{ tzOptions+=``; }); // Julian date info const jdn=getJulianDate(now).toFixed(5); const jdoy=getJulianDayOfYear(now); panel.innerHTML=`

🕓 Time & Date Converter

Julian Day #: ${jdoy}
Julian Date: ${jdn}
Zulu: ${now.toISOString().slice(0,19)}Z

🌐 World Clocks \u2600\uFE0F Day   \u{1F305} Sunrise   \u{1F307} Sunset   \u{1F319} Night

${clockRows}
CityTimeDateUTCMarket

🔄 Convert Time

📅 Quick: What time is it in...?

`; // Quick buttons const quickBtns=document.getElementById('tcQuickBtns'); clocks.forEach(c=>{ const btn=document.createElement('button'); btn.className='btn btn-sm'; btn.style.fontSize='.68rem'; btn.textContent=c.flag+' '+c.city; btn.addEventListener('click',()=>{ const n=new Date(); const t=n.toLocaleTimeString('en-US',{timeZone:c.tz,hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:true}); const d=n.toLocaleDateString('en-US',{timeZone:c.tz,weekday:'long',year:'numeric',month:'long',day:'numeric'}); toast(`${c.flag} ${c.city}: ${t} \u2014 ${d}`,'info',5000); }); quickBtns.appendChild(btn); }); // Convert button handler document.getElementById('tcConvertBtn').addEventListener('click',()=>{ const dateVal=document.getElementById('tcDate').value; const timeVal=document.getElementById('tcTime').value; const fromTz=document.getElementById('tcFrom').value; if(!dateVal||!timeVal){toast('Enter date and time','err');return} // Build a date in the source timezone // We parse the input as local, then adjust using the timezone offset const inputStr=dateVal+'T'+timeVal+':00'; // Use Intl to figure out the offset of the source timezone at that moment const formatter=new Intl.DateTimeFormat('en-US',{timeZone:fromTz,year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:false}); // Create a reference date from the input const refDate=new Date(inputStr); // Find the offset difference: format refDate in fromTz, parse it back, compare const parts=formatter.formatToParts(refDate); const pp={};parts.forEach(p=>pp[p.type]=p.value); const inTz=new Date(`${pp.year}-${pp.month}-${pp.day}T${pp.hour==='24'?'00':pp.hour}:${pp.minute}:${pp.second}`); const offsetMs=inTz.getTime()-refDate.getTime(); const sourceDate=new Date(refDate.getTime()-offsetMs); const results=document.getElementById('tcResults'); let html=''; clocks.forEach(c=>{ const t=sourceDate.toLocaleTimeString('en-US',{timeZone:c.tz,hour:'2-digit',minute:'2-digit',hour12:true}); const d=sourceDate.toLocaleDateString('en-US',{timeZone:c.tz,weekday:'short',year:'numeric',month:'short',day:'numeric'}); const off=sourceDate.toLocaleTimeString('en-US',{timeZone:c.tz,timeZoneName:'shortOffset'}).split(' ').pop(); const isFrom=c.tz===fromTz; const isDST=(()=>{if(c.tz==='UTC'||c.tz==='Etc/UTC'||c.tz==='Etc/GMT') return false;try{const jan=new Date(sourceDate.getFullYear(),0,1);const janOff=jan.toLocaleTimeString('en-US',{timeZone:c.tz,timeZoneName:'shortOffset'}).split('GMT')[1];const nowOff=sourceDate.toLocaleTimeString('en-US',{timeZone:c.tz,timeZoneName:'shortOffset'}).split('GMT')[1];return janOff!==nowOff}catch{return false}})(); const convH=parseInt(sourceDate.toLocaleTimeString('en-US',{timeZone:c.tz,hour:'numeric',hour12:false})); const dnIcon=getDayNightIcon(convH); html+=``; }); html+='
CityTimeDateOffsetDST
${dnIcon} ${c.flag||'\u{1F310}'} ${isFrom?'':''}${esc(c.city)}${isFrom?' (source)':''} ${t} ${d} ${off} ${isDST?'Yes':'No'}
'; results.innerHTML=html; }); // Auto-update clocks every second while panel is visible if(window._tcInterval) clearInterval(window._tcInterval); window._tcInterval=setInterval(()=>{ if(_currentView!=='time-converter'){clearInterval(window._tcInterval);return} const tbl=document.getElementById('tcClocksTable');if(!tbl) return; const rows=tbl.querySelectorAll('tbody tr'); const n=new Date(); clocks.forEach((c,i)=>{ if(!rows[i]) return; const cells=rows[i].querySelectorAll('td'); cells[1].textContent=n.toLocaleTimeString('en-US',{timeZone:c.tz,hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:true}); cells[2].textContent=n.toLocaleDateString('en-US',{timeZone:c.tz,weekday:'short',year:'numeric',month:'short',day:'numeric'}); }); },1000); } // ── Init ── document.addEventListener('DOMContentLoaded',async()=>{ loadState(); // sync from localStorage first (instant) applyTheme(state.settings.theme||'dark');applyUiToggles();restoreBarOrder(); // Then upgrade from IndexedDB if available (has full unpruned cache) try{ const hadIdb=await loadStateFromIdb(); if(hadIdb){rebuildBmSet();renderAll();console.log('State upgraded from IndexedDB (full cache)')} }catch(e){console.warn('IndexedDB load skipped:',e)} // Respect OS dark/light preference on first load if no theme set if(!state.settings.theme&&window.matchMedia){ const prefersDark=window.matchMedia('(prefers-color-scheme:dark)').matches; applyTheme(prefersDark?'dark':'light'); } document.getElementById('hotTimeSelect').value=String(state.settings.hotNewsMinutes||60); document.getElementById('refreshSelect').value=String(state.settings.refreshIntervalMinutes||30); // Restore ticker speed const ts=state.settings.tickerSpeed||60; document.getElementById('tickerSpeed').value=String(ts); applyTickerSpeed(ts); // First-load hint for keyboard shortcuts if(!localStorage.getItem('syncDash_kbHintDismissed')){ const hint=document.createElement('div'); hint.id='kbHint'; hint.style.cssText='position:fixed;bottom:1rem;left:1rem;z-index:var(--z-toast,210);background:var(--bg1);border:1px solid var(--accent);border-radius:12px;padding:.5rem .8rem;font-size:.72rem;color:var(--txt2);box-shadow:0 8px 32px rgba(0,0,0,.3);display:flex;align-items:center;gap:.5rem;animation:toastSlideIn .3s cubic-bezier(.2,1,.3,1)'; hint.innerHTML='Press ? for keyboard shortcuts \u2022 / to search \u2022 G to jump'; document.body.appendChild(hint); hint.querySelector('button').addEventListener('click',()=>{hint.remove();localStorage.setItem('syncDash_kbHintDismissed','1')}); // Auto-dismiss after 20 seconds setTimeout(()=>{if(hint.parentNode){hint.remove();localStorage.setItem('syncDash_kbHintDismissed','1')}},20000); } // Restore persisted view from session try{const sv=sessionStorage.getItem('syncDash_activeView');if(sv&&_panelMap[sv]){_currentView=sv;switchView(sv)}}catch(e){} renderAll();startAutoRefresh(); renderFOMC();initDebtBar();renderCountdownBar();renderSessionBar(); // ── Pause intervals when tab hidden (save CPU/battery) ── let _intervalsRunning=true; const _managedIntervals=[]; function managedSetInterval(fn,ms){const id=setInterval(()=>{if(_intervalsRunning) fn()},ms);_managedIntervals.push(id);return id} document.addEventListener('visibilitychange',()=>{_intervalsRunning=!document.hidden}); managedSetInterval(()=>{updateFOMCCd();updateDebtValues();updateCountdownText();updateSessionText();updateNextSyncCountdown()},1000); fetchAllQuotes().then(renderFWComm); startDataRefresh(); // uses saved dataRefreshRate setting managedSetInterval(renderInfoStrip,60000); // update clocks every 60s (HH:MM display) renderInfoStrip(); // initial render managedSetInterval(renderHotNews,30000); // refresh hot news every 30s (items age out) managedSetInterval(refreshTimestamps,60000); // refresh card timestamps every 60s // New features init renderWotdBar();renderOtdStrip();fetchWeather(); managedSetInterval(fetchWeather,600000); // refresh weather every 10min // Restore shared view from URL hash try{ const hash=location.hash; if(hash.startsWith('#view=')){ const vs=JSON.parse(atob(hash.slice(6))); if(typeof vs.cat!=='string'&&vs.cat!==undefined||typeof vs.reg!=='string'&&vs.reg!==undefined||typeof vs.search!=='string'&&vs.search!==undefined||typeof vs.view!=='string'&&vs.view!==undefined) return; if(vs.cat){const pill=document.querySelector(`#categoryPills .pill[data-cat="${CSS.escape(vs.cat)}"]`);if(pill) pill.click()} if(vs.reg){const pill=document.querySelector(`#regionPills .pill[data-region="${CSS.escape(vs.reg)}"]`);if(pill) pill.click()} if(vs.search) document.getElementById('searchInput').value=vs.search; if(vs.view){const btn=document.querySelector(`.view-toggle button[data-view="${CSS.escape(vs.view)}"]`);if(btn) btn.click()} } }catch{} syncAll(); }); window.addEventListener('beforeunload',saveNow); // ═══════════════════════════════════════════════════════════════════════════════ // INTEL DASHBOARD PANEL // ═══════════════════════════════════════════════════════════════════════════════ // ── Global Threat Bar Update ── function updateThreatBar(){ const escData=calculateEscalationIndex(); const scoreEl=document.getElementById('threatScore'); const fillEl=document.getElementById('threatMeterFill'); const hotspotsEl=document.getElementById('threatHotspots'); const updatedEl=document.getElementById('threatUpdated'); if(!scoreEl) return; if(!escData.length){scoreEl.textContent='--';fillEl.style.width='0%';hotspotsEl.innerHTML='';updatedEl.textContent='No data';return} // Global score = average of top 5 hotspots const top5=escData.slice(0,5); const globalScore=Math.round(top5.reduce((s,r)=>s+r.score,0)/top5.length*10)/10; const color=globalScore>=7?'var(--red)':globalScore>=4?'var(--amber)':globalScore>=2?'var(--purple)':'var(--green)'; scoreEl.textContent=globalScore.toFixed(1);scoreEl.style.color=color; fillEl.style.width=Math.min(globalScore*10,100)+'%'; fillEl.style.background=globalScore>=7?'var(--red)':globalScore>=4?'var(--amber)':globalScore>=2?'var(--purple)':'var(--green)'; // Show top 5 hotspots as clickable chips hotspotsEl.innerHTML=''; top5.forEach(r=>{ const chip=document.createElement('span');chip.className='threat-hotspot'; const c=r.score>=7?'var(--red)':r.score>=4?'var(--amber)':'var(--txt2)'; chip.innerHTML=''+r.score.toFixed(1)+' '+esc(r.country)+''; chip.title=r.country+': '+r.conflictSignals+' conflict signals, '+r.sourceCount+' sources'; // Click-through: filter cards/timeline to this country chip.addEventListener('click',()=>filterByCountry(r.country)); hotspotsEl.appendChild(chip); }); updatedEl.textContent='Updated '+new Date().toLocaleTimeString(); } // Click threat gauge to open Intel Dash document.getElementById('threatGauge')?.addEventListener('click',()=>switchView('intel-dash')); // Filter cards/timeline by country name (issue #11) function filterByCountry(country){ const searchInput=document.getElementById('searchInput'); searchInput.value=country; searchInput.dispatchEvent(new Event('input',{bubbles:true})); // Only switch to cards if user clicks from the threat bar (not from alerts) // Don't disrupt the current view — just set the search filter toast('Filter set: '+country+' — switch to Cards or Timeline to see results','info'); } // ── Alert Rules Engine ── if(!state.alertRules) state.alertRules=[]; function checkAlertRules(){ if(!state.alertRules.length) return; const now=Date.now();const window1h=3600000; state.alertRules.forEach(rule=>{ if(rule.lastFired&&now-rule.lastFired{ if(!src.enabled) return; (state.cache[src.id]||[]).forEach(it=>{ if(!it.pubDate) return; const d=new Date(it.pubDate).getTime(); if(now-d>window1h) return; const text=(it.title||'')+' '+(it.description||''); const allMatch=rule.keywords.every(kw=>text.toLowerCase().includes(kw.toLowerCase())); if(allMatch){matchCount++;matchSources.add(src.name)} }); }); if(matchSources.size>=rule.minSources){ rule.lastFired=now; saveState(); toast('\u{1F6A8} ALERT: "'+rule.keywords.join(' + ')+'" detected in '+matchSources.size+' sources!','err'); // Browser notification if enabled if(Notification.permission==='granted'){ new Notification('Intel Alert',{body:rule.keywords.join(' + ')+' — '+matchSources.size+' sources in last hour',icon:'\u{1F6A8}'}); } } }); } function addAlertRule(keywords,minSources,cooldownMin){ state.alertRules.push({keywords,minSources:minSources||3,cooldownMs:(cooldownMin||30)*60000,lastFired:null,createdAt:new Date().toISOString()}); saveState(); toast('Alert rule added: '+keywords.join(' + '),'ok'); } // ── SVG Mini-Chart Helpers ── function svgBarChart(data,{width=200,height=40,barColor='var(--accent)',bgColor='var(--bg3)',label=''}={}){ if(!data.length) return ''; const max=Math.max(...data,1);const barW=Math.max(2,Math.floor((width-data.length)/data.length)); let bars='';data.forEach((v,i)=>{ const bh=Math.max(1,Math.round(v/max*(height-2)));const x=i*(barW+1);const y=height-bh; bars+=`${label?label+' ':''}${v}`; }); return `${bars}`; } function svgSparkline(data,{width=120,height=24,color='var(--accent)',fillColor='rgba(56,189,248,.1)'}={}){ if(data.length<2) return ''; const max=Math.max(...data,1);const min=Math.min(...data,0);const range=max-min||1; const step=width/(data.length-1); let path='M0,'+Math.round((1-(data[0]-min)/range)*(height-2)+1); let area='M0,'+height+' L0,'+Math.round((1-(data[0]-min)/range)*(height-2)+1); data.forEach((v,i)=>{ if(i===0) return; const x=Math.round(i*step);const y=Math.round((1-(v-min)/range)*(height-2)+1); path+=' L'+x+','+y;area+=' L'+x+','+y; }); area+=' L'+width+','+height+' Z'; return ``; } function svgSentimentGauge(pos,neg,neut,{width=160,height=12}={}){ const total=pos+neg+neut||1; const pw=Math.round(pos/total*width);const nw=Math.round(neg/total*width);const uw=width-pw-nw; return ``; } function svgHourHistogram(items,{width=200,height=30}={}){ const hours=new Array(24).fill(0); items.forEach(it=>{if(it.pubDate){const h=new Date(it.pubDate).getHours();hours[h]++}}); return svgBarChart(hours,{width,height,barColor:'var(--accent)',label:'Hour'}); } function renderIntelDash(){ const panel=document.getElementById('intelDashPanel');panel.innerHTML=''; const h=document.createElement('h3');h.style.cssText='font-size:.95rem;margin-bottom:.6rem;color:var(--accent)';h.textContent='\u{1F4CA} Intelligence Analysis Dashboard';panel.appendChild(h); // ── Threat Level / Escalation Index ── const escH=document.createElement('div');escH.className='prem-section-head';escH.textContent='\u26A0 Conflict Escalation Index (48h)';panel.appendChild(escH); const escData=calculateEscalationIndex(); if(!escData.length){ const noData=document.createElement('p');noData.style.cssText='color:var(--txt3);font-size:.74rem;padding:.3rem 0';noData.textContent='No conflict signals detected. Sync sources first.';panel.appendChild(noData); } else { const escGrid=document.createElement('div');escGrid.style.cssText='display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:.5rem;margin-bottom:.8rem'; escData.slice(0,12).forEach(r=>{ const card=document.createElement('div'); const color=r.score>=7?'var(--red)':r.score>=4?'var(--amber)':r.score>=2?'var(--purple)':'var(--green)'; const bg=r.score>=7?'rgba(248,113,113,.08)':r.score>=4?'rgba(251,191,36,.08)':'rgba(56,189,248,.04)'; card.style.cssText='background:'+bg+';border:1px solid var(--border);border-radius:8px;padding:.5rem .7rem;border-left:3px solid '+color; const pct=Math.min(r.score*10,100); card.innerHTML='
'+esc(r.country)+''+r.score.toFixed(1)+'
\u2694 '+r.conflictSignals+' conflict | \u{1F4C9} '+r.economicSignals+' economic | '+r.sourceCount+' sources
'; escGrid.appendChild(card); }); panel.appendChild(escGrid); } // ── Sentiment Overview + Activity Charts ── const chartH=document.createElement('div');chartH.className='prem-section-head';chartH.style.marginTop='.8rem';chartH.textContent='\u{1F4CA} Sentiment & Activity Overview';panel.appendChild(chartH); { const allItems=[]; state.sources.forEach(src=>{if(!src.enabled) return;(state.cache[src.id]||[]).forEach(it=>{if(it.pubDate) allItems.push({...it,pubDate:new Date(it.pubDate)})})}); const totalPos=allItems.filter(it=>analyzeSentiment((it.title||'')+' '+(it.description||'')).score>=1).length; const totalNeg=allItems.filter(it=>analyzeSentiment((it.title||'')+' '+(it.description||'')).score<=-1).length; const totalNeut=allItems.length-totalPos-totalNeg; const chartRow=document.createElement('div');chartRow.style.cssText='display:flex;gap:1rem;flex-wrap:wrap;align-items:flex-start;margin-bottom:.6rem'; // Sentiment gauge const sentBox=document.createElement('div');sentBox.style.cssText='background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:.5rem .7rem;flex:1;min-width:180px'; sentBox.innerHTML='
Sentiment Distribution
\u25B2 '+totalPos+''+totalNeut+'\u25BC '+totalNeg+'
'+svgSentimentGauge(totalPos,totalNeg,totalNeut,{width:180,height:10}); chartRow.appendChild(sentBox); // Hourly activity histogram const actBox=document.createElement('div');actBox.style.cssText='background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:.5rem .7rem;flex:1;min-width:220px'; actBox.innerHTML='
Activity by Hour (all items)
'+svgHourHistogram(allItems,{width:210,height:32})+'
0h6h12h18h23h
'; chartRow.appendChild(actBox); // Daily trend sparkline (last 7 days) const dayBuckets={};const now=Date.now(); for(let d=6;d>=0;d--){const key=new Date(now-d*86400000).toISOString().slice(0,10);dayBuckets[key]=0} allItems.forEach(it=>{const key=it.pubDate.toISOString().slice(0,10);if(dayBuckets[key]!==undefined) dayBuckets[key]++}); const dayData=Object.values(dayBuckets); const sparkBox=document.createElement('div');sparkBox.style.cssText='background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:.5rem .7rem;min-width:150px'; sparkBox.innerHTML='
7-Day Activity Trend
'+svgSparkline(dayData,{width:140,height:28})+'
7d agoToday
'; chartRow.appendChild(sparkBox); panel.appendChild(chartRow); } // ── Cross-Source Correlation ── const corrH=document.createElement('div');corrH.className='prem-section-head';corrH.style.marginTop='.8rem';corrH.textContent='\u{1F517} Cross-Source Story Correlation (24h)';panel.appendChild(corrH); const corrData=findCorrelatedStories(); if(!corrData.length){ const noCorr=document.createElement('p');noCorr.style.cssText='color:var(--txt3);font-size:.74rem;padding:.3rem 0';noCorr.textContent='No multi-source correlations found yet.';panel.appendChild(noCorr); } else { corrData.slice(0,10).forEach(story=>{ const div=document.createElement('div');div.style.cssText='padding:.4rem 0;border-bottom:1px solid var(--bg2);font-size:.76rem'; div.innerHTML='
'+esc(story.keyword)+''+story.sourceCount+' sources'+story.totalMentions+' mentions
'; const itemList=document.createElement('div');itemList.style.cssText='margin-top:.2rem;padding-left:.5rem'; story.items.slice(0,3).forEach(it=>{ const a=document.createElement('a');a.href=it.link;a.target='_blank';a.rel='noopener';a.style.cssText='display:block;font-size:.7rem;color:var(--txt2);margin:.1rem 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'; a.textContent=it.source+': '+it.title;itemList.appendChild(a); }); div.appendChild(itemList);panel.appendChild(div); }); } // ── Keyword Trends ── const trendH=document.createElement('div');trendH.className='prem-section-head';trendH.style.marginTop='.8rem';trendH.textContent='\u{1F4C8} Trending Keywords (7-day spike detection)';panel.appendChild(trendH); const trends=getKeywordTrends(); if(!trends.length){ const noTrend=document.createElement('p');noTrend.style.cssText='color:var(--txt3);font-size:.74rem;padding:.3rem 0';noTrend.textContent='Not enough historical data. Trends populate after multiple syncs over days.';panel.appendChild(noTrend); } else { const tGrid=document.createElement('div');tGrid.style.cssText='display:flex;flex-wrap:wrap;gap:.3rem;margin-bottom:.5rem'; trends.forEach(t=>{ const chip=document.createElement('span'); const spikeColor=t.spike>=5?'var(--red)':t.spike>=3?'var(--amber)':'var(--green)'; chip.style.cssText='display:inline-flex;align-items:center;gap:.2rem;padding:.15rem .45rem;border-radius:12px;font-size:.68rem;border:1px solid var(--border);background:var(--bg2);cursor:default'; chip.innerHTML=''+esc(t.keyword)+'\u2191'+t.spike.toFixed(1)+'x'+t.todayCount+' today'; chip.title=t.keyword+': '+t.todayCount+' mentions today vs '+t.avgCount+' avg'; tGrid.appendChild(chip); }); panel.appendChild(tGrid); } // ── Source Reliability ── const relH=document.createElement('div');relH.className='prem-section-head';relH.style.marginTop='.8rem';relH.textContent='\u{1F3AF} Source Reliability Scores';panel.appendChild(relH); const relNote=document.createElement('div');relNote.style.cssText='font-size:.68rem;color:var(--txt3);margin-bottom:.4rem';relNote.textContent='Click a score to rate source reliability (0=unreliable, 100=highly trusted). Scores are saved locally.';panel.appendChild(relNote); const relGrid=document.createElement('div');relGrid.style.cssText='display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:.3rem'; state.sources.filter(s=>s.enabled).sort((a,b)=>a.name.localeCompare(b.name)).forEach(src=>{ const rel=getSourceReliability(src.id); const row=document.createElement('div');row.style.cssText='display:flex;align-items:center;gap:.3rem;padding:.2rem .4rem;border-radius:6px;background:var(--bg2);font-size:.7rem;border:1px solid var(--border)'; const color=rel.score>=75?'var(--green)':rel.score>=50?'var(--amber)':rel.score>=25?'var(--red)':'var(--txt3)'; row.innerHTML=''+esc(src.icon||'\u{1F310}')+''+esc(src.name)+''; const scoreBtn=document.createElement('button');scoreBtn.style.cssText='background:none;border:1px solid '+color+';color:'+color+';font-family:var(--mono);font-size:.66rem;font-weight:700;padding:.1rem .3rem;border-radius:6px;cursor:pointer;min-width:32px;text-align:center'; scoreBtn.textContent=rel.score;scoreBtn.title='Click to rate reliability'; scoreBtn.addEventListener('click',()=>{ const newScore=prompt('Reliability score for '+src.name+' (0-100):',rel.score); if(newScore===null) return; const notes=prompt('Notes (optional):',rel.notes||''); setSourceReliability(src.id,parseInt(newScore)||50,notes||''); renderIntelDash(); }); row.appendChild(scoreBtn);relGrid.appendChild(row); }); panel.appendChild(relGrid); // ── OPSEC Tools ── const opsH=document.createElement('div');opsH.className='prem-section-head';opsH.style.marginTop='.8rem';opsH.textContent='\u{1F512} OPSEC Tools';panel.appendChild(opsH); const opsGrid=document.createElement('div');opsGrid.style.cssText='display:flex;gap:.4rem;flex-wrap:wrap'; const mkOpsBtn=(label,icon,fn)=>{const b=document.createElement('button');b.className='btn btn-sm';b.textContent=icon+' '+label;b.addEventListener('click',fn);opsGrid.appendChild(b)}; mkOpsBtn('Encrypted Export','\u{1F512}',exportEncrypted); mkOpsBtn('Encrypted Import','\u{1F513}',importEncrypted); mkOpsBtn('Clear All Data','\u{1F5D1}',()=>{if(confirm('Delete ALL dashboard data including sources, bookmarks, and settings?')){localStorage.clear();location.reload()}}); mkOpsBtn('Audit API Keys','\u{1F50D}',()=>{const keys=state.settings.apiKeys||{};const count=Object.values(keys).filter(Boolean).length;toast(count+' API keys stored in localStorage (unencrypted)','info')}); panel.appendChild(opsGrid); // ── Alert Rules ── const alertH=document.createElement('div');alertH.className='prem-section-head';alertH.style.marginTop='.8rem';alertH.textContent='\u{1F6A8} Alert Rules (auto-notify when conditions met)';panel.appendChild(alertH); const alertNote=document.createElement('div');alertNote.style.cssText='font-size:.68rem;color:var(--txt3);margin-bottom:.4rem';alertNote.textContent='Define keyword combinations. Alert fires when keywords appear together in N+ sources within 1 hour.';panel.appendChild(alertNote); // Add rule form const alertForm=document.createElement('div');alertForm.style.cssText='display:flex;gap:.3rem;align-items:center;margin-bottom:.5rem;flex-wrap:wrap'; alertForm.innerHTML=''; panel.appendChild(alertForm); setTimeout(()=>{ document.getElementById('alertAddBtn')?.addEventListener('click',()=>{ const kw=document.getElementById('alertKeywords').value.trim(); if(!kw){toast('Enter keywords','err');return} const keywords=kw.split(',').map(s=>s.trim()).filter(Boolean); const minSrc=parseInt(document.getElementById('alertMinSources').value)||3; addAlertRule(keywords,minSrc,30); document.getElementById('alertKeywords').value=''; renderIntelDash(); }); },0); // Existing rules if(state.alertRules.length){ const ruleList=document.createElement('div');ruleList.style.cssText='display:flex;flex-direction:column;gap:.2rem'; state.alertRules.forEach((rule,i)=>{ const div=document.createElement('div');div.style.cssText='display:flex;align-items:center;gap:.4rem;padding:.25rem .4rem;border-radius:6px;background:var(--bg2);border:1px solid var(--border);font-size:.72rem'; div.innerHTML='\u{1F6A8}'+esc(rule.keywords.join(' + '))+''+rule.minSources+'+ sources'; const rmBtn=document.createElement('button');rmBtn.className='btn btn-sm';rmBtn.style.cssText='font-size:.6rem;color:var(--red);padding:.1rem .3rem';rmBtn.textContent='\u2716'; rmBtn.addEventListener('click',()=>{state.alertRules.splice(i,1);saveState();renderIntelDash()}); div.appendChild(rmBtn);ruleList.appendChild(div); }); panel.appendChild(ruleList); } } // ═══════════════════════════════════════════════════════════════════════════════ // OSINT ANALYTICS ENGINE // ═══════════════════════════════════════════════════════════════════════════════ // ── Entity Extraction (lightweight regex-based NER) ── const KNOWN_COUNTRIES=['Afghanistan','Albania','Algeria','Argentina','Armenia','Australia','Austria','Azerbaijan','Bahrain','Bangladesh','Belarus','Belgium','Bolivia','Bosnia','Brazil','Bulgaria','Cambodia','Cameroon','Canada','Chad','Chile','China','Colombia','Congo','Costa Rica','Croatia','Cuba','Cyprus','Czech Republic','Denmark','Ecuador','Egypt','El Salvador','Estonia','Ethiopia','Finland','France','Georgia','Germany','Ghana','Greece','Guatemala','Haiti','Honduras','Hungary','Iceland','India','Indonesia','Iran','Iraq','Ireland','Israel','Italy','Jamaica','Japan','Jordan','Kazakhstan','Kenya','Kosovo','Kuwait','Kyrgyzstan','Laos','Latvia','Lebanon','Libya','Lithuania','Luxembourg','Madagascar','Malaysia','Mali','Mexico','Moldova','Mongolia','Montenegro','Morocco','Mozambique','Myanmar','Nepal','Netherlands','New Zealand','Nicaragua','Niger','Nigeria','North Korea','North Macedonia','Norway','Oman','Pakistan','Palestine','Panama','Paraguay','Peru','Philippines','Poland','Portugal','Qatar','Romania','Russia','Rwanda','Saudi Arabia','Senegal','Serbia','Singapore','Slovakia','Slovenia','Somalia','South Africa','South Korea','Spain','Sri Lanka','Sudan','Sweden','Switzerland','Syria','Taiwan','Tajikistan','Tanzania','Thailand','Tunisia','Turkey','Turkmenistan','Uganda','Ukraine','United Arab Emirates','United Kingdom','United States','Uruguay','Uzbekistan','Venezuela','Vietnam','Yemen','Zambia','Zimbabwe']; const KNOWN_ORGS=['NATO','UN','EU','ASEAN','BRICS','G7','G20','IMF','World Bank','WHO','WTO','IAEA','OPCW','ICC','ICJ','OSCE','SWIFT','FATF','Interpol','Europol','FBI','CIA','NSA','DIA','MI6','GCHQ','Mossad','FSB','GRU','PLA','ODNI','DHS','DOJ','SEC','CFTC','FINRA','OFAC','FinCEN','DARPA','NASA','SpaceX','TSMC','Huawei','Lockheed Martin','Raytheon','Northrop Grumman','Boeing','Airbus','Wagner','Hezbollah','Hamas','Houthi','Taliban','ISIS','Al-Qaeda']; const CONFLICT_KEYWORDS=['war','invasion','missile','strike','bomb','troops','military','deploy','escalat','conflict','attack','combat','offensive','defense','weapon','nuclear','ballistic','drone','airstrike','ceasefire','siege','occupation','sanction','embargo','blockade','naval','submarine','warship','fighter jet','tank','artillery','shell','casualties','killed','wounded','hostage','terrorism','insurgent','guerrilla','militia','coup','martial law']; const ECONOMIC_STRESS=['recession','inflation','default','debt crisis','bank run','currency crash','trade war','tariff','embargo','supply chain','shortage','layoffs','unemployment','bankruptcy','credit crunch','bail out','austerity','stagflation','devaluation']; const _entityCache=new Map(); // Disambiguation: words that indicate the entity is NOT the country const _COUNTRY_FALSE_POS={ 'Georgia':/(atlanta|peach|bulldogs|savannah|tbilisi\b)/i, 'Turkey':/(thanksgiving|dinner|recipe|roast|stuffing|cranberry)/i, 'China':/(dinnerware|porcelain|cabinet|plate|bone\s*china)/i, 'Jordan':/(michael\s+jordan|jordan\s+brand|air\s+jordan|jordan\s+peterson|jordan\s+shoes)/i, 'Chad':/(chad\s+smith|chad\s+johnson|virgin.*chad|chad\s+ochocinco)/i, 'Mali':/(tamale|somali|anomal)/i, 'Niger':/(nigeria)/i, 'Panama':/(panama\s+hat|van\s+halen)/i, 'Monaco':/(monaco\s+grand\s+prix|formula|f1)/i, 'Cuba':/(cuba\s+libre|cuba\s+gooding)/i }; // Disambiguation: context words that CONFIRM the entity IS the country const _COUNTRY_CONTEXT=/\b(government|president|minister|military|troops|embassy|capital|border|sanction|regime|opposition|parliament|election|diplomacy|invasion|conflict|GDP|economy|leader|prime\s+minister|foreign\s+affairs|UN\s+security)\b/i; function extractEntities(text){ if(!text) return {countries:[],orgs:[],conflicts:0,economic:0}; const cacheKey=text.substring(0,200); if(_entityCache.has(cacheKey)) return _entityCache.get(cacheKey); if(_entityCache.size>5000) _entityCache.clear(); const countries=[];const orgs=[];let conflicts=0;let economic=0; const lower=text.toLowerCase(); const hasContext=_COUNTRY_CONTEXT.test(text); KNOWN_COUNTRIES.forEach(c=>{ if(!text.includes(c)&&!lower.includes(c.toLowerCase())) return; // Skip false positives for ambiguous country names const fpRx=_COUNTRY_FALSE_POS[c]; if(fpRx&&fpRx.test(text)&&!hasContext) return; // For short/ambiguous names, require word boundary match if(c.length<=5&&!/\b/.test('')){ const rx=new RegExp('\\b'+c.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')+'\\b','i'); if(!rx.test(text)) return; } countries.push(c); }); KNOWN_ORGS.forEach(o=>{ // Org names are typically uppercase acronyms — require exact case or word boundary if(o.length<=4){if(!text.includes(o)) return} else{if(!text.includes(o)&&!lower.includes(o.toLowerCase())) return} orgs.push(o); }); CONFLICT_KEYWORDS.forEach(k=>{if(lower.includes(k)) conflicts++}); ECONOMIC_STRESS.forEach(k=>{if(lower.includes(k)) economic++}); const result={countries:[...new Set(countries)],orgs:[...new Set(orgs)],conflicts,economic}; _entityCache.set(cacheKey,result); return result; } // ── Cross-Source Correlation ── function findCorrelatedStories(){ const storyMap={};const now=Date.now();const window24h=24*3600000; state.sources.forEach(src=>{ if(!src.enabled) return; (state.cache[src.id]||[]).forEach(it=>{ if(!it.pubDate) return; const d=new Date(it.pubDate).getTime(); if(now-d>window24h) return; // Create simplified key from title words (top 4 non-stop words) const stops=new Set(['the','a','an','in','on','at','to','for','of','and','or','is','are','was','were','has','have','had','with','from','by','as','its','that','this','it','be','not','but','will','can','may','could','would','should']); const words=(it.title||'').toLowerCase().replace(/[^a-z0-9\s]/g,'').split(/\s+/).filter(w=>w.length>3&&!stops.has(w)).slice(0,5); words.forEach(w=>{ if(!storyMap[w]) storyMap[w]={count:0,sources:new Set(),items:[]}; storyMap[w].count++; storyMap[w].sources.add(src.name); if(storyMap[w].items.length<5) storyMap[w].items.push({title:it.title,source:src.name,link:it.link,pubDate:it.pubDate}); }); }); }); // Return keywords appearing in 3+ different sources (high correlation) return Object.entries(storyMap) .filter(([k,v])=>v.sources.size>=3&&v.count>=4) .sort((a,b)=>b[1].sources.size-a[1].sources.size) .slice(0,20) .map(([keyword,data])=>({keyword,sourceCount:data.sources.size,totalMentions:data.count,sources:[...data.sources],items:data.items})); } // ── Conflict Escalation Index ── function calculateEscalationIndex(){ const regions={};const now=Date.now();const window48h=48*3600000; state.sources.forEach(src=>{ if(!src.enabled) return; (state.cache[src.id]||[]).forEach(it=>{ if(!it.pubDate) return; const d=new Date(it.pubDate).getTime(); if(now-d>window48h) return; const ent=extractEntities((it.title||'')+' '+(it.description||'')); ent.countries.forEach(c=>{ if(!regions[c]) regions[c]={conflict:0,economic:0,mentions:0,sources:new Set()}; regions[c].conflict+=ent.conflicts; regions[c].economic+=ent.economic; regions[c].mentions++; regions[c].sources.add(src.name); }); }); }); // Calculate score 0-10 for each region return Object.entries(regions) .map(([country,data])=>{ const conflictScore=Math.min(data.conflict/5,5); const econScore=Math.min(data.economic/3,3); const breadth=Math.min(data.sources.size/3,2); const score=Math.min(Math.round((conflictScore+econScore+breadth)*10)/10,10); return {country,score,conflictSignals:data.conflict,economicSignals:data.economic,mentions:data.mentions,sourceCount:data.sources.size}; }) .filter(r=>r.score>0.5) .sort((a,b)=>b.score-a.score) .slice(0,25); } // ── Keyword Trending ── function getKeywordTrends(){ const dayBuckets={};const now=Date.now(); for(let d=0;d<7;d++){ const dayKey=new Date(now-d*86400000).toISOString().slice(0,10); dayBuckets[dayKey]={}; } state.sources.forEach(src=>{ if(!src.enabled) return; (state.cache[src.id]||[]).forEach(it=>{ if(!it.pubDate) return; const dayKey=new Date(it.pubDate).toISOString().slice(0,10); if(!dayBuckets[dayKey]) return; const ent=extractEntities((it.title||'')+' '+(it.description||'')); [...ent.countries,...ent.orgs].forEach(k=>{ dayBuckets[dayKey][k]=(dayBuckets[dayKey][k]||0)+1; }); }); }); // Find keywords with biggest increase in last 24h vs previous 6 days const days=Object.keys(dayBuckets).sort().reverse(); if(days.length<2) return []; const today=dayBuckets[days[0]]||{}; const avgPrev={}; days.slice(1).forEach(d=>{ Object.entries(dayBuckets[d]||{}).forEach(([k,v])=>{avgPrev[k]=(avgPrev[k]||0)+v}); }); Object.keys(avgPrev).forEach(k=>avgPrev[k]/=Math.max(days.length-1,1)); return Object.entries(today) .map(([keyword,count])=>{ const avg=avgPrev[keyword]||0; const spike=avg>0?count/avg:count>2?count:0; return {keyword,todayCount:count,avgCount:Math.round(avg*10)/10,spike:Math.round(spike*10)/10}; }) .filter(t=>t.spike>1.5||t.todayCount>=3) .sort((a,b)=>b.spike-a.spike) .slice(0,20); } // ── Source Reliability Scoring ── if(!state.sourceReliability) state.sourceReliability={}; function getSourceReliability(srcId){ return state.sourceReliability[srcId]||{score:50,notes:'',lastReviewed:null}; } function setSourceReliability(srcId,score,notes){ state.sourceReliability[srcId]={score:Math.max(0,Math.min(100,score)),notes,lastReviewed:new Date().toISOString()}; saveState(); } // ── OPSEC: Error sanitization ── const _origToast=toast; function sanitizedToast(msg,type){ // Redact URLs from error messages shown to user const clean=typeof msg==='string'?msg.replace(/https?:\/\/[^\s)]+/g,'[URL]'):msg; _origToast(clean,type); } // ── OPSEC: Encrypted export ── function exportEncrypted(){ const password=prompt('Enter encryption password for export:'); if(!password) return; const data=JSON.stringify({sources:state.sources,bookmarks:state.bookmarks,settings:state.settings,sourceReliability:state.sourceReliability}); // Simple XOR-based obfuscation (not cryptographic - use for basic protection) let encrypted=''; for(let i=0;i{ const file=e.target.files[0];if(!file) return; const reader=new FileReader(); reader.onload=ev=>{ try{ const raw=atob(ev.target.result); let decrypted=''; for(let i=0;i