Files
trading-daemon/dashboard/public/index.html
Melchior Reimers 15065336c3
All checks were successful
Deployment / deploy-docker (push) Successful in 17s
fixing dashboard
2026-01-25 14:35:49 +01:00

802 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trading Intelligence Hub</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Outfit', sans-serif;
background-color: #0b1120;
color: #e2e8f0;
}
.glass {
background: rgba(30, 41, 59, 0.4);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
}
.active-nav {
background: rgba(56, 189, 248, 0.1);
color: #38bdf8;
border-right: 3px solid #38bdf8;
}
canvas {
max-width: 100%;
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-thumb {
background: #1e293b;
border-radius: 10px;
}
.config-sidebar {
width: 340px;
border-right: 1px solid rgba(255, 255, 255, 0.05);
flex-shrink: 0;
}
.btn-primary {
background: #38bdf8;
color: #0b1120;
padding: 10px 20px;
border-radius: 10px;
font-weight: 700;
transition: all 0.2s;
box-shadow: 0 4px 14px 0 rgba(56, 189, 248, 0.3);
}
.btn-primary:hover {
background: #7dd3fc;
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(56, 189, 248, 0.4);
}
.suggestion-box {
position: absolute;
z-index: 50;
width: 100%;
top: 100%;
left: 0;
background: #1e293b;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
margin-top: 4px;
max-height: 200px;
overflow-y: auto;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
}
.suggestion-item {
padding: 10px 16px;
cursor: pointer;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.suggestion-item:hover {
background: rgba(56, 189, 248, 0.1);
color: #38bdf8;
}
.field-label {
display: block;
font-size: 0.75rem;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.75rem;
}
.input-glass {
width: 100%;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 10px;
border-radius: 10px;
outline: none;
transition: all 0.2s;
}
.input-glass:focus {
border-color: #38bdf8;
background: rgba(56, 189, 248, 0.05);
}
.report-step {
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
padding: 1.5rem;
border-radius: 12px;
overflow: hidden;
max-height: 1000px;
margin-bottom: 1.5rem;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.step-active {
background: rgba(56, 189, 248, 0.05);
border: 1px solid rgba(56, 189, 248, 0.2);
opacity: 1 !important;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
}
.step-completed {
max-height: 80px;
opacity: 0.7;
cursor: pointer;
padding-top: 1rem;
padding-bottom: 1rem;
background: rgba(255, 255, 255, 0.02);
}
.step-completed:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.04);
border-color: rgba(56, 189, 248, 0.3);
}
.step-completed select,
.step-completed input,
.step-completed .relative,
.step-completed .pt-8,
.step-completed .grid {
display: none !important;
}
.step-summary {
font-size: 0.85rem;
color: #38bdf8;
font-weight: 700;
margin-top: 0.5rem;
background: rgba(56, 189, 248, 0.1);
padding: 6px 12px;
border-radius: 8px;
display: block;
width: fit-content;
border: 1px solid rgba(56, 189, 248, 0.2);
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(56, 189, 248, 0.1);
border-top: 3px solid #38bdf8;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body class="flex min-h-screen overflow-hidden">
<!-- Sidebar -->
<aside class="w-20 lg:w-64 glass m-4 mr-0 flex flex-col hidden sm:flex">
<div class="p-6 hidden lg:block">
<h1 class="text-2xl font-bold bg-gradient-to-r from-sky-400 to-blue-500 bg-clip-text text-transparent">
Antigravity</h1>
</div>
<nav class="flex-1 px-4 space-y-2 mt-4" id="sidebar">
<a href="#" onclick="showView('dashboard')" id="nav-dashboard"
class="flex items-center p-3 rounded-xl transition active-nav"><span class="mr-3">📊</span> <span
class="hidden lg:inline">Dashboard</span></a>
<a href="#" onclick="showView('analytics')" id="nav-analytics"
class="flex items-center p-3 rounded-xl transition"><span class="mr-3">📈</span> <span
class="hidden lg:inline">Report Builder</span></a>
<a href="#" onclick="showView('metadata')" id="nav-metadata"
class="flex items-center p-3 rounded-xl transition"><span class="mr-3">🏢</span> <span
class="hidden lg:inline">Companies</span></a>
</nav>
<div class="p-6 mt-auto hidden lg:block">
<div class="glass p-4 text-xs text-sky-400 border-sky-500/30">Status: <span
class="text-green-400 animate-pulse">Connected</span></div>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 flex flex-col min-w-0">
<header class="p-8 pb-4 flex justify-between items-center flex-shrink-0">
<div>
<h2 class="text-3xl font-bold" id="pageTitle">Market Overview</h2>
<div id="activeIsins" class="flex flex-wrap gap-2 mt-2"></div>
</div>
<button onclick="fetchData()" class="glass p-2 px-6 text-sky-400 font-bold hover:text-sky-200 transition">
REFRESH</button>
</header>
<!-- DASHBOARD VIEW -->
<div id="view-dashboard" class="view flex-1 p-8 pt-4 overflow-y-auto">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
<div class="glass p-8 border-l-4 border-sky-400 glow">
<p class="text-slate-500 text-xs font-bold uppercase tracking-wider mb-2">Volume (7d)</p>
<h3 id="statVolume" class="text-4xl font-bold">€0.0</h3>
</div>
<div class="glass p-8 border-l-4 border-amber-400">
<p class="text-slate-500 text-xs font-bold uppercase tracking-wider mb-2">Total Trades</p>
<h3 id="statTrades" class="text-4xl font-bold">0</h3>
</div>
<div class="glass p-8 border-l-4 border-emerald-400">
<p class="text-slate-500 text-xs font-bold uppercase tracking-wider mb-2">Assets</p>
<h3 id="statIsins" class="text-4xl font-bold">0</h3>
</div>
</div>
<div class="grid grid-cols-1 xl:grid-cols-2 gap-8">
<div class="glass p-8">
<h3 class="text-lg font-bold mb-6 text-slate-300">Live Price Feed</h3>
<div class="h-80"><canvas id="priceChart"></canvas></div>
</div>
<div class="glass p-8">
<h3 class="text-lg font-bold mb-6 text-slate-300">Regional Distribution</h3>
<div class="h-80"><canvas id="continentChart"></canvas></div>
</div>
</div>
</div>
<!-- REPORT BUILDER VIEW -->
<div id="view-analytics" class="view hidden flex-1 flex overflow-hidden">
<!-- Configuration Sidebar -->
<div class="config-sidebar p-8 space-y-6 overflow-y-auto bg-slate-900/30" id="reportConfig">
<h3 class="text-sky-400 font-bold text-xs uppercase tracking-tighter mb-4">Step-by-Step Configuration
</h3>
<!-- Step 1: Time Range -->
<div class="report-step step-active" id="step1">
<label class="field-label">1. Choose Analysis Period</label>
<select id="timeRangePreset" class="input-glass" onchange="updateUrlParams(); proceedToStep(2)">
<option value="" disabled selected>Select... </option>
<option value="1">Today</option>
<option value="7">Last 7 Days</option>
<option value="30">Last 30 Days</option>
<option value="ytd">Year to Date (YTD)</option>
<option value="year">Full Year 2026</option>
<option value="custom">Custom Range...</option>
</select>
<div id="customDates" class="hidden grid grid-cols-2 gap-2 mt-2">
<input type="date" id="dateFrom" class="input-glass text-xs p-2"
onchange="updateUrlParams(); checkCustomDates()">
<input type="date" id="dateTo" class="input-glass text-xs p-2"
onchange="updateUrlParams(); checkCustomDates()">
</div>
<div id="summary-step1" class="step-summary"></div>
</div>
<!-- Step 2: X-Axis -->
<div class="report-step hidden opacity-50" id="step2">
<label class="field-label">2. Primary Grouping (X-Axis)</label>
<select id="axisX" class="input-glass"
onchange="updateUrlParams(); proceedToStep(3); updateSubGroupOptions();">
<option value="" disabled selected>Select... </option>
<option value="day">Time (Daily)</option>
<option value="month">Time (Monthly)</option>
<option value="exchange">Exchange Origin</option>
<option value="continent">Region/Continent</option>
<option value="sector">Industry Sector</option>
<option value="isin">Specific ISIN</option>
</select>
<div id="summary-step2" class="step-summary"></div>
</div>
<!-- Step 3: Breakdown -->
<div class="report-step hidden opacity-50" id="step3">
<label class="field-label">3. Secondary Breakdown (Series)</label>
<select id="axisSub" class="input-glass" onchange="updateUrlParams(); proceedToStep(4)">
<option value="">None (Unified)</option>
<option value="isin">By Company / ISIN</option>
<option value="exchange">By Exchange</option>
<option value="continent">By Continent</option>
<option value="sector">By Sector</option>
</select>
<div id="summary-step3" class="step-summary"></div>
</div>
<!-- Step 4: Y-Axis -->
<div class="report-step hidden opacity-50" id="step4">
<label class="field-label">4. Select Metric (Y-Axis)</label>
<select id="axisY" class="input-glass" onchange="updateUrlParams(); proceedToStep(5)">
<option value="" disabled selected>Select... </option>
<option value="volume">Trade Volume (€)</option>
<option value="count">Number of Trades</option>
<option value="avg_price">Average Performance</option>
</select>
<div id="summary-step4" class="step-summary"></div>
</div>
<!-- Step 5: Filters -->
<div class="report-step hidden opacity-50" id="step5">
<div class="relative">
<label class="field-label">5. Optional Company Filters</label>
<input type="text" id="isinSearch" autocomplete="off" placeholder="Search ISIN or Name..."
class="input-glass" onkeyup="handleSearch(event)">
<div id="suggestions" class="suggestion-box hidden"></div>
<div id="filterChips" class="flex flex-wrap gap-2 mt-4"></div>
</div>
<div class="pt-8 space-y-3">
<button id="btnGenerate" onclick="renderAnalyticsReport()"
class="btn-primary w-full shadow-sky-500/20">GENERATE REPORT</button>
<button onclick="resetReportConfig()"
class="w-full text-xs text-slate-500 hover:text-white transition">Reset
Configuration</button>
</div>
</div>
</div>
<div class="flex-1 p-8 overflow-hidden flex flex-col">
<div class="glass p-10 h-full flex flex-col shadow-2xl relative overflow-hidden">
<div class="flex justify-between items-center mb-10 z-10">
<h3 class="text-2xl font-bold" id="reportTitle">Report: Market Activity</h3>
<div class="flex glass p-1 rounded-lg">
<button class="p-2 px-4 text-xs font-bold rounded-md hover:bg-white/5"
onclick="setChartType('line')">Line</button>
<button class="p-2 px-4 text-xs font-bold rounded-md hover:bg-white/5"
onclick="setChartType('bar')">Bar</button>
</div>
</div>
<div class="flex-1 min-h-0 z-10 relative">
<div id="chartLoader"
class="hidden absolute inset-0 flex items-center justify-center bg-[#0b1120]/50 z-20">
<div class="spinner"></div>
</div>
<div id="chartError"
class="hidden absolute inset-0 flex items-center justify-center bg-red-900/20 z-20 p-10 text-center">
<div class="glass p-6 border-red-500/30">
<p class="text-red-400 font-bold mb-2">Analysis Failed</p>
<p id="errorMsg" class="text-xs text-slate-400"></p>
</div>
</div>
<canvas id="analyticsChart"></canvas>
</div>
<!-- Subtle watermark -->
<div class="absolute bottom-8 right-8 text-white/5 text-6xl font-black pointer-events-none">
ANALYTICS</div>
</div>
</div>
</div>
<!-- COMPANIES VIEW -->
<div id="view-metadata" class="view hidden flex-1 p-8 overflow-y-auto">
<div class="glass overflow-hidden">
<div class="p-8 border-b border-white/5 flex justify-between items-center">
<h3 class="text-xl font-bold">Metadata Repository</h3>
<input type="text" onkeyup="filterMetadata(this.value)"
placeholder="Search company, ISIN, sector..."
class="glass p-3 px-6 text-sm w-96 outline-none focus:border-sky-500">
</div>
<table class="w-full text-left">
<tbody id="metadataRows"></tbody>
</table>
</div>
</div>
</main>
<script>
const API = '/api';
let store = { trades: [], metadata: [], summary: [], pinnedIsins: [] };
let charts = {};
let currentChartType = 'bar';
let searchTimeout;
function showView(viewId) {
window.activeView = viewId;
document.querySelectorAll('.view').forEach(v => v.classList.add('hidden'));
document.getElementById(`view-${viewId}`).classList.remove('hidden');
document.querySelectorAll('#sidebar a').forEach(a => a.classList.remove('active-nav'));
document.getElementById(`nav-${viewId}`).classList.add('active-nav');
const titles = { 'dashboard': 'Market Overview', 'analytics': 'Custom Report Builder', 'metadata': 'Entity Metadata' };
document.getElementById('pageTitle').innerText = titles[viewId];
if (viewId === 'analytics') {
updateUrlParams();
} else {
fetchData();
}
}
function handlePresetChange() {
const v = document.getElementById('timeRangePreset').value;
document.getElementById('customDates').classList.toggle('hidden', v !== 'custom');
}
function updateSubGroupOptions() {
const x = document.getElementById('axisX').value;
const sub = document.getElementById('axisSub');
Array.from(sub.options).forEach(opt => {
opt.disabled = (opt.value === x);
if (opt.value === x && sub.value === x) sub.value = '';
});
}
async function handleSearch(e) {
const q = e.target.value;
const sBox = document.getElementById('suggestions');
if (q.length < 2) { sBox.classList.add('hidden'); return; }
clearTimeout(searchTimeout);
searchTimeout = setTimeout(async () => {
const res = await fetch(`${API}/metadata/search?q=${q}`).then(r => r.json());
const data = res.dataset || [];
if (data.length > 0) {
sBox.innerHTML = data.map(item => `<div class="suggestion-item" onclick="addFilter('${item[0]}', '${item[1]}')">
<div class="font-bold">${item[0]}</div>
<div class="text-[10px] text-slate-500">${item[1]}</div>
</div>`).join('');
sBox.classList.remove('hidden');
} else {
sBox.classList.add('hidden');
}
}, 300);
}
function addFilter(isin, name) {
if (!store.pinnedIsins.find(p => p.isin === isin)) {
store.pinnedIsins.push({ isin, name });
updateFilterChips();
document.getElementById('isinSearch').value = '';
document.getElementById('suggestions').classList.add('hidden');
updateUrlParams();
if (window.activeView === 'analytics') proceedToStep(5);
}
}
function removeFilter(isin) {
store.pinnedIsins = store.pinnedIsins.filter(p => p.isin !== isin);
updateFilterChips();
updateUrlParams();
}
function updateFilterChips() {
const container = document.getElementById('filterChips');
const pins = document.getElementById('activeIsins');
const html = store.pinnedIsins.map(p => `
<div class="glass p-1 px-3 text-[10px] font-bold text-sky-400 border-sky-400/30 flex items-center bg-sky-400/10">
${p.isin} <span class="ml-2 cursor-pointer text-slate-500 hover:text-white" onclick="removeFilter('${p.isin}')">×</span>
</div>
`).join('');
if (container) container.innerHTML = html;
if (pins) pins.innerHTML = html;
}
function getDates() {
const preset = document.getElementById('timeRangePreset').value;
const now = new Date();
let from, to = now.toISOString().split('T')[0];
if (preset === '1') from = to;
else if (preset === '7') from = new Date(now.setDate(now.getDate() - 7)).toISOString().split('T')[0];
else if (preset === '30') from = new Date(now.setDate(now.getDate() - 30)).toISOString().split('T')[0];
else if (preset === 'ytd') from = new Date(now.getFullYear(), 0, 1).toISOString().split('T')[0];
else if (preset === 'year') { from = '2026-01-01'; to = '2026-12-31'; }
else if (preset === 'custom') { from = document.getElementById('dateFrom').value; to = document.getElementById('dateTo').value; }
return { from, to };
}
async function fetchData() {
try {
const [t, m, s] = await Promise.all([
fetch(`${API}/trades?days=7`).then(r => r.json()),
fetch(`${API}/metadata`).then(r => r.json()),
fetch(`${API}/summary`).then(r => r.json())
]);
store = { ...store, trades: t.dataset || [], metadata: m.dataset || [], summary: s.dataset || [] };
updateDashboard();
} catch (err) { console.error(err); }
}
function updateDashboard() {
let vol = store.trades.reduce((acc, r) => acc + (parseFloat(r[4]) * parseFloat(r[5] || 0)), 0);
document.getElementById('statVolume').innerText = vol >= 1e6 ? `${(vol / 1e6).toFixed(1)}M` : `${(vol / 1e3).toFixed(0)}k`;
document.getElementById('statTrades').innerText = store.trades.length.toLocaleString();
document.getElementById('statIsins').innerText = store.metadata.length.toLocaleString();
renderDashboardCharts();
fillMetadataTable();
}
function setChartType(type) { currentChartType = type; renderAnalyticsReport(); }
async function renderAnalyticsReport() {
const btn = document.getElementById('btnGenerate');
const loader = document.getElementById('chartLoader');
const errBox = document.getElementById('chartError');
const dates = getDates();
const x = document.getElementById('axisX').value;
const sub = document.getElementById('axisSub').value;
const y = document.getElementById('axisY').value;
const isins = store.pinnedIsins.map(p => p.isin).join(',');
if (!dates.from || !x || !y) return;
btn.innerText = 'GENERATING...';
btn.disabled = true;
loader.classList.remove('hidden');
errBox.classList.add('hidden');
let url = `${API}/analytics?metric=${y}&group_by=${x}`;
if (dates.from) url += `&date_from=${dates.from}`;
if (dates.to) url += `&date_to=${dates.to}`;
if (isins) url += `&isins=${isins}`;
try {
// If multiple ISINs are selected but no breakdown is chosen,
// we automatically breakdown by ISIN to give them different colors.
let effectiveSub = sub;
if (!sub && store.pinnedIsins.length > 1 && x !== 'isin') {
effectiveSub = 'isin';
url += `&sub_group_by=isin`;
} else if (sub) {
url += `&sub_group_by=${sub}`;
}
const res = await fetch(url).then(r => {
if (!r.ok) throw new Error(`Server Error: ${r.status}`);
return r.json();
});
const data = res.dataset || [];
const ctx = document.getElementById('analyticsChart').getContext('2d');
let labels = [...new Set(data.map(r => r[0]))];
if (x === 'day' || x === 'month') labels.sort((a, b) => new Date(a) - new Date(b));
let datasets = [];
if (effectiveSub) {
let seriesNames = [...new Set(data.map(r => r[1]))];
seriesNames.forEach((name, idx) => {
const hue = (idx * 137.5) % 360;
datasets.push({
label: name,
data: labels.map(l => {
const row = data.find(r => r[0] === l && r[1] === name);
return row ? row[2] : 0;
}),
backgroundColor: `hsla(${hue}, 75%, 50%, 0.7)`,
borderColor: `hsla(${hue}, 75%, 50%, 1)`,
borderWidth: 2,
fill: currentChartType === 'line',
tension: 0.3
});
});
} else {
datasets.push({
label: y.toUpperCase(),
data: labels.map(l => {
const row = data.find(r => r[0] === l);
return row ? row[1] : 0;
}),
backgroundColor: '#38bdf866',
borderColor: '#38bdf8',
borderWidth: 2,
fill: currentChartType === 'line',
tension: 0.3
});
}
if (charts.analytics) charts.analytics.destroy();
charts.analytics = new Chart(ctx, {
type: currentChartType,
data: { labels: labels.map(l => (x === 'day' || x === 'month') ? new Date(l).toLocaleDateString() : l), datasets },
options: {
responsive: true, maintainAspectRatio: false,
animation: { duration: 800, easing: 'easeOutQuart' },
scales: {
y: {
stacked: (currentChartType === 'bar'),
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#64748b' }
},
x: {
stacked: (currentChartType === 'bar'),
grid: { display: false },
ticks: { color: '#64748b' }
}
},
plugins: {
legend: {
display: true,
position: 'bottom',
labels: { color: '#94a3b8', boxWidth: 12, usePointStyle: true, padding: 20 }
},
tooltip: {
backgroundColor: '#1e293b',
titleColor: '#38bdf8',
bodyColor: '#e2e8f0',
borderColor: 'rgba(255,255,255,0.1)',
borderWidth: 1
}
}
}
});
document.getElementById('reportTitle').innerText = `Market Analytics: ${y.toUpperCase()} vs ${x.toUpperCase()}`;
} catch (err) {
console.error(err);
document.getElementById('errorMsg').innerText = err.message;
errBox.classList.remove('hidden');
} finally {
btn.innerText = 'GENERATE REPORT';
btn.disabled = false;
loader.classList.add('hidden');
}
}
function renderDashboardCharts() {
const trendCtx = document.getElementById('priceChart').getContext('2d');
const samples = [...store.trades].sort((a, b) => new Date(a[3]) - new Date(b[3])).slice(-100);
if (charts.price) charts.price.destroy();
charts.price = new Chart(trendCtx, {
type: 'line',
data: { labels: samples.map(r => new Date(r[3]).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })), datasets: [{ data: samples.map(r => r[4]), borderColor: '#38bdf8', borderWidth: 2, tension: 0.4, pointRadius: 0 }] },
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { grid: { color: 'rgba(255,255,255,0.05)' } } } }
});
const contCtx = document.getElementById('continentChart').getContext('2d');
if (charts.continent) charts.continent.destroy();
const labels = store.summary.map(r => r[0]);
const baseColors = ['#38bdf8', '#f43f5e', '#10b981', '#fbbf24', '#8b5cf6', '#f97316', '#ec4899', '#6366f1'];
charts.continent = new Chart(contCtx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: store.summary.map(r => r[2]),
backgroundColor: labels.map((l, i) => l.toLowerCase() === 'unknown' ? '#000000' : baseColors[i % baseColors.length]),
borderWidth: 0
}]
},
options: { responsive: true, maintainAspectRatio: false, cutout: '75%', plugins: { legend: { position: 'right', labels: { color: '#94a3b8' } } } }
});
}
function proceedToStep(n) {
document.querySelectorAll('.report-step').forEach((s, idx) => {
const stepNum = idx + 1;
// Active step is the one currently being configured
s.classList.toggle('step-active', stepNum === n);
// Completed steps are those before the current one, but NOT if they are active
s.classList.toggle('step-completed', stepNum < n && stepNum !== n);
// Show all steps but fade future ones slightly
s.classList.remove('hidden');
if (stepNum > n) {
s.classList.add('opacity-50');
} else {
s.classList.remove('opacity-50');
}
const summary = document.getElementById(`summary-step${stepNum}`);
if (summary) {
// Only show summary for non-active steps that have a selection
if (stepNum !== n) {
const input = s.querySelector('select, input');
let val = input ? (input.options && input.selectedIndex >= 0 ? input.options[input.selectedIndex].text : input.value) : '';
if (val && val !== 'Select...') {
summary.innerText = val;
summary.classList.remove('hidden');
} else {
summary.classList.add('hidden');
}
// Allow clicking headers of any step to go back/forward
s.onclick = () => proceedToStep(stepNum);
} else {
summary.classList.add('hidden');
s.onclick = null;
}
}
});
updateUrlParams();
}
function checkCustomDates() {
const from = document.getElementById('dateFrom').value;
const to = document.getElementById('dateTo').value;
if (from && to) proceedToStep(2);
}
function updateUrlParams() {
const params = new URLSearchParams(window.location.search);
const time = document.getElementById('timeRangePreset').value;
const x = document.getElementById('axisX').value;
const sub = document.getElementById('axisSub') ? document.getElementById('axisSub').value : '';
const y = document.getElementById('axisY').value;
const isins = store.pinnedIsins.map(p => p.isin).join(',');
if (time) params.set('time', time); else params.delete('time');
if (x) params.set('x', x); else params.delete('x');
if (sub) params.set('sub', sub); else params.delete('sub');
if (y) params.set('y', y); else params.delete('y');
if (isins) params.set('isins', isins); else params.delete('isins');
if (window.activeView) params.set('view', window.activeView);
// Important: Use hash only if it exists
const cleanUrl = window.location.pathname + '?' + params.toString();
window.history.replaceState({ selection: params.toString() }, '', cleanUrl);
}
function syncStateFromUrl() {
const params = new URLSearchParams(window.location.search);
const time = params.get('time');
const x = params.get('x');
const sub = params.get('sub');
const y = params.get('y');
const isins = params.get('isins');
const view = params.get('view');
if (view) showView(view);
if (time) { document.getElementById('timeRangePreset').value = time; proceedToStep(2); }
if (x) { document.getElementById('axisX').value = x; updateSubGroupOptions(); proceedToStep(3); }
if (sub) { document.getElementById('axisSub').value = sub; proceedToStep(4); }
if (y) { document.getElementById('axisY').value = y; proceedToStep(5); }
if (isins) {
setTimeout(() => {
isins.split(',').forEach(id => {
const ent = store.metadata.find(m => m[0] === id);
addFilter(id, ent ? ent[1] : id);
});
if (x && y && time) renderAnalyticsReport();
}, 800);
}
}
function resetReportConfig() {
document.querySelectorAll('.report-step').forEach((s, i) => { if (i > 0) s.classList.add('hidden', 'opacity-50'); else s.classList.add('step-active'); });
document.getElementById('timeRangePreset').value = '';
document.getElementById('axisX').value = '';
document.getElementById('axisSub').value = '';
document.getElementById('axisY').value = '';
store.pinnedIsins = [];
updateFilterChips();
updateUrlParams();
}
function fillMetadataTable() {
const tbody = document.getElementById('metadataRows');
tbody.innerHTML = store.metadata.map(r => `
<tr class="hover:bg-white/5 transition border-b border-white/5 cursor-pointer" onclick="deepLinkToAnalytics('${r[0]}', '${r[1]}')">
<td class="p-5 px-8 font-mono text-sky-400 font-bold">${r[0]}</td>
<td class="p-5 font-semibold text-slate-200">${r[1]}</td>
<td class="p-5 text-slate-500">${r[2]}</td>
<td class="p-5"><span class="bg-white/5 px-2 py-1 rounded text-[10px] text-slate-400 font-bold uppercase">${r[4] || 'UNKNOWN'}</span></td>
</tr>
`).join('');
}
function deepLinkToAnalytics(isin, name) {
resetReportConfig();
document.getElementById('timeRangePreset').value = '30';
document.getElementById('axisX').value = 'day';
document.getElementById('axisY').value = 'volume';
store.pinnedIsins = [{ isin, name }];
updateFilterChips();
showView('analytics');
for (let i = 1; i <= 5; i++) proceedToStep(i);
renderAnalyticsReport();
}
function filterMetadata(q) {
const rows = document.querySelectorAll('#metadataRows tr');
rows.forEach(r => r.style.display = r.innerText.toLowerCase().includes(q.toLowerCase()) ? '' : 'none');
}
window.onload = async () => { await fetchData(); syncStateFromUrl(); setInterval(fetchData, 30000); };
</script>
</body>
</html>