Improve mobile dashboard layout and map defaults

This commit is contained in:
calesthio
2026-03-18 10:52:04 -07:00
parent 203f359028
commit d570ca6887

View File

@@ -226,7 +226,25 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
/* RESPONSIVE */
@media(max-width:1400px){.grid{grid-template-columns:240px 1fr 320px}.metrics-row{grid-template-columns:repeat(3,1fr)}.src-grid{grid-template-columns:repeat(3,1fr)}}
@media(max-width:1100px){.grid{grid-template-columns:1fr}.lower .lp-ticker,.lower .lp-delta,.lower .lp-macro,.lower .lp-ideas{flex:1 1 100%;max-width:none}.metrics-row{grid-template-columns:repeat(2,1fr)}.src-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:1100px){
#main{padding:8px}
.topbar{padding:10px 12px}
.top-left,.top-center,.top-right{width:100%}
.top-center{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}
.top-right{gap:6px}
.region-btn,.meta-pill,.alert-badge{font-size:10px}
.grid{display:flex;flex-direction:column}
#centerCol{order:1}
#rightRail{order:2}
#leftRail{order:3}
.map-container{min-height:420px}
.map-hint{font-size:8px;right:8px}
.map-legend{left:8px;right:8px;bottom:8px;gap:4px}
.leg-item{font-size:8px}
.lower .lp-ticker,.lower .lp-osint,.lower .lp-delta,.lower .lp-macro,.lower .lp-ideas{flex:1 1 100%;max-width:none}
.metrics-row{grid-template-columns:repeat(2,1fr)}
.src-grid{grid-template-columns:repeat(2,1fr)}
}
/* CONFLICT LAYER */
@keyframes pulse-conflict{0%,100%{opacity:0.5;stroke-width:1.5}50%{opacity:0.9;stroke-width:2.5}}
@@ -295,7 +313,7 @@ let D = null;
// === GLOBALS ===
let globe = null;
let flightsVisible = true;
let isFlat = true;
let isFlat = !isMobileLayout();
let flatSvg, flatProjection, flatPath, flatG, flatZoom, flatW, flatH;
const regionPOV = {
world: { lat: 20, lng: 20, altitude: 1.8 },
@@ -486,18 +504,27 @@ function initMap(){
});
// Resize handler
window.addEventListener('resize', () => {
const c = document.getElementById('mapContainer');
globe.width(c.clientWidth).height(c.clientHeight || 560);
window.addEventListener('resize', () => syncResponsiveLayout());
window.addEventListener('orientationchange', () => setTimeout(() => syncResponsiveLayout(true), 150));
document.addEventListener('visibilitychange', () => {
if(!document.hidden) setTimeout(() => syncResponsiveLayout(true), 150);
});
window.addEventListener('pageshow', () => setTimeout(() => syncResponsiveLayout(true), 150));
// Plot globe markers (preloaded but hidden)
plotMarkers();
// Start in flat mode — hide globe, show flat map
document.getElementById('globeViz').style.display = 'none';
document.getElementById('flatMapSvg').style.display = 'block';
initFlatMap();
if(isFlat){
document.getElementById('globeViz').style.display = 'none';
document.getElementById('flatMapSvg').style.display = 'block';
initFlatMap();
} else {
document.getElementById('globeViz').style.display = 'block';
document.getElementById('flatMapSvg').style.display = 'none';
document.getElementById('projToggle').textContent = 'FLAT MODE';
document.getElementById('mapHint').textContent = 'DRAG TO ROTATE · SCROLL TO ZOOM';
}
// Legend
document.getElementById('mapLegend').innerHTML=
@@ -976,6 +1003,7 @@ function mkSparkSvg(values, isGood){
// === LOWER GRID ===
function renderLower(){
const mobile = isMobileLayout();
const spread=D.fred.find(f=>f.id==='T10Y2Y');
const ff=D.fred.find(f=>f.id==='DFF');
const ue=D.bls.find(b=>b.id==='LNS14000000');
@@ -1092,23 +1120,14 @@ function renderLower(){
}
const deltaHtml = hasDelta ? deltaRows.join('') : '<div style="padding:12px;text-align:center;color:var(--dim);font-family:var(--mono);font-size:10px">No changes since last sweep</div>';
document.getElementById('lowerGrid').innerHTML=`
<div class="g-panel lp-ticker" style="display:flex;flex-direction:column">
const tickerPanel = `<div class="g-panel lp-ticker" style="display:flex;flex-direction:column">
<div class="sec-head"><h3>Live News Ticker</h3><span class="badge">${feed.length} ITEMS</span></div>
<div class="ticker-wrap" style="--ticker-duration:${tickerDuration}s">
<div class="ticker-track">${tickerCards}${tickerCards}</div>
</div>
</div>
<div class="g-panel lp-delta">
<div class="sec-head"><h3>Sweep Delta</h3><span class="badge ${dirClass}">${dirEmoji} ${ds.direction?ds.direction.toUpperCase():'BASELINE'}</span></div>
${hasDelta?`<div style="display:flex;gap:12px;margin-bottom:6px;font-family:var(--mono);font-size:10px">
<span style="color:var(--dim)">Changes: <span style="color:var(--accent)">${ds.totalChanges}</span></span>
<span style="color:var(--dim)">Critical: <span style="color:${ds.criticalChanges>0?'var(--warn)':'var(--dim)'}">${ds.criticalChanges||0}</span></span>
${ds.signalBreakdown?`<span style="color:var(--dim)">New: <span style="color:#4dd0e1">${ds.signalBreakdown.new}</span> &#8593;${ds.signalBreakdown.escalated} &#8595;${ds.signalBreakdown.deescalated}</span>`:''}
</div>`:''}
<div class="delta-list">${deltaHtml}</div>
</div>
<div class="g-panel lp-macro">
</div>`;
const osintPanel = mobile ? buildOsintPanel('lp-osint', 240) : '';
const macroPanel = `<div class="g-panel lp-macro">
<div class="sec-head"><h3>Macro + Markets</h3><span class="badge">${mkt.timestamp?'LIVE':'DELAYED'}</span></div>
${hasMarkets?`<div style="margin-bottom:8px">
<div style="font-family:var(--mono);font-size:9px;color:var(--dim);margin-bottom:4px;letter-spacing:1px">INDEXES</div>
@@ -1129,33 +1148,32 @@ function renderLower(){
<div style="font-family:var(--mono);font-size:10px;color:var(--dim);margin-bottom:4px">WTI 5-DAY</div>
<div class="spark">${sparkHtml}</div>
</div>
</div>
<div class="g-panel lp-ideas">
</div>`;
const ideasPanel = `<div class="g-panel lp-ideas">
<div class="sec-head"><h3>Leverageable Ideas</h3>${D.ideasSource==='llm'?'<span class="ideas-src llm">AI ENHANCED</span>':D.ideasSource==='disabled'?'<span class="ideas-src static">LLM OFF</span>':'<span class="ideas-src static">PENDING</span>'}</div>
${ideasHtml}
<div class="disclosure">FOR INFORMATIONAL PURPOSES ONLY. This is not financial advice, a recommendation to buy or sell any security, or a solicitation of any kind. All signal-based observations are derived from publicly available OSINT data and should not be relied upon for investment decisions. Consult a licensed financial advisor before making any investment. Past performance does not guarantee future results.</div>
</div>`;
const deltaPanel = `<div class="g-panel lp-delta">
<div class="sec-head"><h3>Sweep Delta</h3><span class="badge ${dirClass}">${dirEmoji} ${ds.direction?ds.direction.toUpperCase():'BASELINE'}</span></div>
${hasDelta?`<div style="display:flex;gap:12px;margin-bottom:6px;font-family:var(--mono);font-size:10px">
<span style="color:var(--dim)">Changes: <span style="color:var(--accent)">${ds.totalChanges}</span></span>
<span style="color:var(--dim)">Critical: <span style="color:${ds.criticalChanges>0?'var(--warn)':'var(--dim)'}">${ds.criticalChanges||0}</span></span>
${ds.signalBreakdown?`<span style="color:var(--dim)">New: <span style="color:#4dd0e1">${ds.signalBreakdown.new}</span> &#8593;${ds.signalBreakdown.escalated} &#8595;${ds.signalBreakdown.deescalated}</span>`:''}
</div>`:''}
<div class="delta-list">${deltaHtml}</div>
</div>`;
document.getElementById('lowerGrid').innerHTML=`${tickerPanel}${osintPanel}${macroPanel}${ideasPanel}${deltaPanel}`;
}
// === RIGHT RAIL ===
function renderRight(){
const mobile = isMobileLayout();
// CROSS-SOURCE SIGNALS — moved from lower grid to right rail
const signals=D.tSignals.slice(0,6).map((s,i)=>`<div class="signal-row"><strong>Signal ${i+1}</strong><p>${s}</p></div>`).join('');
// OSINT TICKER — Telegram + WHO as flowing cards
const allPosts=[...D.tg.urgent,...D.tg.topPosts].sort((a,b)=>new Date(b.date||0)-new Date(a.date||0));
const whoItems=D.who.slice(0,4).map(w=>({channel:'WHO ALERT',text:w.title,date:w.date,isWho:true}));
const osintItems=[...allPosts.slice(0,15),...whoItems];
const osintCards=osintItems.map(p=>{
const isU=p.urgentFlags&&p.urgentFlags.length>0;
const views=p.views?p.views>=1000?`${(p.views/1000).toFixed(0)}K`:p.views:'';
const age=p.date?getAge(p.date):'';
const flags=(p.urgentFlags||[]).map(f=>`<span class="tk-src tg" style="margin-right:2px">${f}</span>`).join('');
const srcCls=p.isWho?'style="color:#69f0ae;border-color:rgba(105,240,174,0.4)"':'class="tk-src tg"';
return `<div class="tk-card ${isU?'urgent':''}"><span ${srcCls}>${(p.channel||'OSINT').toUpperCase().substring(0,14)}</span>${views?`<span class="tk-src other">${views}</span>`:''}<span class="tk-time">${age}</span>${flags}<div class="tk-head">${cleanText((p.text||'').substring(0,160))}</div></div>`;
}).join('');
const osintDuration=Math.max(25,osintItems.length*3);
const signalMetrics=[
{l:'Incident Tempo',v:D.tg.urgent.length,p:70},
{l:'Air Theaters',v:D.air.length,p:60},
@@ -1166,17 +1184,12 @@ function renderRight(){
];
document.getElementById('rightRail').innerHTML=`
<div class="g-panel">
<div class="g-panel right-signals">
<div class="sec-head"><h3>Cross-Source Signals</h3><span class="badge">WORLDVIEW</span></div>
${signals}
</div>
<div class="g-panel" style="display:flex;flex-direction:column">
<div class="sec-head"><h3>OSINT Stream</h3><span class="badge">${D.tg.urgent.length} URGENT</span></div>
<div class="ticker-wrap" style="--ticker-duration:${osintDuration}s;max-height:260px">
<div class="ticker-track">${osintCards}${osintCards}</div>
</div>
</div>
<div class="g-panel">
${mobile ? '' : buildOsintPanel('right-osint', 260)}
<div class="g-panel right-core">
<div class="sec-head"><h3>Signal Core</h3><span class="badge">HOT METRICS</span></div>
${signalMetrics.map(s=>`<div class="sm"><span class="sml">${s.l}</span><div class="smb"><span style="width:${s.p}%"></span></div><span class="smv">${s.v}</span></div>`).join('')}
</div>`;
@@ -1210,8 +1223,8 @@ function runBoot(){
tl.call(()=>{
const div=document.createElement('div');div.innerHTML=line.text;div.style.opacity='0';
container.appendChild(div);gsap.to(div,{opacity:1,duration:0.2});
},null,line.delay/1000+0.5);
});
},[],line.delay/1000+0.5);
});
tl.to('#bootFinal',{opacity:1,duration:0.4},3.1);
tl.to('#boot',{opacity:0,duration:0.5,ease:'power2.in'},3.7);
tl.set('#boot',{display:'none'},4.2);
@@ -1232,6 +1245,70 @@ function runBoot(){
},4.0);
}
function isMobileLayout(){ return window.innerWidth <= 1100; }
function buildOsintPanel(panelClass='', maxHeight=260){
const allPosts=[...D.tg.urgent,...D.tg.topPosts].sort((a,b)=>new Date(b.date||0)-new Date(a.date||0));
const whoItems=D.who.slice(0,4).map(w=>({channel:'WHO ALERT',text:w.title,date:w.date,isWho:true}));
const osintItems=[...allPosts.slice(0,15),...whoItems];
const osintCards=osintItems.map(p=>{
const isU=p.urgentFlags&&p.urgentFlags.length>0;
const views=p.views?p.views>=1000?`${(p.views/1000).toFixed(0)}K`:p.views:'';
const age=p.date?getAge(p.date):'';
const flags=(p.urgentFlags||[]).map(f=>`<span class="tk-src tg" style="margin-right:2px">${f}</span>`).join('');
const srcCls=p.isWho?'style="color:#69f0ae;border-color:rgba(105,240,174,0.4)"':'class="tk-src tg"';
return `<div class="tk-card ${isU?'urgent':''}"><span ${srcCls}>${(p.channel||'OSINT').toUpperCase().substring(0,14)}</span>${views?`<span class="tk-src other">${views}</span>`:''}<span class="tk-time">${age}</span>${flags}<div class="tk-head">${cleanText((p.text||'').substring(0,160))}</div></div>`;
}).join('');
const osintDuration=Math.max(25,osintItems.length*3);
return `<div class="g-panel ${panelClass}" style="display:flex;flex-direction:column">
<div class="sec-head"><h3>OSINT Stream</h3><span class="badge">${D.tg.urgent.length} URGENT</span></div>
<div class="ticker-wrap" style="--ticker-duration:${osintDuration}s;max-height:${maxHeight}px">
<div class="ticker-track">${osintCards}${osintCards}</div>
</div>
</div>`;
}
function refreshMapViewport(forceGlobeReflow=false){
const container = document.getElementById('mapContainer');
if(!container) return;
const width = container.clientWidth;
const height = container.clientHeight || (isMobileLayout() ? 420 : 560);
if(globe){
globe.width(width).height(height);
if(forceGlobeReflow && !isFlat){
const globeEl = document.getElementById('globeViz');
globeEl.style.display = 'none';
requestAnimationFrame(() => {
globeEl.style.display = 'block';
globe.width(width).height(height);
});
}
}
if(flatSvg){
flatW = width;
flatH = height;
flatSvg.attr('viewBox',`0 0 ${flatW} ${flatH}`).attr('preserveAspectRatio','xMidYMid meet');
if(flatProjection && flatG){
flatProjection = d3.geoNaturalEarth1().fitSize([flatW-20,flatH-20],{type:'Sphere'}).translate([flatW/2,flatH/2]);
flatPath = d3.geoPath(flatProjection);
flatG.selectAll('*').remove();
drawFlatMap();
}
}
}
let lastResponsiveMobile = null;
function syncResponsiveLayout(force=false){
const mobileNow = isMobileLayout();
if(force || lastResponsiveMobile === null || mobileNow !== lastResponsiveMobile){
lastResponsiveMobile = mobileNow;
renderLeftRail();
renderLower();
renderRight();
}
refreshMapViewport(force && !isFlat);
}
// === REINIT (for live updates without boot sequence) ===
function reinit(){
renderTopbar();renderLeftRail();renderLower();renderRight();
@@ -1288,6 +1365,7 @@ function init(){
if(url) window.open(url,'_blank','noopener');
}
});
syncResponsiveLayout(true);
}
document.addEventListener('DOMContentLoaded', () => {