feat: Add dashboard charts with interactivity and basic authentication support

This commit is contained in:
2026-06-01 12:28:02 +02:00
parent cde181f343
commit 24f2b2ed88
8 changed files with 323 additions and 26 deletions
+112
View File
@@ -5,6 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ title }}</title>
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
<style>
body {
margin: 0;
@@ -124,6 +126,17 @@
gap: 12px;
flex-wrap: wrap;
}
.chart-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.chart-canvas {
width: 100%;
min-height: 260px;
}
</style>
</head>
<body>
@@ -176,7 +189,106 @@
{% include "partials/controls.html" %}
</section>
<section
id="charts-shell"
hx-get="{{ charts_endpoint }}"
hx-target="this"
hx-trigger="load, every 30s"
hx-swap="outerHTML"
>
{% include "partials/charts.html" %}
</section>
<script>
window.arbitradeRenderCharts = (payload) => {
const chartHost = document.getElementById("opportunity-chart");
if (!chartHost || typeof Chart === "undefined") {
return;
}
const existing = Chart.getChart(chartHost);
if (existing) {
existing.destroy();
}
const data = JSON.parse(payload);
if (!data.has_chart_data) {
return;
}
new Chart(chartHost, {
type: "line",
data: {
labels: data.labels,
datasets: [
{
label: "Net %",
data: data.net_pct_values,
borderColor: "#2d6cdf",
backgroundColor: "rgba(45, 108, 223, 0.18)",
tension: 0.3,
fill: true,
yAxisID: "y",
},
{
label: "Est profit USD",
data: data.est_profit_values,
borderColor: "#52c41a",
backgroundColor: "rgba(82, 196, 26, 0.12)",
tension: 0.3,
fill: false,
yAxisID: "y1",
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: "index",
intersect: false,
},
plugins: {
legend: {
labels: {
color: "#e5eefb",
},
},
},
scales: {
x: {
ticks: {
color: "#9fb2d0",
maxRotation: 0,
autoSkip: true,
},
grid: {
color: "rgba(255, 255, 255, 0.06)",
},
},
y: {
position: "left",
ticks: {
color: "#9fb2d0",
},
grid: {
color: "rgba(255, 255, 255, 0.06)",
},
},
y1: {
position: "right",
ticks: {
color: "#9fb2d0",
},
grid: {
drawOnChartArea: false,
},
},
},
},
});
};
const stream = new EventSource("{{ stream_endpoint }}");
stream.addEventListener("metrics", (event) => {
const panel = document.getElementById("metrics-panel");
+37
View File
@@ -0,0 +1,37 @@
<div
id="charts-panel"
class="panel"
style="margin-top: 16px"
x-data="{ expanded: true }"
>
<div class="chart-head">
<div>
<div class="label">Opportunity Trend</div>
<div class="meta">Recent opportunities from DuckDB. Updated {{ generated_at }}</div>
</div>
<button type="button" class="button secondary" x-on:click="expanded = !expanded">
<span x-text="expanded ? 'Hide chart' : 'Show chart'"></span>
</button>
</div>
<div x-show="expanded" x-transition style="margin-top: 16px">
<div class="card" style="padding: 12px">
{% if has_chart_data %}
<canvas id="opportunity-chart" class="chart-canvas"></canvas>
<script>
window.arbitradeRenderCharts(
{{ {
"has_chart_data": has_chart_data,
"labels": labels,
"net_pct_values": net_pct_values,
"est_profit_values": est_profit_values,
"cycles": cycles,
} | tojson }}
);
</script>
{% else %}
<div class="meta">No opportunity data yet.</div>
{% endif %}
</div>
</div>
</div>