Files
trading-daemon/dashboard/public/index.html
Melchior Reimers dae3e9eb29
All checks were successful
Deployment / deploy-docker (push) Successful in 15s
feat: enable unlimited historical data fetching for EIX
2026-01-23 19:19:52 +01:00

512 lines
24 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;
text-xs font-bold text-slate-500 uppercase tracking-widest mb-3;
}
.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);
}
</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">
<div class="config-sidebar p-8 space-y-8 overflow-y-auto bg-slate-900/30">
<h3 class="text-sky-400 font-bold text-xs uppercase tracking-tighter mb-4">Report Configuration</h3>
<!-- Time Selection -->
<div>
<label class="field-label">Time Period</label>
<select id="timeRangePreset" class="input-glass mb-2" onchange="handlePresetChange()">
<option value="1">Today</option>
<option value="7" selected>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 Date 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">
<input type="date" id="dateTo" class="input-glass text-xs p-2">
</div>
</div>
<!-- Dimension Selection (X) -->
<div>
<label class="field-label">Group By (X-Axis)</label>
<select id="axisX" class="input-glass" onchange="updateSubGroupOptions()">
<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>
<!-- Breakdown Selection (Series) -->
<div id="subGroupContainer">
<label class="field-label">Breakdown by (Series)</label>
<select id="axisSub" class="input-glass">
<option value="">None (Single Series)</option>
<option value="exchange">Exchange</option>
<option value="continent">Continent</option>
<option value="sector">Sector</option>
</select>
</div>
<!-- Metric Selection (Y) -->
<div>
<label class="field-label">Measurement (Y-Axis)</label>
<select id="axisY" class="input-glass">
<option value="volume">Trade Volume (€)</option>
<option value="count">Number of Trades</option>
<option value="avg_price">Average Price</option>
</select>
</div>
<!-- Search & Filter -->
<div class="relative">
<label class="field-label">Filter to Companies</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-4">
<button onclick="renderAnalyticsReport()" class="btn-primary w-full">GENERATE REPORT</button>
<p class="text-[10px] text-slate-500 mt-4 text-center">Charts are automatically optimized (Line vs
Bar) based on selected grouping.</p>
</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"><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) {
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') renderAnalyticsReport(); 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');
const container = document.getElementById('subGroupContainer');
// Contextual Logic: If X is already a metadata field, don't allow it as series (too complex)
container.classList.remove('hidden');
Array.from(sub.options).forEach(opt => {
opt.disabled = (opt.value === x);
});
}
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();
// Close search
document.getElementById('isinSearch').value = '';
document.getElementById('suggestions').classList.add('hidden');
}
}
function removeFilter(isin) {
store.pinnedIsins = store.pinnedIsins.filter(p => p.isin !== isin);
updateFilterChips();
}
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('');
container.innerHTML = html;
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 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(',');
let url = `${API}/analytics?metric=${y}&group_by=${x}`;
if (sub) url += `&sub_group_by=${sub}`;
if (dates.from) url += `&date_from=${dates.from}`;
if (dates.to) url += `&date_to=${dates.to}`;
if (isins) url += `&isins=${isins}`;
try {
const res = await fetch(url).then(r => 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 (sub) {
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.8)`,
borderColor: `hsla(${hue}, 75%, 50%, 1)`,
borderWidth: 2,
fill: currentChartType === 'line'
});
});
} else {
datasets.push({
label: y,
data: labels.map(l => {
const row = data.find(r => r[0] === l);
return row ? row[1] : 0;
}),
backgroundColor: '#38bdf888',
borderColor: '#38bdf8',
borderWidth: 2,
fill: currentChartType === 'line'
});
}
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,
scales: { y: { stacked: true, grid: { color: 'rgba(255,255,255,0.05)' } }, x: { stacked: true, grid: { display: false } } },
plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } }
}
});
document.getElementById('reportTitle').innerText = `Analysis: ${y} by ${x} ${sub ? ' (Splitted by ' + sub + ')' : ''}`;
} catch (err) { console.error(err); }
}
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();
charts.continent = new Chart(contCtx, {
type: 'doughnut',
data: { labels: store.summary.map(r => r[0]), datasets: [{ data: store.summary.map(r => r[2]), backgroundColor: ['#38bdf8', '#fbbf24', '#f43f5e', '#10b981', '#8b5cf6'], borderWidth: 0 }] },
options: { responsive: true, maintainAspectRatio: false, cutout: '75%', plugins: { legend: { position: 'right', labels: { color: '#94a3b8' } } } }
});
}
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="addFilter('${r[0]}', '${r[1]}'); showView('analytics');">
<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 filterMetadata(q) {
const rows = document.querySelectorAll('#metadataRows tr');
rows.forEach(r => r.style.display = r.innerText.toLowerCase().includes(q.toLowerCase()) ? '' : 'none');
}
window.onload = () => { fetchData(); setInterval(fetchData, 30000); };
</script>
</body>
</html>