import React, { useState, useEffect } from 'react';
import axios from 'axios';
import {
ComposedChart,
Area,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
import './App.css';
import { formatChartTime } from './utils/timeUtils';
const asNumberOrZero = (value) => {
const n = Number(value);
return Number.isFinite(n) ? n : 0;
};
function History() {
const [historyData, setHistoryData] = useState([]);
const [days, setDays] = useState(7);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [showAveraged, setShowAveraged] = useState(true);
const [showTrends, setShowTrends] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [totalItems, setTotalItems] = useState(0);
useEffect(() => {
const loadHistory = async () => {
setLoading(true);
setError(null);
try {
// Ask backend for averaged data (server-side bucketing) when enabled
const response = await axios.get('/api/history/overview', {
params: { days, page: currentPage, limit: pageSize, averaged: showAveraged }
});
let data = [];
let total = 0;
// Handle both response formats
if (response.data.resources && Array.isArray(response.data.resources)) {
data = response.data.resources;
total = response.data.total || data.length;
} else if (Array.isArray(response.data)) {
data = response.data;
total = data.length;
}
// Fetch trends data if enabled
if (showTrends && data.length > 0) {
const trendsPromises = data.map(async (resource) => {
try {
const trendsResponse = await axios.get(`/api/resources/${resource.id}/trends`, {
params: { days }
});
return { ...resource, trendsData: trendsResponse.data };
} catch (err) {
console.error(`Error loading trends for resource ${resource.id}:`, err);
return resource;
}
});
data = await Promise.all(trendsPromises);
}
setHistoryData(data);
setTotalItems(total);
setLoading(false);
} catch (error) {
console.error('Error loading history:', error);
setError(error.message);
setLoading(false);
}
};
loadHistory();
}, [days, currentPage, pageSize, showAveraged, showTrends]);
if (loading) return
Loading history...
;
if (error) return Error loading history: {error}
;
return (
{historyData.length === 0 ? (
No check history
Add resources and wait for checks to complete
) : (
{historyData.map((resource) => {
// Server already handles averaging when averaged=true is passed
const chartData = prepareChartData(resource.checks || []);
return (
{resource.name}
Type: {resource.type.toUpperCase()}
Uptime: = 95 ? '#4caf50' : resource.uptime >= 80 ? '#ff9800' : '#f44336' }}>{resource.uptime}%
Avg Response: {resource.avgResponseTime}ms
{chartData.length > 0 ? (
{
if (name === 'Response Time') return `${value}ms`;
if (name === 'Status') return value === 1 ? '✓ UP' : '✗ DOWN';
if (name === 'Uptime %') return `${value}%`;
return value;
}}
/>
{
const { x, y, width, height, payload } = props;
const color = payload.status === 'up' ? '#4caf50' : '#f44336';
return (
);
}}
/>
) : (
No check data available
)}
{showTrends && resource.trendsData && (
Week-over-Week Comparison
{(() => {
const comparison = resource.trendsData?.comparison || {};
const responseTimeChange = asNumberOrZero(comparison.response_time_change);
const uptimeChange = asNumberOrZero(comparison.uptime_change);
return (
Response Time Change:{' '}
= 0 ? '#f44336' : '#4caf50' }}>
{responseTimeChange >= 0 ? '+' : ''}
{responseTimeChange.toFixed(1)}ms
Uptime Change:{' '}
= 0 ? '#4caf50' : '#f44336' }}>
{uptimeChange >= 0 ? '+' : ''}
{uptimeChange.toFixed(1)}%
);
})()}
{
if (name === 'Current Period') return `${value}ms`;
if (name === 'Previous Period') return `${value}ms`;
return value;
}}
/>
)}
{resource.checks?.length || 0}
Total Checks
= 95 ? '#4caf50' : resource.uptime >= 80 ? '#ff9800' : '#f44336', fontSize: '2rem', fontWeight: 'bold' }}>{resource.uptime}%
Uptime
{resource.avgResponseTime}ms
Avg Response
{(resource.checks || []).filter((c) => c.status === 'up').length}/{resource.checks?.length || 0}
Successful Checks
);
})}
{totalItems > pageSize && (
Page
{
const page = Math.max(1, Math.min(Math.ceil(totalItems / pageSize), parseInt(e.target.value) || 1));
setCurrentPage(page);
}}
style={{ width: '50px', padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px', textAlign: 'center' }}
/>
of {Math.ceil(totalItems / pageSize)}
)}
)}
);
}
function prepareChartData(checks) {
return checks.map((check) => {
const label = formatChartTime(check.checked_at);
return {
label,
responseTime: check.response_time || 0,
status: check.status,
statusNumeric: check.status === 'up' ? 1 : 0,
};
});
}
function prepareTrendsChartData(trendsData) {
if (!trendsData || !trendsData.current || !trendsData.previous) return [];
const maxLength = Math.max(
trendsData.current.data.length,
trendsData.previous.data.length
);
const result = [];
for (let i = 0; i < maxLength; i++) {
const currentDay = trendsData.current.data[i];
const previousDay = trendsData.previous.data[i];
result.push({
day: `Day ${i + 1}`,
currentResponseTime: currentDay ? asNumberOrZero(currentDay.avg_response_time) : null,
previousResponseTime: previousDay ? asNumberOrZero(previousDay.avg_response_time) : null,
});
}
return result;
}
export default History;