← Назад<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Options Screener Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
background: '#0B0E14',
surface: '#1A1E29',
surfaceHover: '#232836',
border: '#2C3245',
primary: '#3B82F6',
success: '#10B981',
danger: '#EF4444',
warning: '#F59E0B',
textMain: '#F8FAFC',
textMuted: '#94A3B8'
}
}
}
}
</script>
<style>
body {
background-color: theme('colors.background');
color: theme('colors.textMain');
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.glass-card {
background: theme('colors.surface');
border: 1px solid theme('colors.border');
}
.glass-card:hover {
border-color: theme('colors.primary');
}
</style>
</head>
<body class="min-h-screen text-textMain antialiased overflow-x-hidden">
<!-- Header -->
<header class="border-b border-border bg-background sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex flex-col sm:flex-row justify-between items-center gap-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-primary/20 flex items-center justify-center text-primary font-bold shadow-[0_0_15px_rgba(59,130,246,0.5)]">
Ω
</div>
<h1 class="text-xl font-bold tracking-tight">Options Screener</h1>
</div>
<div id="header-stats" class="flex gap-6 text-sm">
<div class="flex items-center gap-2">
<span class="text-textMuted">BTC:</span>
<span id="btc-price" class="font-mono font-semibold text-textMain animate-pulse">Loading...</span>
</div>
<div class="flex items-center gap-2">
<span class="text-textMuted">ETH:</span>
<span id="eth-price" class="font-mono font-semibold text-textMain animate-pulse">Loading...</span>
</div>
</div>
<div class="text-xs text-textMuted flex items-center gap-2">
<span class="relative flex h-2 w-2">
<span id="live-ping" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75 hidden"></span>
<span id="live-dot" class="relative inline-flex rounded-full h-2 w-2 bg-textMuted"></span>
</span>
<span id="last-update">Connecting...</span>
</div>
</div>
</header>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-6 flex justify-between items-end">
<div>
<h2 class="text-2xl font-bold mb-1">Trading Signals</h2>
<p class="text-textMuted text-sm">Real-time analysis from Unusual Volume, Skew, Gamma, and Strategies</p>
</div>
<div class="text-sm font-medium text-textMuted">
<span id="signal-count" class="text-white font-bold">0</span> signals found
</div>
</div>
<div id="error-container" class="hidden bg-danger/10 border border-danger/30 text-danger px-4 py-3 rounded-lg mb-6">
<p id="error-message"></p>
</div>
<!-- Signals Grid -->
<div id="signals-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<p class="text-textMuted animate-pulse col-span-full">Loading live dashboard data...</p>
</div>
</main>
<script>
const CONFIG = { API_URL: '/api/dashboard' };
const formatCurrency = (val) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(val);
const formatDate = (isoStr) => new Date(isoStr).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute:'2-digit', second:'2-digit' });
const getColors = (signal) => {
let bg = 'bg-surfaceHover', text = 'text-textMuted';
if(signal.direction.includes('BULL')) { text = 'text-success'; bg = 'bg-success/10 border-success/20'; }
else if(signal.direction.includes('BEAR')) { text = 'text-danger'; bg = 'bg-danger/10 border-danger/20'; }
let sevColor = 'text-textMuted bg-surfaceHover';
if(signal.severity === 'EXTREME') sevColor = 'text-white bg-danger shadow-[0_0_8px_rgba(239,68,68,0.6)]';
if(signal.severity === 'HIGH') sevColor = 'text-danger bg-danger/20';
if(signal.severity === 'MEDIUM') sevColor = 'text-warning bg-warning/20';
let confBg = 'bg-textMuted';
if (signal.confidence >= 75) confBg = 'bg-success';
else if (signal.confidence >= 55) confBg = 'bg-warning';
return { dirClass: `${text} ${bg} border`, sevClass: sevColor, confBg };
};
const renderCard = (s) => {
const c = getColors(s);
return `
<div class="glass-card rounded-xl p-5 flex flex-col transition-all duration-300 relative group">
<div class="flex justify-between items-start mb-3">
<div class="flex items-center gap-2">
<span class="font-bold text-lg text-white">${s.underlying}</span>
<span class="text-[10px] px-2 py-0.5 rounded-full uppercase tracking-wider font-semibold ${c.dirClass}">${s.direction.replace(/_/g,' ')}</span>
</div>
${s.severity ? `<span class="text-[10px] uppercase font-bold px-2 py-1 rounded tracking-wider ${c.sevClass}">${s.severity}</span>` : ''}
</div>
<h3 class="font-semibold text-blue-400 text-sm mb-1">${s.strategy}</h3>
<p class="text-white text-sm leading-snug mb-3 font-medium">${s.description || 'No description provided'}</p>
<p class="text-[12px] text-textMuted flex-grow mb-5 leading-relaxed">${s.rationale}</p>
<div class="mt-auto">
<div class="flex justify-between text-xs mb-1">
<span class="text-textMuted font-medium">Confidence Match</span>
<span class="text-white font-bold">${s.confidence}%</span>
</div>
<div class="w-full bg-surfaceHover/80 rounded-full h-1.5 overflow-hidden">
<div class="h-1.5 rounded-full ${c.confBg}" style="width: ${Math.max(5, s.confidence)}%"></div>
</div>
</div>
</div>
`;
};
const fetchAPI = async () => {
try {
const res = await fetch(CONFIG.API_URL);
const data = await res.json();
if(!data.success) throw new Error(data.error || 'Server returned false');
document.getElementById('btc-price').textContent = formatCurrency(data.spotPrices.BTC);
document.getElementById('eth-price').textContent = formatCurrency(data.spotPrices.ETH);
const bPriceClasses = document.getElementById('btc-price').classList;
bPriceClasses.remove('animate-pulse');
document.getElementById('eth-price').classList.remove('animate-pulse');
document.getElementById('last-update').textContent = 'Live \u2022 Local';
document.getElementById('live-dot').classList.replace('bg-textMuted', 'bg-success');
document.getElementById('live-ping').classList.remove('hidden');
document.getElementById('signal-count').textContent = data.signals ? data.signals.length : 0;
const grid = document.getElementById('signals-grid');
grid.innerHTML = data.signals && data.signals.length
? data.signals.map(renderCard).join('')
: '<div class="col-span-full py-10 text-center text-textMuted border border-border border-dashed rounded-xl">No active signals found.</div>';
document.getElementById('error-container').classList.add('hidden');
} catch (err) {
document.getElementById('error-message').textContent = err.message;
document.getElementById('error-container').classList.remove('hidden');
document.getElementById('live-dot').classList.replace('bg-success', 'bg-danger');
}
};
fetchAPI();
setInterval(fetchAPI, 10000);
</script>
</body>
</html>