fix: emit stats/histogram snapshot on SSE connect; remove polling api.get('/stats') from Dashboard
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user