Skip to content

Commit 15298bc

Browse files
committed
Fix:A bunch of tiny improvements here and there
1 parent c63acb9 commit 15298bc

File tree

6 files changed

+403
-226
lines changed

6 files changed

+403
-226
lines changed

Cargo.lock

Lines changed: 52 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ opentelemetry-otlp = "0.28.0"
4444
tracing-opentelemetry = "0.29.0"
4545
bincode = "1.3.3"
4646
opentelemetry_sdk = { version = "0.28.0", features = ["experimental_async_runtime"] }
47+
actix-files = "0.6.6"

src/dashboard/dashboard.html

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
<!-- src/dashboard/dashboard.html -->
2+
<!DOCTYPE html>
3+
<html lang="en">
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>TimeFusion Dashboard</title>
8+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
9+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
10+
<script src="https://cdn.jsdelivr.net/npm/tippy.js@6/dist/tippy-bundle.umd.min.js"></script>
11+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tippy.js@6/themes/light.css">
12+
</head>
13+
<body>
14+
<div class="sidebar">
15+
<h2>TimeFusion</h2>
16+
<nav>
17+
<a href="#" class="active">Dashboard</a>
18+
<a href="/ingest">Ingest</a>
19+
<a href="/data">Data</a>
20+
<button id="theme-toggle">Toggle Theme</button>
21+
</nav>
22+
</div>
23+
<div class="main-content">
24+
<header>
25+
<h1>Dashboard</h1>
26+
<p>Real-time application insights</p>
27+
<button id="refresh-btn">Refresh</button>
28+
</header>
29+
<main>
30+
<section class="stats-grid">
31+
<div class="stat-card" data-tippy-content="Application uptime in seconds">
32+
<h2>Uptime (s)</h2>
33+
<p id="uptime">{{uptime}}</p>
34+
</div>
35+
<div class="stat-card" data-tippy-content="Current ingestion queue size">
36+
<h2>Queue Size</h2>
37+
<p id="queue_size">{{queue_size}}</p>
38+
</div>
39+
<div class="stat-card" data-tippy-content="Database health status">
40+
<h2>DB Status</h2>
41+
<p id="db_status" class="{{db_status}}">{{db_status}}</p>
42+
</div>
43+
<div class="stat-card" data-tippy-content="Ingestion rate (records/sec)">
44+
<h2>Ingestion Rate</h2>
45+
<p id="ingestion_rate">{{ingestion_rate}}</p>
46+
</div>
47+
<div class="stat-card" data-tippy-content="Error rate (errors/sec)">
48+
<h2>Error Rate</h2>
49+
<p id="error_rate">{{error_rate}}</p>
50+
</div>
51+
<div class="stat-card" data-tippy-content="Total records in database">
52+
<h2>Total Records</h2>
53+
<p id="total_records">{{total_records}}</p>
54+
</div>
55+
<div class="stat-card" data-tippy-content="Average latency in milliseconds">
56+
<h2>Avg Latency (ms)</h2>
57+
<p id="avg_latency">{{avg_latency}}</p>
58+
</div>
59+
</section>
60+
<section class="charts-grid">
61+
<div class="chart-container">
62+
<h2>Request Trends (Last Hour)</h2>
63+
<canvas id="trendsChart"></canvas>
64+
</div>
65+
</section>
66+
<section class="tables-grid">
67+
<div class="table-container">
68+
<h2>Recent Ingestion Statuses</h2>
69+
<input type="text" id="status-filter" placeholder="Filter by ID or Status">
70+
<table>
71+
<thead>
72+
<tr>
73+
<th>ID</th>
74+
<th>Status</th>
75+
</tr>
76+
</thead>
77+
<tbody id="status-table"></tbody>
78+
</table>
79+
<div class="pagination" id="status-pagination"></div>
80+
</div>
81+
<div class="table-container">
82+
<h2>Recent Records</h2>
83+
<input type="text" id="records-filter" placeholder="Filter by Project ID or Timestamp">
84+
<table>
85+
<thead>
86+
<tr>
87+
<th>Project ID</th>
88+
<th>Record ID</th>
89+
<th>Timestamp</th>
90+
<th>Latency (ms)</th>
91+
</tr>
92+
</thead>
93+
<tbody id="records-table"></tbody>
94+
</table>
95+
<div class="pagination" id="records-pagination"></div>
96+
</div>
97+
</section>
98+
<section class="logs-container">
99+
<h2>Real-Time Logs</h2>
100+
<pre id="logs">Connecting to log stream...</pre>
101+
</section>
102+
</main>
103+
<footer>
104+
<p>© 2025 TimeFusion</p>
105+
</footer>
106+
</div>
107+
108+
<style>
109+
:root {
110+
--primary: #3498db;
111+
--secondary: #2980b9;
112+
--text: #2c3e50;
113+
--bg: #f5f7fa;
114+
--card-bg: #ffffff;
115+
--shadow: rgba(0, 0, 0, 0.1);
116+
--success: #27ae60;
117+
--error: #e74c3c;
118+
}
119+
[data-theme="dark"] {
120+
--primary: #5dade2;
121+
--secondary: #4e91c6;
122+
--text: #ecf0f1;
123+
--bg: #2c3e50;
124+
--card-bg: #34495e;
125+
--shadow: rgba(0, 0, 0, 0.3);
126+
}
127+
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Inter', sans-serif; }
128+
body { background: var(--bg); color: var(--text); display: flex; transition: all 0.3s; }
129+
.sidebar { width: 250px; background: var(--secondary); color: white; padding: 2rem 1rem; height: 100vh; position: fixed; }
130+
.sidebar h2 { font-size: 1.8rem; font-weight: 700; margin-bottom: 2rem; }
131+
.sidebar nav a, .sidebar nav button { display: block; color: white; padding: 0.75rem 1rem; margin: 0.5rem 0; border-radius: 8px; text-decoration: none; background: none; border: none; cursor: pointer; transition: background 0.3s; }
132+
.sidebar nav a.active, .sidebar nav a:hover, .sidebar nav button:hover { background: var(--primary); }
133+
.main-content { margin-left: 250px; flex-grow: 1; padding: 2rem; }
134+
header { background: linear-gradient(135deg, var(--primary), var(--secondary)); color: white; padding: 2rem; border-radius: 15px; text-align: center; margin-bottom: 2rem; box-shadow: 0 4px 12px var(--shadow); }
135+
header h1 { font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem; }
136+
#refresh-btn { position: absolute; right: 2rem; top: 50%; transform: translateY(-50%); background: #ffffff33; color: white; border: none; padding: 0.5rem 1rem; border-radius: 5px; cursor: pointer; }
137+
#refresh-btn:hover { background: #ffffff66; }
138+
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
139+
.stat-card { background: var(--card-bg); border-radius: 15px; padding: 1.5rem; box-shadow: 0 6px 20px var(--shadow); transition: transform 0.3s; }
140+
.stat-card:hover { transform: translateY(-5px); }
141+
.stat-card h2 { font-size: 1.1rem; color: var(--primary); margin-bottom: 0.75rem; }
142+
.stat-card p { font-size: 1.6rem; font-weight: 500; }
143+
.stat-card p.healthy { color: var(--success); }
144+
.stat-card p.unhealthy { color: var(--error); }
145+
.charts-grid, .tables-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
146+
.chart-container, .table-container, .logs-container { background: var(--card-bg); border-radius: 15px; padding: 2rem; box-shadow: 0 6px 20px var(--shadow); }
147+
.chart-container h2, .table-container h2, .logs-container h2 { font-size: 1.4rem; margin-bottom: 1rem; }
148+
input[type="text"] { width: 100%; padding: 0.75rem; margin-bottom: 1rem; border: 1px solid #ddd; border-radius: 8px; }
149+
table { width: 100%; border-collapse: collapse; }
150+
th, td { padding: 1rem; text-align: left; border-bottom: 1px solid #eee; }
151+
th { background: var(--primary); color: white; }
152+
.pagination { margin-top: 1rem; text-align: center; }
153+
.pagination button { background: var(--primary); color: white; border: none; padding: 0.5rem 1rem; margin: 0 0.25rem; border-radius: 5px; cursor: pointer; }
154+
.pagination button:disabled { background: #ccc; cursor: not-allowed; }
155+
.logs-container pre { font-size: 0.9rem; white-space: pre-wrap; max-height: 200px; overflow-y: auto; }
156+
footer { text-align: center; padding: 1rem; font-size: 0.9rem; }
157+
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
158+
.fade-in { animation: fadeIn 0.5s ease-in; }
159+
</style>
160+
161+
<script>
162+
const recentStatuses = JSON.parse('{{ recent_statuses }}');
163+
const recentRecords = JSON.parse('{{ recent_records | safe }}');
164+
const requestTrends = JSON.parse('{{ request_trends | safe }}');
165+
166+
document.addEventListener('DOMContentLoaded', () => {
167+
// Theme Toggle
168+
const themeToggle = document.getElementById('theme-toggle');
169+
themeToggle.addEventListener('click', () => {
170+
document.documentElement.dataset.theme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
171+
localStorage.setItem('theme', document.documentElement.dataset.theme);
172+
tippy('[data-tippy-content]').forEach(t => t.setProps({ theme: document.documentElement.dataset.theme }));
173+
});
174+
if (localStorage.getItem('theme') === 'dark') document.documentElement.dataset.theme = 'dark';
175+
176+
// Tooltips
177+
tippy('[data-tippy-content]', { theme: document.documentElement.dataset.theme || 'light' });
178+
179+
// Request Trends Chart
180+
const trendsChart = new Chart(document.getElementById('trendsChart').getContext('2d'), {
181+
type: 'line',
182+
data: {
183+
labels: requestTrends.map(t => t.timestamp),
184+
datasets: [{ label: 'Requests', data: requestTrends.map(t => parseInt(t.requests)), borderColor: '#3498db', fill: false }]
185+
},
186+
options: { responsive: true, scales: { y: { beginAtZero: true }, x: { title: { display: true, text: 'Time' } } } }
187+
});
188+
189+
// Table Population with Pagination and Filtering
190+
function populateTable(tableId, data, filterId, rowFn, paginationId, itemsPerPage = 5) {
191+
const table = document.getElementById(tableId);
192+
const filter = document.getElementById(filterId);
193+
const pagination = document.getElementById(paginationId);
194+
let filteredData = [...data];
195+
let currentPage = 1;
196+
197+
const render = () => {
198+
const start = (currentPage - 1) * itemsPerPage;
199+
const end = start + itemsPerPage;
200+
table.innerHTML = '';
201+
filteredData.slice(start, end).forEach(item => {
202+
const row = document.createElement('tr');
203+
row.classList.add('fade-in');
204+
row.innerHTML = rowFn(item);
205+
table.appendChild(row);
206+
});
207+
pagination.innerHTML = '';
208+
for (let i = 1; i <= Math.ceil(filteredData.length / itemsPerPage); i++) {
209+
const btn = document.createElement('button');
210+
btn.textContent = i;
211+
btn.disabled = i === currentPage;
212+
btn.addEventListener('click', () => { currentPage = i; render(); });
213+
pagination.appendChild(btn);
214+
}
215+
};
216+
217+
filter.addEventListener('input', () => {
218+
filteredData = data.filter(item => Object.values(item).some(v => v.toString().toLowerCase().includes(filter.value.toLowerCase())));
219+
currentPage = 1;
220+
render();
221+
});
222+
render();
223+
}
224+
225+
populateTable('status-table', recentStatuses, 'status-filter', s => `<td>${s.id.substring(0, 8)}...</td><td>${s.status}</td>`, 'status-pagination');
226+
populateTable('records-table', recentRecords, 'records-filter', r => `
227+
<td>${r.project_id}</td>
228+
<td>${r.id.substring(0, 8)}...</td>
229+
<td>${r.timestamp}</td>
230+
<td>${(parseInt(r.duration_ns) / 1000000).toFixed(2)}</td>
231+
`, 'records-pagination');
232+
233+
// WebSocket for Real-Time Logs
234+
const logs = document.getElementById('logs');
235+
const ws = new WebSocket(`ws://${location.host}/ws/logs`);
236+
ws.onmessage = (event) => logs.textContent += `\n${event.data}`;
237+
ws.onerror = () => logs.textContent = 'Log stream error';
238+
ws.onclose = () => logs.textContent += '\nLog stream closed';
239+
240+
// Refresh Button
241+
document.getElementById('refresh-btn').addEventListener('click', () => {
242+
fetch('/dashboard')
243+
.then(res => res.text())
244+
.then(html => {
245+
const doc = new DOMParser().parseFromString(html, 'text/html');
246+
document.body.innerHTML = doc.body.innerHTML;
247+
248+
// Reinitialize
249+
['uptime', 'queue_size', 'db_status', 'ingestion_rate', 'error_rate', 'total_records', 'avg_latency'].forEach(id =>
250+
document.getElementById(id).textContent = doc.getElementById(id).textContent);
251+
const newTrends = JSON.parse(doc.querySelector('script').textContent.match(/const requestTrends = (.+?);/)[1]);
252+
trendsChart.data.labels = newTrends.map(t => t.timestamp);
253+
trendsChart.data.datasets[0].data = newTrends.map(t => parseInt(t.requests));
254+
trendsChart.update();
255+
populateTable('status-table', JSON.parse(doc.querySelector('script').textContent.match(/const recentStatuses = (.+?);/)[1]), 'status-filter', s => `<td>${s.id.substring(0, 8)}...</td><td>${s.status}</td>`, 'status-pagination');
256+
populateTable('records-table', JSON.parse(doc.querySelector('script').textContent.match(/const recentRecords = (.+?);/)[1]), 'records-filter', r => `
257+
<td>${r.project_id}</td>
258+
<td>${r.id.substring(0, 8)}...</td>
259+
<td>${r.timestamp}</td>
260+
<td>${(parseInt(r.duration_ns) / 1000000).toFixed(2)}</td>
261+
`, 'records-pagination');
262+
tippy('[data-tippy-content]', { theme: document.documentElement.dataset.theme || 'light' });
263+
});
264+
});
265+
});
266+
</script>
267+
</body>
268+
</html>

src/ingest.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// src/ingest.rs
12
use actix_web::{get, post, web, HttpResponse, Responder};
23
use chrono::DateTime;
34
use serde::Deserialize;
@@ -11,7 +12,7 @@ use datafusion::arrow::record_batch::RecordBatch;
1112

1213
#[derive(Clone)]
1314
pub struct IngestStatusStore {
14-
inner: Arc<RwLock<HashMap<String, String>>>,
15+
pub inner: Arc<RwLock<HashMap<String, String>>>,
1516
}
1617

1718
impl IngestStatusStore {

0 commit comments

Comments
 (0)