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 './CommandCenterPages.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 [maxDays, setMaxDays] = useState(30); 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); const [effectiveDays, setEffectiveDays] = useState(7); const [limitedByRetention, setLimitedByRetention] = useState(false); useEffect(() => { // Fetch retention setting from server const fetchRetention = async () => { try { const response = await axios.get('/api/settings'); const retentionDays = response.data.retention_days || 30; setMaxDays(parseInt(retentionDays)); // Adjust current days if it exceeds retention if (days > retentionDays) { setDays(parseInt(retentionDays)); } } catch (error) { // Failed to fetch retention setting, use default } }; fetchRetention(); }, []); // eslint-disable-next-line react-hooks/exhaustive-deps 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; setEffectiveDays(response.data.effective_days || days); setLimitedByRetention(Boolean(response.data.limited_by_retention)); } 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) { return resource; } }); data = await Promise.all(trendsPromises); } setHistoryData(data); setTotalItems(total); setLoading(false); } catch (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 (
{limitedByRetention && (
⚠️ Data limited to {effectiveDays} days (server retention setting). Requested {days} days.
)}

Monitoring History

{[7, 14, 30].map((d) => ( ))}
{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 ? '#22c55e' : resource.uptime >= 80 ? '#f59e0b' : '#ef4444' }}>{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; }} />
)}

Total Checks

{resource.checks?.length || 0}

Uptime

= 95 ? '#22c55e' : resource.uptime >= 80 ? '#f59e0b' : '#ef4444' }}>{resource.uptime}%

Avg Response

{resource.avgResponseTime}ms

Successful Checks

{(resource.checks || []).filter((c) => c.status === 'up').length}/{resource.checks?.length || 0}

); })} {totalItems > pageSize && (
Page { const page = Math.max(1, Math.min(Math.ceil(totalItems / pageSize), parseInt(e.target.value) || 1)); setCurrentPage(page); }} className="cc-input-sm" /> 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;