/* ═══════════════════════════════════════════════════════════════════
SHADOW PANEL · right rail
Shadow AI status + thought stream + top movers + market context mini
═══════════════════════════════════════════════════════════════════ */
function ShadowPanel({ thoughts, movers, intel }) {
return (
);
}
/* ═══════════════════════════════════════════════════════════════════
MY HOLDINGS · real portfolio overview (replaces the duplicate market
context — that data already lives in the top Intel Bar)
═══════════════════════════════════════════════════════════════════ */
function MyHoldingsPanel() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
ST_fetch('/market/assets').then(setData).catch(() => setData({ holdings: [] }));
}, []);
const holdings = data?.holdings || [];
const total = data?.total_value;
const pnlPct = data?.total_pnl_pct;
const pnlUsd = data?.total_pnl;
return (
MY HOLDINGS
{holdings.length} coins
{total != null ? `$${total.toLocaleString('en-US', { maximumFractionDigits: 0 })}` : '—'}
{pnlPct != null && (
= 0 ? ST.green : ST.red }}>
{pnlPct >= 0 ? '+' : ''}{pnlPct.toFixed(2)}%
{pnlUsd != null && ` (${pnlUsd >= 0 ? '+' : '-'}$${Math.abs(pnlUsd).toLocaleString('en-US', { maximumFractionDigits: 0 })})`}
)}
);
}
window.MyHoldingsPanel = MyHoldingsPanel;
/* ═══════════════════════════════════════════════════════════════════
Shadow AI Status
═══════════════════════════════════════════════════════════════════ */
/* CSS-only scan animation — eliminates the 420ms JS re-render loop */
(function injectScanCSS() {
if (document.getElementById('st-scan-anim')) return;
const s = document.createElement('style');
s.id = 'st-scan-anim';
s.textContent = `
@keyframes st-scan-pulse {
0%,100% { opacity:0.3 }
50% { opacity:1; box-shadow:0 0 6px #F7A600 }
}
.st-scan-bar { height:8px; border-radius:1px; background:#1E2130; animation:st-scan-pulse 2s ease-in-out infinite; }
.st-scan-bar:nth-child(1){animation-delay:0s} .st-scan-bar:nth-child(2){animation-delay:.08s}
.st-scan-bar:nth-child(3){animation-delay:.16s} .st-scan-bar:nth-child(4){animation-delay:.24s}
.st-scan-bar:nth-child(5){animation-delay:.32s} .st-scan-bar:nth-child(6){animation-delay:.40s}
.st-scan-bar:nth-child(7){animation-delay:.48s} .st-scan-bar:nth-child(8){animation-delay:.56s}
.st-scan-bar:nth-child(9){animation-delay:.64s} .st-scan-bar:nth-child(10){animation-delay:.72s}
.st-scan-bar:nth-child(11){animation-delay:.80s} .st-scan-bar:nth-child(12){animation-delay:.88s}
.st-scan-bar:nth-child(13){animation-delay:.96s} .st-scan-bar:nth-child(14){animation-delay:1.04s}
.st-scan-bar:nth-child(15){animation-delay:1.12s} .st-scan-bar:nth-child(16){animation-delay:1.20s}
.st-scan-bar:nth-child(17){animation-delay:1.28s} .st-scan-bar:nth-child(18){animation-delay:1.36s}
.st-scan-bar:nth-child(19){animation-delay:1.44s} .st-scan-bar:nth-child(20){animation-delay:1.52s}
.st-scan-bar:nth-child(21){animation-delay:1.60s} .st-scan-bar:nth-child(22){animation-delay:1.68s}
.st-scan-bar:nth-child(23){animation-delay:1.76s} .st-scan-bar:nth-child(24){animation-delay:1.84s}
`;
document.head.appendChild(s);
})();
function ShadowAIStatus() {
/* Removed 420ms pulse interval — replaced with CSS animation */
/* ── live status from API ── */
/* API: {scan_count, last_scan_ts(ms), last_regime, is_scanning, coins_tracked, log_stats} */
const [status, setStatus] = React.useState(null);
React.useEffect(() => {
const load = () => ST_fetch('/shadow/status').then(d => setStatus(d)).catch(() => {});
load();
const id = setInterval(load, 60000);
return () => clearInterval(id);
}, []);
/* ── countdown: last_scan_ts is milliseconds from API ── */
const [secs, setSecs] = React.useState(347);
React.useEffect(() => {
if (status?.last_scan_ts) {
const tsMs = status.last_scan_ts > 1e12
? status.last_scan_ts /* already ms */
: status.last_scan_ts * 1000; /* was seconds */
const elapsed = Math.floor((Date.now() - tsMs) / 1000);
setSecs(Math.max(0, 600 - (elapsed % 600)));
}
}, [status]);
React.useEffect(() => {
const id = setInterval(() => setSecs(s => (s <= 0 ? 600 : s - 1)), 1000);
return () => clearInterval(id);
}, []);
const mm = Math.floor(secs / 60);
const ss_str = String(secs % 60).padStart(2, '0');
const scanning = secs <= 1;
/* API fields: last_regime (not regime), scan_count, log_stats.by_type.SIGNAL */
const regime = status?.last_regime || status?.regime || 'BEARISH';
const scanCount = status?.scan_count != null ? Number(status.scan_count).toLocaleString('en-US') : '—';
const sigCount = status?.log_stats?.by_type?.SIGNAL ?? status?.active_signals ?? 0;
const signals = sigCount;
const winRate = 0; /* win_rate comes from /shadow/accuracy, not /shadow/status */
const winRatePct = 0;
/* re-use mm/ss names expected below */
const mm_disp = mm;
const ss_disp = ss_str;
return (
{/* header */}
Shadow AI
SCANNING · 24 COINS
NEXT SCAN
{scanning ? 'NOW' : `${mm_disp}:${ss_disp}`}
{/* scan progress visualizer — CSS animation, no JS re-renders */}
{Array.from({ length: 24 }).map((_, i) => (
))}
{/* stats grid */}
{/* win rate meter */}
WIN RATE · TARGET 90%
LEARNING
{/* target zone (above 70%) */}
{/* target line at 90% */}
0/1 signals scored
need 30+ to grade
{/* Bot status */}
BOT-2 · PAPER
NEAR · DOT · LINK
ORACLE · 24/7
);
}
function StatTile({ label, value, valueColor, subtitle }) {
return (
{label}
{value}
{subtitle}
);
}
/* ═══════════════════════════════════════════════════════════════════
Thought stream — pill + mono message per event
═══════════════════════════════════════════════════════════════════ */
const EVENT_COLORS = {
SCAN: ST.blue,
HYPOTHESIS: ST.violet,
SIGNAL: ST.amber,
LEARN: ST.green,
VETO: ST.red,
};
function ThoughtStream({ events = [] }) {
return (
THOUGHT STREAM
LAST 8 · LIVE
{events.map((e, i) => )}
);
}
function ThoughtRow({ time, type, message, meta, fresh }) {
const c = EVENT_COLORS[type] || ST.ink3;
return (
{time}
{type}
{fresh &&
{'\u203A'}}
{message}
{meta && (
{meta}
)}
);
}
/* ═══════════════════════════════════════════════════════════════════
Top Movers
═══════════════════════════════════════════════════════════════════ */
function TopMoversCard({ movers }) {
return (
TOP MOVERS · 24H
{movers.length}
{movers.map((m, i) => )}
);
}
function MoverRow({ sym, price, chg, signal, conf, group }) {
const up = chg >= 0;
const sigColor = signal === 'LONG' ? ST.green : signal === 'SHORT' ? ST.red : null;
return (
{/* row 1 — coin + price + change */}
{sym}
{group && {ST_GROUP[group].label}}
${ST_fmt.price(price)}
{ST_fmt.pct(chg, 2)}
{/* row 2 — Shadow signal or empty state */}
{signal ? (
{signal} · PAPER
conf {conf}
) : (
NO SIGNAL · WATCHING
)}
);
}
/* ═══════════════════════════════════════════════════════════════════
Market context mini
═══════════════════════════════════════════════════════════════════ */
function MarketContextMini({ intel }) {
/* Live values from /market/intel (normalized in app.jsx), with safe fallback */
const fg = Number(intel && intel.fear_greed != null ? intel.fear_greed : 25) || 25;
const btcd = Number(intel && intel.btc_dominance != null ? intel.btc_dominance : 58.2) || 58.2;
const mr = Number(intel && intel.macro_risk != null ? intel.macro_risk : 42) || 42;
const eqUp = (intel && intel.equity_outlook) ? String(intel.equity_outlook).toUpperCase().startsWith('BULL') : true;
const regime = (intel && intel.regime) ? String(intel.regime) : 'BEARISH';
const fgC = fg <= 25 ? ST.red : fg <= 45 ? ST.amber : fg <= 55 ? ST.ink3 : ST.green;
const fgL = fg <= 25 ? 'EXTREME FEAR' : fg <= 45 ? 'FEAR' : fg <= 55 ? 'NEUTRAL' : fg <= 75 ? 'GREED' : 'EXTREME GREED';
const mrC = mr >= 60 ? ST.red : mr >= 40 ? ST.amber : ST.green;
const mrL = mr >= 60 ? 'HIGH' : mr >= 40 ? 'ELEVATED' : 'LOW';
const tiles = [
{ label: 'FEAR & GREED', valueStr: String(Math.round(fg)), tag: fgL, c: fgC, bar: fg, max: 100 },
{ label: 'BTC.D', valueStr: `${btcd.toFixed(1)}%`, tag: btcd > 56 ? 'RISING' : 'FALLING', c: ST.amber, bar: btcd, max: 100 },
{ label: 'MACRO RISK', valueStr: String(Math.round(mr)), tag: mrL, c: mrC, bar: mr, max: 100 },
{ label: 'EQUITY', valueStr: eqUp ? 'BULLISH' : 'BEARISH', tag: 'vs crypto', c: eqUp ? ST.green : ST.red, bar: eqUp ? 72 : 30, max: 100 },
];
const fgWord = fg <= 25 ? 'extreme fear' : fg <= 45 ? 'fear' : fg <= 55 ? 'neutral sentiment' : 'greed';
const domWord = btcd > 56 ? 'rising' : 'easing';
const reg = regime.toUpperCase();
return (
MARKET CONTEXT
shadow.thesis
{tiles.map((t, i) => )}
BTC dominance {domWord} into {fgWord} · alt risk{' '}
{mr >= 40 ? 'elevated' : 'contained'}. Shadow holds the{' '}
{reg.replace('_', ' ')} regime
and gates momentum entries on a BTC structure flip.
);
}
function ContextTile({ label, valueStr, tag, c, bar, max }) {
return (
);
}
window.ShadowPanel = ShadowPanel;