2026-02-23 12:20:48 -05:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Phoenix Intelligence Dashboard</ title >
2026-02-23 12:49:11 -05:00
< link rel = "icon" href = "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><text y='14' font-size='14'>🔥</text></svg>" >
2026-02-23 12:20:48 -05:00
< script src = "https://cdn.jsdelivr.net/npm/chart.js@4" ></ script >
< script src = "https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js" ></ script >
2026-02-23 12:49:11 -05:00
< link rel = "stylesheet" href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
< script src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" ></ script >
2026-02-23 12:20:48 -05:00
< style >
* { margin : 0 ; padding : 0 ; box-sizing : border-box ; }
body { font-family : - apple-system , BlinkMacSystemFont , 'SF Mono' , 'Fira Code' , monospace ; background : #0a0e1a ; color : #c8d6e5 ; line-height : 1.5 ; }
a { color : #60a5fa ; text-decoration : none ; }
/* Status bar */
. status-bar {
position : sticky ; top : 0 ; z-index : 100 ;
display : flex ; justify-content : space-between ; align-items : center ;
padding : 0.5 rem 1.5 rem ;
background : linear-gradient ( 135 deg , #0f172a 0 % , #1a1f36 100 % );
border-bottom : 1 px solid #2d3748 ;
font-size : 0.8 rem ;
}
. status-bar . title { font-size : 1 rem ; font-weight : 700 ; color : #f1f5f9 ; letter-spacing : 0.5 px ; }
. status-bar . title span { color : #f59e0b ; }
. status-dot { display : inline-block ; width : 8 px ; height : 8 px ; border-radius : 50 % ; margin-right : 6 px ; }
. dot-green { background : #22c55e ; box-shadow : 0 0 6 px #22c55e ; }
. dot-yellow { background : #f59e0b ; box-shadow : 0 0 6 px #f59e0b ; }
. dot-red { background : #ef4444 ; box-shadow : 0 0 6 px #ef4444 ; }
. pulse { animation : pulse 2 s ease-in-out infinite ; }
@ keyframes pulse { 0 %, 100 % { opacity : 1 ; } 50 % { opacity : 0.4 ; } }
2026-02-23 12:49:11 -05:00
/* Navigation tabs */
. nav-tabs {
display : flex ; gap : 0 ; background : #0f172a ; border-bottom : 1 px solid #1e293b ;
padding : 0 1.5 rem ; overflow-x : auto ;
}
. nav-tab {
padding : 0.6 rem 1.2 rem ; font-size : 0.78 rem ; font-weight : 500 ;
color : #64748b ; cursor : pointer ; border-bottom : 2 px solid transparent ;
white-space : nowrap ; transition : all 0.2 s ;
}
. nav-tab : hover { color : #94a3b8 ; }
. nav-tab . active { color : #f59e0b ; border-bottom-color : #f59e0b ; }
/* Sections */
. section { display : none ; }
. section . active { display : block ; }
2026-02-23 12:20:48 -05:00
/* Grid layout */
. container { max-width : 1600 px ; margin : 0 auto ; padding : 1 rem ; }
. grid { display : grid ; gap : 1 rem ; }
2026-02-23 12:49:11 -05:00
. grid-4 { grid-template-columns : repeat ( 4 , 1 fr ); }
2026-02-23 12:20:48 -05:00
. grid-3 { grid-template-columns : repeat ( 3 , 1 fr ); }
. grid-2 { grid-template-columns : repeat ( 2 , 1 fr ); }
/* Cards */
. card {
background : linear-gradient ( 180 deg , #111827 0 % , #0f1523 100 % );
border : 1 px solid #1e293b ;
border-radius : 8 px ;
padding : 1 rem ;
overflow : hidden ;
transition : border-color 0.3 s ;
}
. card : hover { border-color : #334155 ; }
. card-header {
display : flex ; justify-content : space-between ; align-items : center ;
margin-bottom : 0.75 rem ; padding-bottom : 0.5 rem ;
border-bottom : 1 px solid #1e293b ;
}
. card-title { font-size : 0.85 rem ; font-weight : 600 ; color : #94a3b8 ; text-transform : uppercase ; letter-spacing : 1 px ; }
. card-badge { font-size : 0.7 rem ; padding : 2 px 8 px ; border-radius : 4 px ; background : #1e293b ; color : #64748b ; }
. card-error { color : #64748b ; font-style : italic ; font-size : 0.85 rem ; padding : 1 rem 0 ; }
. span-2 { grid-column : span 2 ; }
2026-02-23 12:49:11 -05:00
. span-3 { grid-column : span 3 ; }
2026-02-23 12:20:48 -05:00
/* Tables */
table { width : 100 % ; border-collapse : collapse ; font-size : 0.82 rem ; }
th { color : #64748b ; font-weight : 500 ; text-align : left ; padding : 0.35 rem 0.5 rem ; font-size : 0.75 rem ; text-transform : uppercase ; letter-spacing : 0.5 px ; }
td { padding : 0.35 rem 0.5 rem ; border-top : 1 px solid #1a2236 ; white-space : nowrap ; }
tr : hover td { background : rgba ( 255 , 255 , 255 , 0.02 ); }
/* Values */
. up { color : #22c55e ; }
. down { color : #ef4444 ; }
. muted { color : #4a5568 ; }
. bright { color : #f1f5f9 ; font-weight : 600 ; }
. warn { color : #f59e0b ; }
/* Stat boxes */
. stat-row { display : flex ; gap : 0.75 rem ; flex-wrap : wrap ; }
. stat-box {
flex : 1 ; min-width : 100 px ;
background : #0d1117 ; border : 1 px solid #1e293b ; border-radius : 6 px ;
padding : 0.75 rem ; text-align : center ;
}
. stat-value { font-size : 1.6 rem ; font-weight : 700 ; color : #f1f5f9 ; }
. stat-label { font-size : 0.7 rem ; color : #64748b ; text-transform : uppercase ; margin-top : 0.25 rem ; }
/* Heatmap cells */
. heatmap { display : flex ; flex-wrap : wrap ; gap : 4 px ; }
. hm-cell {
padding : 6 px 10 px ; border-radius : 4 px ; font-size : 0.78 rem ;
font-weight : 600 ; min-width : 90 px ; text-align : center ;
}
/* Tags */
. tag { display : inline-block ; padding : 2 px 8 px ; border-radius : 4 px ; font-size : 0.75 rem ; margin : 2 px ; background : #1a2236 ; color : #94a3b8 ; }
. tag-hot { background : #7f1d1d ; color : #fca5a5 ; }
/* Scrollable */
. scroll-box { max-height : 280 px ; overflow-y : auto ; }
. scroll-box :: -webkit-scrollbar { width : 4 px ; }
. scroll-box :: -webkit-scrollbar-thumb { background : #334155 ; border-radius : 2 px ; }
/* Severity badges */
. sev-critical { color : #ef4444 ; font-weight : 700 ; }
. sev-high { color : #f59e0b ; }
. sev-medium { color : #6b7280 ; }
2026-02-23 12:49:11 -05:00
/* Risk levels */
. risk-extreme { color : #ef4444 ; font-weight : 700 ; }
. risk-high { color : #f97316 ; font-weight : 600 ; }
. risk-elevated { color : #f59e0b ; }
. risk-moderate { color : #64748b ; }
. risk-low { color : #22c55e ; }
/* Map */
# geoMap { height : 400 px ; border-radius : 8 px ; border : 1 px solid #1e293b ; }
. leaflet-container { background : #0a0e1a ; }
. map-legend { position : absolute ; bottom : 10 px ; right : 10 px ; background : rgba ( 15 , 23 , 42 , 0.9 ); padding : 8 px 12 px ; border-radius : 6 px ; font-size : 0.75 rem ; z-index : 1000 ; border : 1 px solid #1e293b ; }
. map-legend div { margin : 2 px 0 ; }
. legend-dot { display : inline-block ; width : 10 px ; height : 10 px ; border-radius : 50 % ; margin-right : 6 px ; vertical-align : middle ; }
/* Charts */
. chart-container { position : relative ; height : 220 px ; }
2026-02-23 12:20:48 -05:00
/* Loading overlay */
# loading {
position : fixed ; inset : 0 ; z-index : 200 ;
display : flex ; align-items : center ; justify-content : center ;
background : #0a0e1a ; transition : opacity 0.5 s ;
}
# loading . hidden { opacity : 0 ; pointer-events : none ; }
. loader { width : 40 px ; height : 40 px ; border : 3 px solid #1e293b ; border-top-color : #f59e0b ; border-radius : 50 % ; animation : spin 1 s linear infinite ; }
@ keyframes spin { to { transform : rotate ( 360 deg ); } }
/* Responsive */
2026-02-23 12:49:11 -05:00
@ media ( max-width : 1200px ) { . grid-4 { grid-template-columns : repeat ( 2 , 1 fr ); } . grid-3 { grid-template-columns : repeat ( 2 , 1 fr ); } . span-3 { grid-column : span 2 ; } }
@ media ( max-width : 800px ) { . grid-4 , . grid-3 , . grid-2 { grid-template-columns : 1 fr ; } . span-2 , . span-3 { grid-column : span 1 ; } }
2026-02-23 12:20:48 -05:00
</ style >
</ head >
< body >
< div id = "loading" >< div class = "loader" ></ div ></ div >
<!-- Status Bar -->
< div class = "status-bar" >
< div class = "title" >< span > PHOENIX</ span > INTELLIGENCE DASHBOARD</ div >
< div >
< span class = "status-dot dot-green pulse" id = "connDot" ></ span >
< span id = "connLabel" > Connecting...</ span >
2026-02-23 12:49:11 -05:00
· < span id = "sourceHealth" > --</ span >
· < span id = "lastUpdate" > --</ span >
· < span id = "feedCount" > --</ span >
2026-02-23 12:20:48 -05:00
</ div >
</ div >
2026-02-23 12:49:11 -05:00
<!-- Navigation -->
< div class = "nav-tabs" >
< div class = "nav-tab active" data-tab = "overview" > Overview</ div >
< div class = "nav-tab" data-tab = "markets" > Markets</ div >
< div class = "nav-tab" data-tab = "security" > Security</ div >
< div class = "nav-tab" data-tab = "conflict" > Conflict & Humanitarian</ div >
< div class = "nav-tab" data-tab = "natural" > Natural & Climate</ div >
< div class = "nav-tab" data-tab = "geomap" > Geospatial Map</ div >
< div class = "nav-tab" data-tab = "charts" > Trend Charts</ div >
</ div >
2026-02-23 12:20:48 -05:00
< div class = "container" >
2026-02-23 12:49:11 -05:00
<!-- ======================= OVERVIEW TAB ======================= -->
< div class = "section active" id = "tab-overview" >
< div class = "stat-row" style = "margin-top:1rem;" id = "overviewStats" ></ div >
2026-02-23 12:20:48 -05:00
< div class = "grid grid-3" style = "margin-top:1rem;" >
< div class = "card" >
2026-02-23 12:49:11 -05:00
< div class = "card-header" >< span class = "card-title" > Equity Indices</ span >< span class = "card-badge" id = "equityBadge" > --</ span ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Symbol</ th >< th > Price</ th >< th > Chg</ th >< th > %</ th ></ tr ></ thead >< tbody id = "equityBody" ></ tbody ></ table ></ div >
2026-02-23 12:20:48 -05:00
</ div >
< div class = "card" >
2026-02-23 12:49:11 -05:00
< div class = "card-header" >< span class = "card-title" > Cyber Threats</ span >< span class = "card-badge" id = "cyberBadge" > --</ span ></ div >
< div id = "cyberStats" class = "stat-row" style = "margin-bottom:0.75rem;" ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Threat</ th >< th > Type</ th >< th > Severity</ th ></ tr ></ thead >< tbody id = "cyberBody" ></ tbody ></ table ></ div >
2026-02-23 12:20:48 -05:00
</ div >
< div class = "card" >
2026-02-23 12:49:11 -05:00
< div class = "card-header" >< span class = "card-title" > Risk Scores</ span >< span class = "card-badge" id = "riskBadge" > --</ span ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Country</ th >< th > Score</ th >< th > Level</ th >< th > Events</ th ></ tr ></ thead >< tbody id = "riskBody" ></ tbody ></ table ></ div >
2026-02-23 12:20:48 -05:00
</ div >
</ div >
2026-02-23 12:49:11 -05:00
< div class = "grid grid-3" style = "margin-top:1rem;" >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Armed Conflict (ACLED)</ span >< span class = "card-badge" id = "acledBadge" > --</ span ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Event</ th >< th > Country</ th >< th > Fatalities</ th >< th > Date</ th ></ tr ></ thead >< tbody id = "acledBody" ></ tbody ></ table ></ div >
</ div >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Displacement</ span ></ div >
< div id = "displacementStats" class = "stat-row" ></ div >
< div class = "scroll-box" style = "margin-top:0.75rem;" >< table >< thead >< tr >< th > Country</ th >< th > Refugees</ th >< th > IDPs</ th ></ tr ></ thead >< tbody id = "displacementBody" ></ tbody ></ table ></ div >
</ div >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Signal Convergence</ span ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Region</ th >< th > Score</ th >< th > Signals</ th ></ tr ></ thead >< tbody id = "convergenceBody" ></ tbody ></ table ></ div >
</ div >
</ div >
< div class = "grid grid-3" style = "margin-top:1rem;" >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Natural Events</ span >< span class = "card-badge" id = "naturalBadge" > --</ span ></ div >
< div id = "naturalStats" class = "stat-row" style = "margin-bottom:0.75rem;" ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Mag</ th >< th > Location</ th >< th > Depth</ th >< th > Time</ th ></ tr ></ thead >< tbody id = "quakeBody" ></ tbody ></ table ></ div >
</ div >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Intelligence News</ span >< span class = "card-badge" id = "newsBadge" > --</ span ></ div >
< div id = "trendingKeywords" style = "margin-bottom:0.75rem;" ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Headline</ th >< th > Source</ th ></ tr ></ thead >< tbody id = "newsBody" ></ tbody ></ table ></ div >
</ div >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Prediction Markets</ span >< span class = "card-badge" id = "predBadge" > --</ span ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Market</ th >< th > Prob</ th >< th > Vol</ th ></ tr ></ thead >< tbody id = "predBody" ></ tbody ></ table ></ div >
</ div >
</ div >
</ div >
2026-02-23 12:20:48 -05:00
2026-02-23 12:49:11 -05:00
<!-- ======================= MARKETS TAB ======================= -->
< div class = "section" id = "tab-markets" >
< div class = "grid grid-3" style = "margin-top:1rem;" >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Equity Indices</ span >< span class = "card-badge" id = "equityBadge2" > --</ span ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Symbol</ th >< th > Price</ th >< th > Chg</ th >< th > %</ th ></ tr ></ thead >< tbody id = "equityBody2" ></ tbody ></ table ></ div >
</ div >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Cryptocurrency</ span >< span class = "card-badge" id = "cryptoBadge" > --</ span ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Coin</ th >< th > Price</ th >< th > 24h</ th >< th > MCap</ th ></ tr ></ thead >< tbody id = "cryptoBody" ></ tbody ></ table ></ div >
</ div >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Stablecoin Peg Status</ span ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Coin</ th >< th > Price</ th >< th > Deviation</ th >< th > Status</ th ></ tr ></ thead >< tbody id = "stablecoinBody" ></ tbody ></ table ></ div >
</ div >
</ div >
2026-02-23 12:20:48 -05:00
< div class = "grid grid-3" style = "margin-top:1rem;" >
< div class = "card span-2" >
2026-02-23 12:49:11 -05:00
< div class = "card-header" >< span class = "card-title" > Sector Performance</ span ></ div >
2026-02-23 12:20:48 -05:00
< div class = "heatmap" id = "sectorHeatmap" ></ div >
</ div >
< div class = "card" >
2026-02-23 12:49:11 -05:00
< div class = "card-header" >< span class = "card-title" > Macro Signals</ span >< span class = "card-badge" > Live</ span ></ div >
< div id = "macroContent" class = "stat-row" style = "flex-wrap:wrap;" ></ div >
2026-02-23 12:20:48 -05:00
</ div >
</ div >
2026-02-23 12:49:11 -05:00
< div class = "grid grid-2" style = "margin-top:1rem;" >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Energy Prices</ span ></ div >
< div id = "energyContent" ></ div >
</ div >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > ETF Flows</ span >< span class = "card-badge" id = "etfBadge" > --</ span ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > ETF</ th >< th > Price</ th >< th > Chg%</ th >< th > Volume</ th ></ tr ></ thead >< tbody id = "etfBody" ></ tbody ></ table ></ div >
</ div >
</ div >
</ div >
2026-02-23 12:20:48 -05:00
2026-02-23 12:49:11 -05:00
<!-- ======================= SECURITY TAB ======================= -->
< div class = "section" id = "tab-security" >
2026-02-23 12:20:48 -05:00
< div class = "grid grid-3" style = "margin-top:1rem;" >
< div class = "card" >
2026-02-23 12:49:11 -05:00
< div class = "card-header" >< span class = "card-title" > Cyber Threats</ span >< span class = "card-badge" id = "cyberBadge2" > --</ span ></ div >
< div id = "cyberStats2" class = "stat-row" style = "margin-bottom:0.75rem;" ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Threat</ th >< th > Type</ th >< th > Severity</ th ></ tr ></ thead >< tbody id = "cyberBody2" ></ tbody ></ table ></ div >
2026-02-23 12:20:48 -05:00
</ div >
< div class = "card" >
2026-02-23 12:49:11 -05:00
< div class = "card-header" >< span class = "card-title" > Military Activity</ span >< span class = "card-badge" id = "milBadge" > --</ span ></ div >
2026-02-23 12:20:48 -05:00
< div id = "milStats" class = "stat-row" style = "margin-bottom:0.75rem;" ></ div >
2026-02-23 12:49:11 -05:00
< div class = "scroll-box" >< table >< thead >< tr >< th > Callsign</ th >< th > Type</ th >< th > Alt</ th >< th > Origin</ th ></ tr ></ thead >< tbody id = "milBody" ></ tbody ></ table ></ div >
2026-02-23 12:20:48 -05:00
</ div >
< div class = "card" >
2026-02-23 12:49:11 -05:00
< div class = "card-header" >< span class = "card-title" > Infrastructure</ span ></ div >
2026-02-23 12:20:48 -05:00
< div id = "infraStats" class = "stat-row" style = "margin-bottom:0.75rem;" ></ div >
< div class = "scroll-box" id = "infraContent" ></ div >
</ div >
</ div >
< div class = "grid grid-3" style = "margin-top:1rem;" >
< div class = "card" >
2026-02-23 12:49:11 -05:00
< div class = "card-header" >< span class = "card-title" > Airport Delays</ span >< span class = "card-badge" id = "aviationBadge" > --</ span ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Airport</ th >< th > Reason</ th >< th > Delay</ th ></ tr ></ thead >< tbody id = "aviationBody" ></ tbody ></ table ></ div >
2026-02-23 12:20:48 -05:00
</ div >
< div class = "card" >
2026-02-23 12:49:11 -05:00
< div class = "card-header" >< span class = "card-title" > Nav Warnings</ span >< span class = "card-badge" id = "navBadge" > --</ span ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Area</ th >< th > Warning</ th ></ tr ></ thead >< tbody id = "navBody" ></ tbody ></ table ></ div >
2026-02-23 12:20:48 -05:00
</ div >
< div class = "card" >
2026-02-23 12:49:11 -05:00
< div class = "card-header" >< span class = "card-title" > Risk Scores</ span >< span class = "card-badge" id = "riskBadge2" > --</ span ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Country</ th >< th > Score</ th >< th > Level</ th >< th > Events</ th ></ tr ></ thead >< tbody id = "riskBody2" ></ tbody ></ table ></ div >
2026-02-23 12:20:48 -05:00
</ div >
</ div >
2026-02-23 12:49:11 -05:00
</ div >
2026-02-23 12:20:48 -05:00
2026-02-23 12:49:11 -05:00
<!-- ======================= CONFLICT & HUMANITARIAN TAB ======================= -->
< div class = "section" id = "tab-conflict" >
< div class = "grid grid-2" style = "margin-top:1rem;" >
2026-02-23 12:20:48 -05:00
< div class = "card" >
2026-02-23 12:49:11 -05:00
< div class = "card-header" >< span class = "card-title" > Armed Conflict - ACLED</ span >< span class = "card-badge" id = "acledBadge2" > --</ span ></ div >
< div class = "scroll-box" style = "max-height:350px;" >< table >< thead >< tr >< th > Type</ th >< th > Country</ th >< th > Location</ th >< th > Fatalities</ th >< th > Date</ th ></ tr ></ thead >< tbody id = "acledBody2" ></ tbody ></ table ></ div >
2026-02-23 12:20:48 -05:00
</ div >
< div class = "card" >
2026-02-23 12:49:11 -05:00
< div class = "card-header" >< span class = "card-title" > Armed Conflict - UCDP</ span >< span class = "card-badge" id = "ucdpBadge" > --</ span ></ div >
< div class = "scroll-box" style = "max-height:350px;" >< table >< thead >< tr >< th > Conflict</ th >< th > Type</ th >< th > Fatalities</ th >< th > Location</ th ></ tr ></ thead >< tbody id = "ucdpBody" ></ tbody ></ table ></ div >
</ div >
</ div >
< div class = "grid grid-3" style = "margin-top:1rem;" >
< div class = "card span-2" >
< div class = "card-header" >< span class = "card-title" > Global Displacement (UNHCR)</ span ></ div >
< div id = "displacementStats2" class = "stat-row" style = "margin-bottom:0.75rem;" ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Country of Origin</ th >< th > Refugees</ th >< th > Asylum Seekers</ th >< th > IDPs</ th >< th > Stateless</ th ></ tr ></ thead >< tbody id = "displacementBody2" ></ tbody ></ table ></ div >
</ div >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Signal Convergence</ span ></ div >
< div class = "scroll-box" >< table >< thead >< tr >< th > Region</ th >< th > Score</ th >< th > Quakes</ th ></ tr ></ thead >< tbody id = "convergenceBody2" ></ tbody ></ table ></ div >
</ div >
</ div >
</ div >
<!-- ======================= NATURAL & CLIMATE TAB ======================= -->
< div class = "section" id = "tab-natural" >
< div class = "grid grid-3" style = "margin-top:1rem;" >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Earthquakes</ span >< span class = "card-badge" id = "quakeBadge2" > --</ span ></ div >
< div class = "scroll-box" style = "max-height:350px;" >< table >< thead >< tr >< th > Mag</ th >< th > Location</ th >< th > Depth</ th >< th > Time</ th ></ tr ></ thead >< tbody id = "quakeBody2" ></ tbody ></ table ></ div >
</ div >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Wildfires</ span >< span class = "card-badge" id = "fireBadge" > --</ span ></ div >
< div id = "fireContent" ></ div >
</ div >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Climate Anomalies</ span ></ div >
2026-02-23 12:20:48 -05:00
< div class = "scroll-box" id = "climateContent" ></ div >
</ div >
2026-02-23 12:49:11 -05:00
</ div >
</ div >
<!-- ======================= GEOSPATIAL MAP TAB ======================= -->
< div class = "section" id = "tab-geomap" >
< div style = "margin-top:1rem;" >
2026-02-23 12:20:48 -05:00
< div class = "card" >
< div class = "card-header" >
2026-02-23 12:49:11 -05:00
< span class = "card-title" > Geospatial Intelligence</ span >
< span >
< label style = "font-size:0.75rem;margin-right:8px;" >< input type = "checkbox" id = "mapQuakes" checked > Quakes</ label >
< label style = "font-size:0.75rem;margin-right:8px;" >< input type = "checkbox" id = "mapFlights" checked > Military</ label >
< label style = "font-size:0.75rem;margin-right:8px;" >< input type = "checkbox" id = "mapConflict" checked > Conflict</ label >
< label style = "font-size:0.75rem;" >< input type = "checkbox" id = "mapConvergence" checked > Convergence</ label >
</ span >
2026-02-23 12:20:48 -05:00
</ div >
2026-02-23 12:49:11 -05:00
< div id = "geoMap" ></ div >
< div class = "map-legend" >
< div >< span class = "legend-dot" style = "background:#ef4444;" ></ span > Earthquakes</ div >
< div >< span class = "legend-dot" style = "background:#60a5fa;" ></ span > Military Flights</ div >
< div >< span class = "legend-dot" style = "background:#f59e0b;" ></ span > Conflict Events</ div >
< div >< span class = "legend-dot" style = "background:#a78bfa;" ></ span > Signal Convergence</ div >
2026-02-23 12:20:48 -05:00
</ div >
</ div >
</ div >
2026-02-23 12:49:11 -05:00
</ div >
<!-- ======================= TREND CHARTS TAB ======================= -->
< div class = "section" id = "tab-charts" >
< div class = "grid grid-2" style = "margin-top:1rem;" >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Earthquake Activity (Recent Updates)</ span ></ div >
< div class = "chart-container" >< canvas id = "chartQuakes" ></ canvas ></ div >
</ div >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Cyber Threat Volume</ span ></ div >
< div class = "chart-container" >< canvas id = "chartCyber" ></ canvas ></ div >
</ div >
</ div >
< div class = "grid grid-2" style = "margin-top:1rem;" >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Military Aircraft Tracked</ span ></ div >
< div class = "chart-container" >< canvas id = "chartMilitary" ></ canvas ></ div >
</ div >
< div class = "card" >
< div class = "card-header" >< span class = "card-title" > Conflict Events (ACLED)</ span ></ div >
< div class = "chart-container" >< canvas id = "chartConflict" ></ canvas ></ div >
</ div >
</ div >
</ div >
2026-02-23 12:20:48 -05:00
</ div >
< script >
// ========================================================================
2026-02-23 12:49:11 -05:00
// Phoenix Intelligence Dashboard v2 - Full-spectrum live intelligence
// NOTE: All innerHTML assignments use DOMPurify.sanitize() for XSS safety.
2026-02-23 12:20:48 -05:00
// ========================================================================
2026-02-23 12:49:11 -05:00
var $ = function ( sel ) { return document . querySelector ( sel ); };
var $$ = function ( sel ) { return document . querySelectorAll ( sel ); };
var safe = function ( html ) { return DOMPurify . sanitize ( html , { ALLOWED_TAGS : [ 'tr' , 'td' , 'th' , 'table' , 'thead' , 'tbody' , 'a' , 'span' , 'div' , 'br' , 'canvas' ], ALLOWED_ATTR : [ 'class' , 'style' , 'href' , 'target' , 'id' ]}); };
2026-02-23 12:20:48 -05:00
function fmtNum ( n , dec ) { dec = dec != null ? dec : 2 ; return n == null ? '\u2014' : Number ( n ). toLocaleString ( undefined , { minimumFractionDigits : dec , maximumFractionDigits : dec }); }
function fmtPct ( n ) { return n == null ? '\u2014' : ( n >= 0 ? '+' : '' ) + fmtNum ( n ); }
function fmtBig ( n ) { if ( n == null ) return '\u2014' ; if ( Math . abs ( n ) >= 1e12 ) return '$' + ( n / 1e12 ). toFixed ( 1 ) + 'T' ; if ( Math . abs ( n ) >= 1e9 ) return '$' + ( n / 1e9 ). toFixed ( 1 ) + 'B' ; if ( Math . abs ( n ) >= 1e6 ) return '$' + ( n / 1e6 ). toFixed ( 1 ) + 'M' ; return '$' + fmtNum ( n , 0 ); }
2026-02-23 12:49:11 -05:00
function fmtBigPlain ( n ) { if ( n == null ) return '\u2014' ; if ( Math . abs ( n ) >= 1e9 ) return ( n / 1e9 ). toFixed ( 1 ) + 'B' ; if ( Math . abs ( n ) >= 1e6 ) return ( n / 1e6 ). toFixed ( 1 ) + 'M' ; if ( Math . abs ( n ) >= 1e3 ) return ( n / 1e3 ). toFixed ( 1 ) + 'K' ; return String ( n ); }
2026-02-23 12:20:48 -05:00
function cls ( n ) { return n == null ? '' : n >= 0 ? 'up' : 'down' ; }
function sevCls ( s ) { s = ( s || '' ). toLowerCase (); return s === 'critical' ? 'sev-critical' : s === 'high' ? 'sev-high' : 'sev-medium' ; }
2026-02-23 12:49:11 -05:00
function riskCls ( s ) { s = ( s || '' ). toLowerCase (); if ( s === 'extreme' || s === 'critical' ) return 'risk-extreme' ; if ( s === 'high' ) return 'risk-high' ; if ( s === 'elevated' ) return 'risk-elevated' ; if ( s === 'moderate' ) return 'risk-moderate' ; return 'risk-low' ; }
function ago ( ts ) { if ( ! ts ) return '\u2014' ; var d = new Date ( ts ), s = Math . floor (( Date . now () - d . getTime ()) / 1000 ); if ( s < 60 ) return s + 's ago' ; 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' ; }
2026-02-23 12:20:48 -05:00
function esc ( s ) { var d = document . createElement ( 'div' ); d . textContent = s || '' ; return d . innerHTML ; }
function trunc ( s , n ) { n = n || 50 ; s = s || '' ; return s . length > n ? s . slice ( 0 , n ) + '...' : s ; }
2026-02-23 12:49:11 -05:00
// Tab navigation
$$ ( '.nav-tab' ). forEach ( function ( tab ) {
tab . addEventListener ( 'click' , function () {
$$ ( '.nav-tab' ). forEach ( function ( t ) { t . classList . remove ( 'active' ); });
$$ ( '.section' ). forEach ( function ( s ) { s . classList . remove ( 'active' ); });
tab . classList . add ( 'active' );
var target = document . getElementById ( 'tab-' + tab . dataset . tab );
if ( target ) target . classList . add ( 'active' );
if ( tab . dataset . tab === 'geomap' && ! mapInitialized ) initMap ();
});
});
2026-02-23 12:20:48 -05:00
// ========================================================================
2026-02-23 12:49:11 -05:00
// Update functions - all use safe() for innerHTML (DOMPurify XSS protection)
2026-02-23 12:20:48 -05:00
// ========================================================================
function updateEquity ( data ) {
if ( ! data || data . error ) { $ ( '#equityBody' ). innerHTML = safe ( '<tr><td colspan="4" class="card-error">Unavailable</td></tr>' ); return ; }
var quotes = data . quotes || data . data || ( Array . isArray ( data ) ? data : []);
$ ( '#equityBadge' ). textContent = quotes . length + ' symbols' ;
var rows = quotes . map ( function ( q ) {
var price = q . price || q . regularMarketPrice || 0 ;
var chg = q . change || q . regularMarketChange || 0 ;
var pct = q . change_pct || q . regularMarketChangePercent || 0 ;
return '<tr><td class="bright">' + esc ( q . symbol || '?' ) + '</td><td>' + fmtNum ( price ) + '</td><td class="' + cls ( chg ) + '">' + fmtPct ( chg ) + '</td><td class="' + cls ( pct ) + '">' + fmtPct ( pct ) + '%</td></tr>' ;
}). join ( '' );
$ ( '#equityBody' ). innerHTML = safe ( rows );
2026-02-23 12:49:11 -05:00
if ( $ ( '#equityBody2' )) { $ ( '#equityBadge2' ). textContent = quotes . length + ' symbols' ; $ ( '#equityBody2' ). innerHTML = safe ( rows ); }
2026-02-23 12:20:48 -05:00
}
function updateCrypto ( data ) {
if ( ! data || data . error ) { $ ( '#cryptoBody' ). innerHTML = safe ( '<tr><td colspan="4" class="card-error">Unavailable</td></tr>' ); return ; }
var coins = data . coins || data . data || ( Array . isArray ( data ) ? data : []);
$ ( '#cryptoBadge' ). textContent = coins . length + ' coins' ;
var rows = coins . slice ( 0 , 15 ). map ( function ( c ) {
var price = c . current_price || c . price || 0 ;
var chg = c . price_change_percentage_24h || c . change_24h || 0 ;
var mcap = c . market_cap || 0 ;
return '<tr><td class="bright">' + esc (( c . symbol || c . id || '?' ). toUpperCase ()) + '</td><td>$' + fmtNum ( price ) + '</td><td class="' + cls ( chg ) + '">' + fmtPct ( chg ) + '%</td><td class="muted">' + fmtBig ( mcap ) + '</td></tr>' ;
}). join ( '' );
$ ( '#cryptoBody' ). innerHTML = safe ( rows );
}
2026-02-23 12:49:11 -05:00
function updateStablecoins ( data ) {
if ( ! data || data . error ) { $ ( '#stablecoinBody' ). innerHTML = safe ( '<tr><td colspan="4" class="card-error">Unavailable</td></tr>' ); return ; }
var coins = data . stablecoins || data . data || ( Array . isArray ( data ) ? data : []);
var rows = coins . map ( function ( c ) {
var depegged = c . is_depegged ;
var devPct = c . peg_deviation_pct || 0 ;
return '<tr><td class="bright">' + esc (( c . id || '?' ). toUpperCase ()) + '</td><td>$' + fmtNum ( c . price , 4 ) + '</td><td class="' + ( devPct > 0.5 ? 'warn' : 'muted' ) + '">' + fmtNum ( devPct , 3 ) + '%</td><td class="' + ( depegged ? 'sev-critical' : 'up' ) + '">' + ( depegged ? 'DEPEGGED' : 'Stable' ) + '</td></tr>' ;
}). join ( '' );
$ ( '#stablecoinBody' ). innerHTML = safe ( rows );
}
2026-02-23 12:20:48 -05:00
function updateMacro ( data ) {
if ( ! data || data . error ) { $ ( '#macroContent' ). innerHTML = safe ( '<div class="card-error">Unavailable</div>' ); return ; }
var signals = data . signals || data ;
if ( typeof signals !== 'object' ) { $ ( '#macroContent' ). innerHTML = safe ( '<div class="card-error">No data</div>' ); return ; }
var html = '' ;
for ( var name in signals ) {
if ( ! signals . hasOwnProperty ( name )) continue ;
var info = signals [ name ];
if ( info && typeof info === 'object' && ! Array . isArray ( info )) {
var entries = Object . entries ( info );
var mainVal = entries [ 0 ] ? entries [ 0 ][ 1 ] : '\u2014' ;
html += '<div class="stat-box"><div class="stat-value" style="font-size:1.1rem;">' + ( typeof mainVal === 'number' ? fmtNum ( mainVal , mainVal < 1 ? 4 : 2 ) : esc ( String ( mainVal ))) + '</div><div class="stat-label">' + esc ( name . replace ( /_/g , ' ' )) + '</div></div>' ;
} else {
html += '<div class="stat-box"><div class="stat-value" style="font-size:1.1rem;">' + ( info != null ? esc ( String ( info )) : '\u2014' ) + '</div><div class="stat-label">' + esc ( name . replace ( /_/g , ' ' )) + '</div></div>' ;
}
}
$ ( '#macroContent' ). innerHTML = safe ( html );
}
function updateSectorHeatmap ( data ) {
if ( ! data || data . error ) { $ ( '#sectorHeatmap' ). innerHTML = safe ( '<div class="card-error">Unavailable</div>' ); return ; }
var sectors = data . sectors || data . data || ( Array . isArray ( data ) ? data : []);
var html = sectors . map ( function ( s ) {
2026-02-23 12:49:11 -05:00
var pct = s . change_pct || 0 ; var bg , fg ;
if ( pct >= 2 ) { bg = '#166534' ; fg = '#86efac' ; } else if ( pct >= 0.5 ) { bg = '#14532d' ; fg = '#86efac' ; }
else if ( pct >= 0 ) { bg = '#1a2e1a' ; fg = '#86efac' ; } else if ( pct >= - 0.5 ) { bg = '#2e1a1a' ; fg = '#fca5a5' ; }
else if ( pct >= - 2 ) { bg = '#7f1d1d' ; fg = '#fca5a5' ; } else { bg = '#991b1b' ; fg = '#fca5a5' ; }
2026-02-23 12:20:48 -05:00
return '<div class="hm-cell" style="background:' + bg + ';color:' + fg + ';">' + esc ( s . symbol || s . name || '?' ) + '<br>' + ( pct >= 0 ? '+' : '' ) + pct . toFixed ( 1 ) + '%</div>' ;
}). join ( '' );
$ ( '#sectorHeatmap' ). innerHTML = safe ( html );
}
function updateEnergy ( data ) {
if ( ! data || data . error ) { $ ( '#energyContent' ). innerHTML = safe ( '<div class="card-error">Unavailable</div>' ); return ; }
var prices = data . prices || data . data || data ;
if ( typeof prices !== 'object' ) { $ ( '#energyContent' ). innerHTML = safe ( '<div class="card-error">No data</div>' ); return ; }
var html = '<table><thead><tr><th>Commodity</th><th>Price</th></tr></thead><tbody>' ;
var entries = Array . isArray ( prices ) ? prices : Object . entries ( prices ). map ( function ( e ) { return Object . assign ({ name : e [ 0 ]}, typeof e [ 1 ] === 'object' ? e [ 1 ] : { value : e [ 1 ]}); });
entries . forEach ( function ( item ) {
var name = item . name || item . series_id || '?' ;
var val = item . value || item . price || item . last_value || '\u2014' ;
html += '<tr><td>' + esc ( name . replace ( /_/g , ' ' )) + '</td><td class="bright">' + ( typeof val === 'number' ? fmtNum ( val ) : esc ( String ( val ))) + '</td></tr>' ;
});
html += '</tbody></table>' ;
$ ( '#energyContent' ). innerHTML = safe ( html );
}
2026-02-23 12:49:11 -05:00
function updateETF ( data ) {
if ( ! data || data . error ) { $ ( '#etfBody' ). innerHTML = safe ( '<tr><td colspan="4" class="card-error">Unavailable</td></tr>' ); return ; }
var etfs = data . etfs || data . data || ( Array . isArray ( data ) ? data : []);
$ ( '#etfBadge' ). textContent = etfs . length + ' ETFs' ;
var rows = etfs . map ( function ( e ) {
return '<tr><td class="bright">' + esc ( e . symbol || '?' ) + '</td><td>' + fmtNum ( e . price ) + '</td><td class="' + cls ( e . change_pct ) + '">' + fmtPct ( e . change_pct ) + '%</td><td class="muted">' + fmtBigPlain ( e . volume ) + '</td></tr>' ;
}). join ( '' );
$ ( '#etfBody' ). innerHTML = safe ( rows );
}
2026-02-23 12:20:48 -05:00
function updateCyber ( data ) {
2026-02-23 12:49:11 -05:00
if ( ! data || data . error ) { return ; }
2026-02-23 12:20:48 -05:00
var threats = data . threats || data . data || ( Array . isArray ( data ) ? data : []);
var bySev = data . by_severity || {};
2026-02-23 12:49:11 -05:00
var badge = threats . length + ' IOCs' ;
var stats = safe ( '<div class="stat-box"><div class="stat-value sev-critical">' + ( bySev . critical || 0 ) + '</div><div class="stat-label">Critical</div></div><div class="stat-box"><div class="stat-value sev-high">' + ( bySev . high || 0 ) + '</div><div class="stat-label">High</div></div><div class="stat-box"><div class="stat-value">' + ( bySev . medium || 0 ) + '</div><div class="stat-label">Medium</div></div>' );
2026-02-23 12:20:48 -05:00
var rows = threats . slice ( 0 , 20 ). map ( function ( t ) {
return '<tr><td>' + esc ( trunc ( t . indicator || t . url || t . ioc || '?' , 40 )) + '</td><td class="muted">' + esc ( t . threat_type || t . type || t . source || '\u2014' ) + '</td><td class="' + sevCls ( t . severity ) + '">' + esc ( t . severity || '\u2014' ) + '</td></tr>' ;
}). join ( '' );
2026-02-23 12:49:11 -05:00
[ '' , '2' ]. forEach ( function ( sfx ) {
var b = $ ( '#cyberBadge' + sfx ); if ( b ) b . textContent = badge ;
var s = $ ( '#cyberStats' + sfx ); if ( s ) s . innerHTML = stats ;
var bd = $ ( '#cyberBody' + sfx ); if ( bd ) bd . innerHTML = safe ( rows );
});
2026-02-23 12:20:48 -05:00
}
function updateMilitary ( data ) {
if ( ! data || data . error ) { $ ( '#milBody' ). innerHTML = safe ( '<tr><td colspan="4" class="card-error">Unavailable</td></tr>' ); return ; }
var flights = data . flights || data . aircraft || data . data || ( Array . isArray ( data ) ? data : []);
var total = data . total_military_aircraft || flights . length ;
$ ( '#milBadge' ). textContent = total + ' aircraft' ;
$ ( '#milStats' ). innerHTML = safe ( '<div class="stat-box"><div class="stat-value">' + total + '</div><div class="stat-label">Military Aircraft</div></div>' );
var rows = flights . slice ( 0 , 15 ). map ( function ( f ) {
var alt = f . altitude ? Math . round ( f . altitude ) + 'ft' : f . baro_altitude ? Math . round ( f . baro_altitude ) + 'ft' : '\u2014' ;
return '<tr><td class="bright">' + esc ( f . callsign || f . icao24 || '?' ) + '</td><td class="muted">' + esc ( f . type || f . category || '\u2014' ) + '</td><td>' + alt + '</td><td class="muted">' + esc ( f . origin_country || '\u2014' ) + '</td></tr>' ;
}). join ( '' );
$ ( '#milBody' ). innerHTML = safe ( rows );
}
function updateInfra ( data_outages , data_cables ) {
2026-02-23 12:49:11 -05:00
var statsHtml = '' , contentHtml = '' ;
2026-02-23 12:20:48 -05:00
if ( data_outages && ! data_outages . error ) {
var outages = data_outages . outages || data_outages . data || ( Array . isArray ( data_outages ) ? data_outages : []);
var count = data_outages . ongoing_count || outages . length ;
statsHtml += '<div class="stat-box"><div class="stat-value ' + ( count > 0 ? 'warn' : '' ) + '">' + count + '</div><div class="stat-label">Outages</div></div>' ;
if ( outages . length > 0 ) {
contentHtml += '<table><thead><tr><th>Location</th><th>Status</th></tr></thead><tbody>' ;
2026-02-23 12:49:11 -05:00
outages . slice ( 0 , 8 ). forEach ( function ( o ) { contentHtml += '<tr><td>' + esc ( o . location || o . asName || '?' ) + '</td><td class="warn">' + esc ( o . status || 'ongoing' ) + '</td></tr>' ; });
2026-02-23 12:20:48 -05:00
contentHtml += '</tbody></table>' ;
}
}
if ( data_cables && ! data_cables . error ) {
var score = data_cables . health_score || data_cables . overall_score ;
var warnings = data_cables . warnings || data_cables . data || [];
var warnCount = Array . isArray ( warnings ) ? warnings . length : 0 ;
statsHtml += '<div class="stat-box"><div class="stat-value">' + ( score != null ? score + '%' : warnCount ) + '</div><div class="stat-label">' + ( score != null ? 'Cable Health' : 'Cable Warnings' ) + '</div></div>' ;
}
$ ( '#infraStats' ). innerHTML = safe ( statsHtml || '<div class="stat-box"><div class="stat-value muted">\u2014</div><div class="stat-label">No data</div></div>' );
$ ( '#infraContent' ). innerHTML = safe ( contentHtml || '<div class="card-error">No active outages</div>' );
}
function updateNatural ( data_quakes , data_fires ) {
var statsHtml = '' ;
if ( data_quakes && ! data_quakes . error ) {
var quakes = data_quakes . earthquakes || data_quakes . features || data_quakes . data || ( Array . isArray ( data_quakes ) ? data_quakes : []);
statsHtml += '<div class="stat-box"><div class="stat-value">' + quakes . length + '</div><div class="stat-label">Quakes</div></div>' ;
$ ( '#naturalBadge' ). textContent = quakes . length + ' events' ;
var rows = quakes . slice ( 0 , 15 ). map ( function ( q ) {
var mag = q . magnitude || ( q . properties && q . properties . mag ) || '?' ;
var place = q . place || ( q . properties && q . properties . place ) || '?' ;
var depth = q . depth || ( q . geometry && q . geometry . coordinates && q . geometry . coordinates [ 2 ]) || '?' ;
var time = q . time || ( q . properties && q . properties . time ) || '' ;
var magClass = mag >= 6 ? 'sev-critical' : mag >= 5 ? 'sev-high' : '' ;
return '<tr><td class="' + magClass + ' bright">' + ( typeof mag === 'number' ? mag . toFixed ( 1 ) : mag ) + '</td><td>' + esc ( trunc ( place , 35 )) + '</td><td class="muted">' + ( typeof depth === 'number' ? depth . toFixed ( 0 ) + 'km' : depth ) + '</td><td class="muted">' + ago ( time ) + '</td></tr>' ;
}). join ( '' );
$ ( '#quakeBody' ). innerHTML = safe ( rows );
2026-02-23 12:49:11 -05:00
if ( $ ( '#quakeBody2' )) { $ ( '#quakeBadge2' ). textContent = quakes . length + ' quakes' ; $ ( '#quakeBody2' ). innerHTML = safe ( rows ); }
2026-02-23 12:20:48 -05:00
}
if ( data_fires && ! data_fires . error ) {
2026-02-23 12:49:11 -05:00
var fires = data_fires . fires || data_fires . hotspots || data_fires . data || [];
var count = data_fires . fire_count || data_fires . count || ( Array . isArray ( fires ) ? fires . length : 0 );
2026-02-23 12:20:48 -05:00
statsHtml += '<div class="stat-box"><div class="stat-value ' + ( count > 100 ? 'warn' : '' ) + '">' + count + '</div><div class="stat-label">Wildfires</div></div>' ;
2026-02-23 12:49:11 -05:00
if ( $ ( '#fireBadge' )) $ ( '#fireBadge' ). textContent = count + ' fires' ;
if ( $ ( '#fireContent' )) {
if ( Array . isArray ( fires ) && fires . length > 0 ) {
var fhtml = '<table><thead><tr><th>Location</th><th>Brightness</th><th>Confidence</th></tr></thead><tbody>' ;
fires . slice ( 0 , 15 ). forEach ( function ( f ) { fhtml += '<tr><td>' + esc ( f . location || ( f . latitude ? f . latitude . toFixed ( 2 ) + ', ' + f . longitude . toFixed ( 2 ) : '?' )) + '</td><td class="warn">' + ( f . brightness || f . bright_ti4 || '\u2014' ) + '</td><td class="muted">' + esc ( f . confidence || '\u2014' ) + '</td></tr>' ; });
fhtml += '</tbody></table>' ;
$ ( '#fireContent' ). innerHTML = safe ( fhtml );
} else {
$ ( '#fireContent' ). innerHTML = safe ( '<div class="stat-row"><div class="stat-box"><div class="stat-value ' + ( count > 100 ? 'warn' : '' ) + '">' + count + '</div><div class="stat-label">Active Fires</div></div></div>' );
}
}
2026-02-23 12:20:48 -05:00
}
$ ( '#naturalStats' ). innerHTML = safe ( statsHtml || '<div class="card-error">Unavailable</div>' );
}
function updateNews ( data_news , data_trending ) {
if ( data_trending && ! data_trending . error ) {
var keywords = data_trending . keywords || data_trending . data || ( Array . isArray ( data_trending ) ? data_trending : []);
var top = keywords . slice ( 0 , 20 );
if ( top . length > 0 ) {
var khtml = top . map ( function ( k ) {
var word = typeof k === 'string' ? k : ( k . keyword || k . word || k . term || '?' );
var count = typeof k === 'object' ? ( k . count || k . frequency || '' ) : '' ;
var hot = count && count > 5 ;
return '<span class="tag ' + ( hot ? 'tag-hot' : '' ) + '">' + esc ( word ) + ( count ? ' (' + count + ')' : '' ) + '</span>' ;
}). join ( '' );
$ ( '#trendingKeywords' ). innerHTML = safe ( khtml );
}
}
if ( data_news && ! data_news . error ) {
var articles = data_news . articles || data_news . items || data_news . data || ( Array . isArray ( data_news ) ? data_news : []);
$ ( '#newsBadge' ). textContent = articles . length + ' items' ;
var rows = articles . slice ( 0 , 20 ). map ( function ( a ) {
var title = esc ( trunc ( a . title || '?' , 55 ));
var link = a . link ? '<a href="' + esc ( a . link ) + '" target="_blank">' + title + '</a>' : title ;
return '<tr><td>' + link + '</td><td class="muted">' + esc ( trunc ( a . source || a . feed || '' , 15 )) + '</td></tr>' ;
}). join ( '' );
$ ( '#newsBody' ). innerHTML = safe ( rows );
}
}
function updatePredictions ( data ) {
if ( ! data || data . error ) { $ ( '#predBody' ). innerHTML = safe ( '<tr><td colspan="3" class="card-error">Unavailable</td></tr>' ); return ; }
var mkts = data . markets || data . data || ( Array . isArray ( data ) ? data : []);
$ ( '#predBadge' ). textContent = mkts . length + ' markets' ;
var rows = mkts . slice ( 0 , 15 ). map ( function ( m ) {
var prob = m . probability || m . outcomePrices || m . yes_price ;
var vol = m . volume || m . liquidityCloses || 0 ;
return '<tr><td>' + esc ( trunc ( m . question || m . title || m . groupItemTitle || '?' , 45 )) + '</td><td class="bright">' + ( prob != null ? ( typeof prob === 'number' ? ( prob * 100 ). toFixed ( 0 ) + '%' : esc ( String ( prob ))) : '\u2014' ) + '</td><td class="muted">' + fmtBig ( vol ) + '</td></tr>' ;
}). join ( '' );
$ ( '#predBody' ). innerHTML = safe ( rows );
}
function updateAviation ( data ) {
if ( ! data || data . error ) { $ ( '#aviationBody' ). innerHTML = safe ( '<tr><td colspan="3" class="card-error">Unavailable</td></tr>' ); return ; }
var delays = data . delays || data . data || ( Array . isArray ( data ) ? data : []);
$ ( '#aviationBadge' ). textContent = delays . length + ' delays' ;
if ( delays . length === 0 ) { $ ( '#aviationBody' ). innerHTML = safe ( '<tr><td colspan="3" class="card-error">No active delays</td></tr>' ); return ; }
var rows = delays . slice ( 0 , 15 ). map ( function ( d ) {
return '<tr><td class="bright">' + esc ( d . airport || d . iata || d . name || '?' ) + '</td><td class="muted">' + esc ( trunc ( d . reason || d . type || '\u2014' , 30 )) + '</td><td class="warn">' + esc ( d . delay || d . average || d . min || '\u2014' ) + '</td></tr>' ;
}). join ( '' );
$ ( '#aviationBody' ). innerHTML = safe ( rows );
}
function updateClimate ( data ) {
if ( ! data || data . error ) { $ ( '#climateContent' ). innerHTML = safe ( '<div class="card-error">Unavailable</div>' ); return ; }
2026-02-23 12:25:06 -05:00
var src = data . anomalies || data . zones || data . data || ( Array . isArray ( data ) ? data : []);
var anomalies = Array . isArray ( src ) ? src : ( typeof src === 'object' ? Object . values ( src ) : []);
2026-02-23 12:20:48 -05:00
if ( anomalies . length === 0 ) { $ ( '#climateContent' ). innerHTML = safe ( '<div class="card-error">No anomalies detected</div>' ); return ; }
var html = '<table><thead><tr><th>Zone</th><th>Temp</th><th>Precip</th></tr></thead><tbody>' ;
anomalies . slice ( 0 , 15 ). forEach ( function ( a ) {
var temp = a . temperature_anomaly || a . temp_anomaly || a . temp_deviation ;
var precip = a . precipitation_anomaly || a . precip_anomaly || a . precip_deviation ;
html += '<tr><td>' + esc ( a . zone || a . name || a . region || '?' ) + '</td><td class="' + ( temp > 0 ? 'warn' : 'up' ) + '">' + ( temp != null ? ( temp > 0 ? '+' : '' ) + fmtNum ( temp , 1 ) + '\u00B0C' : '\u2014' ) + '</td><td class="muted">' + ( precip != null ? fmtNum ( precip , 1 ) + 'mm' : '\u2014' ) + '</td></tr>' ;
});
html += '</tbody></table>' ;
$ ( '#climateContent' ). innerHTML = safe ( html );
}
function updateNavWarnings ( data ) {
if ( ! data || data . error ) { $ ( '#navBody' ). innerHTML = safe ( '<tr><td colspan="2" class="card-error">Unavailable</td></tr>' ); return ; }
var warnings = data . warnings || data . data || ( Array . isArray ( data ) ? data : []);
$ ( '#navBadge' ). textContent = warnings . length + ' active' ;
if ( warnings . length === 0 ) { $ ( '#navBody' ). innerHTML = safe ( '<tr><td colspan="2" class="card-error">No active warnings</td></tr>' ); return ; }
var rows = warnings . slice ( 0 , 12 ). map ( function ( w ) {
return '<tr><td class="bright">' + esc ( w . navArea || w . area || w . subregion || '?' ) + '</td><td class="muted">' + esc ( trunc ( w . text || w . description || w . subject || '\u2014' , 50 )) + '</td></tr>' ;
}). join ( '' );
$ ( '#navBody' ). innerHTML = safe ( rows );
}
2026-02-23 12:49:11 -05:00
// New: Conflict, Displacement, Risk, Signal Convergence
function updateACLED ( data ) {
if ( ! data || data . error ) { return ; }
var events = data . events || data . data || ( Array . isArray ( data ) ? data : []);
var badge = ( data . count || events . length ) + ' events' ;
var rows1 = events . slice ( 0 , 10 ). map ( function ( e ) {
var fat = e . fatalities || 0 ;
return '<tr><td>' + esc ( trunc ( e . event_type || e . type || '?' , 20 )) + '</td><td class="muted">' + esc ( e . country || '?' ) + '</td><td class="' + ( fat > 10 ? 'sev-critical' : fat > 0 ? 'warn' : 'muted' ) + '">' + fat + '</td><td class="muted">' + esc ( e . event_date || '\u2014' ) + '</td></tr>' ;
}). join ( '' );
if ( $ ( '#acledBadge' )) $ ( '#acledBadge' ). textContent = badge ;
if ( $ ( '#acledBody' )) $ ( '#acledBody' ). innerHTML = safe ( rows1 );
var rows2 = events . slice ( 0 , 25 ). map ( function ( e ) {
var fat = e . fatalities || 0 ;
return '<tr><td>' + esc ( trunc ( e . event_type || e . type || '?' , 25 )) + '</td><td class="muted">' + esc ( e . country || '?' ) + '</td><td>' + esc ( trunc ( e . location || e . admin1 || '?' , 25 )) + '</td><td class="' + ( fat > 10 ? 'sev-critical' : fat > 0 ? 'warn' : 'muted' ) + '">' + fat + '</td><td class="muted">' + esc ( e . event_date || '\u2014' ) + '</td></tr>' ;
}). join ( '' );
if ( $ ( '#acledBadge2' )) $ ( '#acledBadge2' ). textContent = badge ;
if ( $ ( '#acledBody2' )) $ ( '#acledBody2' ). innerHTML = safe ( rows2 );
}
function updateUCDP ( data ) {
if ( ! data || data . error ) { $ ( '#ucdpBody' ). innerHTML = safe ( '<tr><td colspan="4" class="card-error">Unavailable</td></tr>' ); return ; }
var events = data . events || data . data || ( Array . isArray ( data ) ? data : []);
$ ( '#ucdpBadge' ). textContent = ( data . count || events . length ) + ' events' ;
var rows = events . slice ( 0 , 25 ). map ( function ( e ) {
var best = e . best || e . best_fatality_estimate || 0 ;
return '<tr><td>' + esc ( trunc ( e . dyad_name || e . side_a || '?' , 30 )) + '</td><td class="muted">' + esc ( e . type_of_violence || '\u2014' ) + '</td><td class="' + ( best > 10 ? 'sev-critical' : best > 0 ? 'warn' : 'muted' ) + '">' + best + '</td><td class="muted">' + esc ( trunc ( e . where_description || e . country || '?' , 25 )) + '</td></tr>' ;
}). join ( '' );
$ ( '#ucdpBody' ). innerHTML = safe ( rows );
}
function updateDisplacement ( data ) {
if ( ! data || data . error ) { return ; }
var totals = data . global_totals || {};
var origins = data . by_origin || data . data || ( Array . isArray ( data ) ? data : []);
var statsHtml = '<div class="stat-box"><div class="stat-value">' + fmtBigPlain ( totals . total_refugees || 0 ) + '</div><div class="stat-label">Refugees</div></div><div class="stat-box"><div class="stat-value">' + fmtBigPlain ( totals . total_idps || 0 ) + '</div><div class="stat-label">IDPs</div></div><div class="stat-box"><div class="stat-value">' + fmtBigPlain ( totals . total_asylum_seekers || 0 ) + '</div><div class="stat-label">Asylum Seekers</div></div><div class="stat-box"><div class="stat-value warn">' + fmtBigPlain ( totals . grand_total || 0 ) + '</div><div class="stat-label">Total Displaced</div></div>' ;
if ( $ ( '#displacementStats' )) $ ( '#displacementStats' ). innerHTML = safe ( statsHtml );
var rows1 = ( Array . isArray ( origins ) ? origins : []). slice ( 0 , 10 ). map ( function ( c ) {
return '<tr><td class="bright">' + esc ( c . country_name || c . country || '?' ) + '</td><td>' + fmtBigPlain ( c . refugees || 0 ) + '</td><td>' + fmtBigPlain ( c . idps || 0 ) + '</td></tr>' ;
}). join ( '' );
if ( $ ( '#displacementBody' )) $ ( '#displacementBody' ). innerHTML = safe ( rows1 );
if ( $ ( '#displacementStats2' )) $ ( '#displacementStats2' ). innerHTML = safe ( statsHtml );
var rows2 = ( Array . isArray ( origins ) ? origins : []). slice ( 0 , 20 ). map ( function ( c ) {
return '<tr><td class="bright">' + esc ( c . country_name || c . country || '?' ) + '</td><td>' + fmtBigPlain ( c . refugees || 0 ) + '</td><td>' + fmtBigPlain ( c . asylum_seekers || 0 ) + '</td><td>' + fmtBigPlain ( c . idps || 0 ) + '</td><td class="muted">' + fmtBigPlain ( c . stateless || 0 ) + '</td></tr>' ;
}). join ( '' );
if ( $ ( '#displacementBody2' )) $ ( '#displacementBody2' ). innerHTML = safe ( rows2 );
}
function updateRiskScores ( data ) {
if ( ! data || data . error ) { return ; }
var countries = data . countries || data . data || ( Array . isArray ( data ) ? data : []);
var badge = ( data . count || countries . length ) + ' countries' ;
var mkRows = function ( limit ) {
return countries . slice ( 0 , limit ). map ( function ( c ) {
return '<tr><td class="bright">' + esc ( c . country || c . country_name || '?' ) + '</td><td>' + fmtNum ( c . risk_score , 1 ) + '</td><td class="' + riskCls ( c . risk_level ) + '">' + esc ( c . risk_level || '\u2014' ) + '</td><td class="muted">' + ( c . events_30d || '\u2014' ) + '</td></tr>' ;
}). join ( '' );
};
[ '' , '2' ]. forEach ( function ( sfx ) {
var b = $ ( '#riskBadge' + sfx ); if ( b ) b . textContent = badge ;
var bd = $ ( '#riskBody' + sfx ); if ( bd ) bd . innerHTML = safe ( mkRows ( sfx === '' ? 10 : 15 ));
});
}
function updateSignalConvergence ( data ) {
if ( ! data || data . error ) { return ; }
var hotspots = data . hotspots || data . data || ( Array . isArray ( data ) ? data : []);
var mkRows = function ( limit ) {
return hotspots . slice ( 0 , limit ). map ( function ( h ) {
var score = h . convergence_score || 0 ;
var signals = h . signals || {};
var sigStr = Object . entries ( signals ). map ( function ( e ) { return e [ 0 ] + ':' + e [ 1 ]; }). join ( ', ' );
return '<tr><td class="bright">' + esc ( h . name || '?' ) + '</td><td class="' + ( score >= 7 ? 'sev-critical' : score >= 4 ? 'warn' : 'muted' ) + '">' + fmtNum ( score , 1 ) + '</td><td class="muted">' + esc ( sigStr || '\u2014' ) + '</td></tr>' ;
}). join ( '' );
};
if ( $ ( '#convergenceBody' )) $ ( '#convergenceBody' ). innerHTML = safe ( mkRows ( 8 ));
if ( $ ( '#convergenceBody2' )) $ ( '#convergenceBody2' ). innerHTML = safe ( mkRows ( 15 ));
}
// Overview summary stats
function updateOverviewStats ( data ) {
var quakeCount = 0 , cyberCount = 0 , conflictCount = 0 , fireCount = 0 , displacedTotal = 0 , milCount = 0 ;
if ( data . earthquakes && ! data . earthquakes . error ) { quakeCount = ( data . earthquakes . earthquakes || data . earthquakes . features || []). length ; }
if ( data . cyber_threats && ! data . cyber_threats . error ) { cyberCount = ( data . cyber_threats . threats || []). length ; }
if ( data . acled_events && ! data . acled_events . error ) { conflictCount = data . acled_events . count || 0 ; }
if ( data . wildfires && ! data . wildfires . error ) { fireCount = data . wildfires . fire_count || data . wildfires . count || 0 ; }
if ( data . displacement && ! data . displacement . error && data . displacement . global_totals ) { displacedTotal = data . displacement . global_totals . grand_total || 0 ; }
if ( data . military_flights && ! data . military_flights . error ) { milCount = data . military_flights . total_military_aircraft || 0 ; }
var html = '<div class="stat-box"><div class="stat-value">' + quakeCount + '</div><div class="stat-label">Earthquakes</div></div>' +
'<div class="stat-box"><div class="stat-value">' + cyberCount + '</div><div class="stat-label">Cyber IOCs</div></div>' +
'<div class="stat-box"><div class="stat-value ' + ( conflictCount > 100 ? 'warn' : '' ) + '">' + conflictCount + '</div><div class="stat-label">Conflict Events</div></div>' +
'<div class="stat-box"><div class="stat-value ' + ( fireCount > 100 ? 'warn' : '' ) + '">' + fireCount + '</div><div class="stat-label">Wildfires</div></div>' +
'<div class="stat-box"><div class="stat-value">' + milCount + '</div><div class="stat-label">Mil. Aircraft</div></div>' +
'<div class="stat-box"><div class="stat-value warn">' + fmtBigPlain ( displacedTotal ) + '</div><div class="stat-label">Displaced</div></div>' ;
$ ( '#overviewStats' ). innerHTML = safe ( html );
}
// ========================================================================
// Geospatial Map (Leaflet) - initialized on tab click
// ========================================================================
var map = null , mapInitialized = false ;
var mapLayers = { quakes : null , flights : null , conflict : null , convergence : null };
var latestData = null ;
function initMap () {
if ( mapInitialized ) return ;
mapInitialized = true ;
map = L . map ( 'geoMap' , { zoomControl : true , attributionControl : false }). setView ([ 20 , 0 ], 2 );
L . tileLayer ( 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png' , { maxZoom : 18 }). addTo ( map );
mapLayers . quakes = L . layerGroup (). addTo ( map );
mapLayers . flights = L . layerGroup (). addTo ( map );
mapLayers . conflict = L . layerGroup (). addTo ( map );
mapLayers . convergence = L . layerGroup (). addTo ( map );
[ 'mapQuakes' , 'mapFlights' , 'mapConflict' , 'mapConvergence' ]. forEach ( function ( id ) {
var el = document . getElementById ( id );
if ( el ) el . addEventListener ( 'change' , function () { updateMapLayers (); });
});
if ( latestData ) updateMapLayers ();
}
function updateMapLayers () {
if ( ! map || ! latestData ) return ;
var data = latestData ;
mapLayers . quakes . clearLayers ();
if ( document . getElementById ( 'mapQuakes' ). checked && data . earthquakes && ! data . earthquakes . error ) {
( data . earthquakes . earthquakes || data . earthquakes . features || []). slice ( 0 , 50 ). forEach ( function ( q ) {
var lat = q . latitude || ( q . geometry && q . geometry . coordinates && q . geometry . coordinates [ 1 ]);
var lon = q . longitude || ( q . geometry && q . geometry . coordinates && q . geometry . coordinates [ 0 ]);
var mag = q . magnitude || ( q . properties && q . properties . mag ) || 1 ;
if ( lat != null && lon != null ) {
L . circleMarker ([ lat , lon ], { radius : Math . max ( 3 , mag * 2.5 ), color : '#ef4444' , fillColor : '#ef4444' , fillOpacity : 0.6 , weight : 1
}). bindPopup ( '<b>M' + ( typeof mag === 'number' ? mag . toFixed ( 1 ) : mag ) + '</b><br>' + ( q . place || ( q . properties && q . properties . place ) || '?' )). addTo ( mapLayers . quakes );
}
});
}
mapLayers . flights . clearLayers ();
if ( document . getElementById ( 'mapFlights' ). checked && data . military_flights && ! data . military_flights . error ) {
( data . military_flights . flights || data . military_flights . aircraft || []). slice ( 0 , 50 ). forEach ( function ( f ) {
var lat = f . latitude || f . lat , lon = f . longitude || f . lon ;
if ( lat != null && lon != null ) {
L . circleMarker ([ lat , lon ], { radius : 4 , color : '#60a5fa' , fillColor : '#60a5fa' , fillOpacity : 0.7 , weight : 1
}). bindPopup ( '<b>' + ( f . callsign || f . icao24 || '?' ) + '</b><br>' + ( f . origin_country || '?' )). addTo ( mapLayers . flights );
}
});
}
mapLayers . conflict . clearLayers ();
if ( document . getElementById ( 'mapConflict' ). checked && data . acled_events && ! data . acled_events . error ) {
( data . acled_events . events || []). slice ( 0 , 50 ). forEach ( function ( e ) {
var lat = e . latitude || e . lat , lon = e . longitude || e . lon ;
if ( lat != null && lon != null ) {
var fat = e . fatalities || 0 ;
L . circleMarker ([ Number ( lat ), Number ( lon )], { radius : Math . max ( 3 , Math . min ( 10 , fat / 2 + 3 )), color : '#f59e0b' , fillColor : '#f59e0b' , fillOpacity : 0.5 , weight : 1
}). bindPopup ( '<b>' + ( e . event_type || '?' ) + '</b><br>' + ( e . country || '' ) + ' - ' + fat + ' fatalities' ). addTo ( mapLayers . conflict );
}
});
}
mapLayers . convergence . clearLayers ();
if ( document . getElementById ( 'mapConvergence' ). checked && data . signal_convergence && ! data . signal_convergence . error ) {
( data . signal_convergence . hotspots || []). forEach ( function ( h ) {
if ( h . lat != null && h . lon != null ) {
var score = h . convergence_score || 0 ;
L . circle ([ h . lat , h . lon ], { radius : 200000 + score * 50000 , color : '#a78bfa' , fillColor : '#a78bfa' , fillOpacity : 0.15 , weight : 2
}). bindPopup ( '<b>' + ( h . name || '?' ) + '</b><br>Score: ' + score . toFixed ( 1 )). addTo ( mapLayers . convergence );
}
});
}
}
// ========================================================================
// Trend Charts (Chart.js) - rolling time series from SSE updates
// ========================================================================
var MAX_CHART_POINTS = 30 ;
var chartData = { labels : [], quakes : [], cyber : [], military : [], conflict : [] };
var charts = {};
function initCharts () {
var commonOpts = {
responsive : true , maintainAspectRatio : false ,
scales : {
x : { display : true , ticks : { color : '#4a5568' , maxRotation : 0 , maxTicksLimit : 8 , font : { size : 10 } }, grid : { color : '#1e293b' } },
y : { display : true , ticks : { color : '#4a5568' , font : { size : 10 } }, grid : { color : '#1e293b' }, beginAtZero : true }
},
plugins : { legend : { display : false } }, animation : { duration : 300 }
};
charts . quakes = new Chart ( $ ( '#chartQuakes' ), { type : 'line' , data : { labels : [], datasets : [{ data : [], borderColor : '#ef4444' , backgroundColor : 'rgba(239,68,68,0.1)' , fill : true , tension : 0.3 , pointRadius : 2 }] }, options : commonOpts });
charts . cyber = new Chart ( $ ( '#chartCyber' ), { type : 'bar' , data : { labels : [], datasets : [{ data : [], backgroundColor : 'rgba(245,158,11,0.6)' , borderColor : '#f59e0b' , borderWidth : 1 }] }, options : commonOpts });
charts . military = new Chart ( $ ( '#chartMilitary' ), { type : 'line' , data : { labels : [], datasets : [{ data : [], borderColor : '#60a5fa' , backgroundColor : 'rgba(96,165,250,0.1)' , fill : true , tension : 0.3 , pointRadius : 2 }] }, options : commonOpts });
charts . conflict = new Chart ( $ ( '#chartConflict' ), { type : 'bar' , data : { labels : [], datasets : [{ data : [], backgroundColor : 'rgba(167,139,250,0.6)' , borderColor : '#a78bfa' , borderWidth : 1 }] }, options : commonOpts });
}
function updateCharts ( data ) {
var now = new Date ();
var label = now . getHours (). toString (). padStart ( 2 , '0' ) + ':' + now . getMinutes (). toString (). padStart ( 2 , '0' );
chartData . labels . push ( label );
var qC = 0 , cC = 0 , mC = 0 , aC = 0 ;
if ( data . earthquakes && ! data . earthquakes . error ) { qC = ( data . earthquakes . earthquakes || data . earthquakes . features || []). length ; }
if ( data . cyber_threats && ! data . cyber_threats . error ) { cC = ( data . cyber_threats . threats || []). length ; }
if ( data . military_flights && ! data . military_flights . error ) { mC = data . military_flights . total_military_aircraft || 0 ; }
if ( data . acled_events && ! data . acled_events . error ) { aC = data . acled_events . count || 0 ; }
chartData . quakes . push ( qC ); chartData . cyber . push ( cC ); chartData . military . push ( mC ); chartData . conflict . push ( aC );
if ( chartData . labels . length > MAX_CHART_POINTS ) { chartData . labels . shift (); chartData . quakes . shift (); chartData . cyber . shift (); chartData . military . shift (); chartData . conflict . shift (); }
[ 'quakes' , 'cyber' , 'military' , 'conflict' ]. forEach ( function ( k ) {
if ( charts [ k ]) { charts [ k ]. data . labels = chartData . labels . slice (); charts [ k ]. data . datasets [ 0 ]. data = chartData [ k ]. slice (); charts [ k ]. update ( 'none' ); }
});
}
initCharts ();
2026-02-23 12:20:48 -05:00
// ========================================================================
// Master update
// ========================================================================
function updateAll ( data ) {
2026-02-23 12:49:11 -05:00
latestData = data ;
updateOverviewStats ( data );
2026-02-23 12:20:48 -05:00
updateEquity ( data . market_quotes );
updateCrypto ( data . crypto_quotes );
2026-02-23 12:49:11 -05:00
updateStablecoins ( data . stablecoin_status );
2026-02-23 12:20:48 -05:00
updateMacro ( data . macro_signals );
updateSectorHeatmap ( data . sector_heatmap );
updateEnergy ( data . energy_prices );
2026-02-23 12:49:11 -05:00
updateETF ( data . etf_flows );
2026-02-23 12:20:48 -05:00
updateCyber ( data . cyber_threats );
updateMilitary ( data . military_flights );
updateInfra ( data . internet_outages , data . cable_health );
2026-02-23 12:49:11 -05:00
updateAviation ( data . airport_delays );
updateNavWarnings ( data . nav_warnings );
updateRiskScores ( data . risk_scores );
updateACLED ( data . acled_events );
updateUCDP ( data . ucdp_events );
updateDisplacement ( data . displacement );
updateSignalConvergence ( data . signal_convergence );
2026-02-23 12:20:48 -05:00
updateNatural ( data . earthquakes , data . wildfires );
2026-02-23 12:49:11 -05:00
updateClimate ( data . climate_anomalies );
2026-02-23 12:20:48 -05:00
updateNews ( data . news_feed , data . trending_keywords );
updatePredictions ( data . prediction_markets );
2026-02-23 12:49:11 -05:00
if ( mapInitialized ) updateMapLayers ();
updateCharts ( data );
var feedKeys = Object . keys ( data ). filter ( function ( k ) { return k !== 'source_health' && k !== 'cache_stats' && k !== 'timestamp' ; });
var okCount = feedKeys . filter ( function ( k ) { return data [ k ] && ! data [ k ]. error ; }). length ;
$ ( '#feedCount' ). textContent = okCount + '/' + feedKeys . length + ' feeds' ;
if ( data . timestamp ) { $ ( '#lastUpdate' ). textContent = new Date ( data . timestamp ). toLocaleTimeString (); }
2026-02-23 12:20:48 -05:00
}
// ========================================================================
2026-02-23 12:49:11 -05:00
// SSE connection with exponential backoff
2026-02-23 12:20:48 -05:00
// ========================================================================
var reconnectDelay = 1000 ;
function connectSSE () {
var source = new EventSource ( '/api/stream' );
2026-02-23 12:49:11 -05:00
source . onopen = function () { $ ( '#connDot' ). className = 'status-dot dot-green pulse' ; $ ( '#connLabel' ). textContent = 'Live' ; reconnectDelay = 1000 ; };
2026-02-23 12:20:48 -05:00
source . onmessage = function ( event ) {
try {
var data = JSON . parse ( event . data );
2026-02-23 12:49:11 -05:00
if ( data . error && ! data . market_quotes ) { return ; }
2026-02-23 12:20:48 -05:00
updateAll ( data );
document . getElementById ( 'loading' ). classList . add ( 'hidden' );
2026-02-23 12:49:11 -05:00
} catch ( e ) { console . error ( 'Parse error:' , e ); }
2026-02-23 12:20:48 -05:00
};
2026-02-23 12:49:11 -05:00
source . onerror = function () { source . close (); $ ( '#connDot' ). className = 'status-dot dot-red' ; $ ( '#connLabel' ). textContent = 'Reconnecting...' ; setTimeout ( connectSSE , reconnectDelay ); reconnectDelay = Math . min ( reconnectDelay * 2 , 30000 ); };
2026-02-23 12:20:48 -05:00
}
connectSSE ();
</ script >
</ body >
</ html >