fix: emit stats/histogram snapshot on SSE connect; remove polling api.get('/stats') from Dashboard

This commit is contained in:
2026-04-09 19:23:24 -04:00
parent d2a569496d
commit 6b8392102e
2 changed files with 11 additions and 25 deletions

View File

@@ -32,6 +32,15 @@ async def stream_events(
if last_id == 0: if last_id == 0:
last_id = await repo.get_max_log_id() last_id = await repo.get_max_log_id()
# Emit initial snapshot immediately so the client never needs to poll /stats
stats = await repo.get_stats_summary()
yield f"event: message\ndata: {json.dumps({'type': 'stats', 'data': stats})}\n\n"
histogram = await repo.get_log_histogram(
search=search, start_time=start_time,
end_time=end_time, interval_minutes=15,
)
yield f"event: message\ndata: {json.dumps({'type': 'histogram', 'data': histogram})}\n\n"
while True: while True:
if await request.is_disconnected(): if await request.is_disconnected():
break break

View File

@@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import api from '../utils/api';
import './Dashboard.css'; import './Dashboard.css';
import { Shield, Users, Activity, Clock } from 'lucide-react'; import { Shield, Users, Activity, Clock } from 'lucide-react';
@@ -31,26 +30,7 @@ const Dashboard: React.FC<DashboardProps> = ({ searchQuery }) => {
const [logs, setLogs] = useState<LogEntry[]>([]); const [logs, setLogs] = useState<LogEntry[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const fetchData = async () => {
try {
const [statsRes, logsRes] = await Promise.all([
api.get('/stats'),
api.get('/logs', { params: { limit: 50, search: searchQuery } })
]);
setStats(statsRes.data);
setLogs(logsRes.data.data);
} catch (err) {
console.error('Failed to fetch dashboard data', err);
} finally {
setLoading(false);
}
};
useEffect(() => { useEffect(() => {
// Initial fetch to populate UI immediately
fetchData();
// Setup SSE connection
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000/api/v1'; const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000/api/v1';
let url = `${baseUrl}/stream?token=${token}`; let url = `${baseUrl}/stream?token=${token}`;
@@ -64,13 +44,10 @@ const Dashboard: React.FC<DashboardProps> = ({ searchQuery }) => {
try { try {
const payload = JSON.parse(event.data); const payload = JSON.parse(event.data);
if (payload.type === 'logs') { if (payload.type === 'logs') {
setLogs(prev => { setLogs(prev => [...payload.data, ...prev].slice(0, 100));
const newLogs = payload.data;
// Prepend new logs, keep up to 100 in UI to prevent infinite DOM growth
return [...newLogs, ...prev].slice(0, 100);
});
} else if (payload.type === 'stats') { } else if (payload.type === 'stats') {
setStats(payload.data); setStats(payload.data);
setLoading(false);
} }
} catch (err) { } catch (err) {
console.error('Failed to parse SSE payload', err); console.error('Failed to parse SSE payload', err);