Add History page with detailed check data graphs and statistics

This commit is contained in:
nmemmert committed 2025-12-10 15:16:28 -05:00
1 parent 08ea842206
commit 69e321d35f
3 files changed
+240

No files matched your search

+3
View File
@@ -4,6 +4,7 @@ import axios from 'axios';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import './App.css';
import SettingsWizard from './SettingsWizard';
import History from './History';
function Dashboard() {
const [resources, setResources] = useState([]);
@@ -500,6 +501,7 @@ function Navbar() {
<h1>🔍 Resource Monitor</h1>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Link to="/" style={{ color: 'white', textDecoration: 'none', opacity: 0.9, fontWeight: 500 }}>Dashboard</Link>
<Link to="/history" style={{ color: 'white', textDecoration: 'none', opacity: 0.9, fontWeight: 500 }}>History</Link>
<Link to="/settings" style={{ color: 'white', textDecoration: 'none', opacity: 0.9, fontWeight: 500, display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
Settings
{!notificationsConfigured && (
@@ -534,6 +536,7 @@ function App() {
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/resource/:id" element={<ResourceDetail />} />
<Route path="/history" element={<History />} />
<Route path="/settings" element={<div className="container"><SettingsWizard /></div>} />
</Routes>
</div>
+170
View File
@@ -0,0 +1,170 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import {
LineChart,
Line,
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
ComposedChart,
Bar,
} from 'recharts';
import './App.css';
function History() {
const [historyData, setHistoryData] = useState([]);
const [days, setDays] = useState(7);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadHistory();
}, [days]);
const loadHistory = async () => {
setLoading(true);
try {
const response = await axios.get('/api/history/overview', { params: { days } });
setHistoryData(response.data);
setLoading(false);
} catch (error) {
console.error('Error loading history:', error);
setLoading(false);
}
};
if (loading) return <div className="container">Loading history...</div>;
return (
<div className="container">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
<h2>Monitoring History</h2>
<div style={{ display: 'flex', gap: '0.5rem' }}>
{[7, 14, 30].map((d) => (
<button
key={d}
className={`btn ${days === d ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setDays(d)}
>
{d} Days
</button>
))}
</div>
</div>
{historyData.length === 0 ? (
<div className="empty-state">
<h3>No check history</h3>
<p>Add resources and wait for checks to complete</p>
</div>
) : (
<div>
{historyData.map((resource) => (
<div key={resource.id} className="detail-section" style={{ marginBottom: '3rem' }}>
<Link to={`/resource/${resource.id}`} style={{ textDecoration: 'none', color: 'inherit' }}>
<h3 style={{ marginBottom: '0.5rem', cursor: 'pointer', color: '#667eea' }}>
{resource.name}
</h3>
</Link>
<p style={{ color: '#666', marginBottom: '1rem' }}>
Type: <strong>{resource.type}</strong> | Uptime: <strong>{resource.uptime}%</strong> | Avg Response: <strong>{resource.avgResponseTime}ms</strong>
</p>
{resource.checks.length > 0 ? (
<div className="chart-container" style={{ height: '400px', marginBottom: '2rem' }}>
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={prepareChartData(resource.checks)}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="time"
tick={{ fontSize: 12 }}
angle={-45}
textAnchor="end"
height={80}
/>
<YAxis yAxisId="left" />
<YAxis yAxisId="right" orientation="right" />
<Tooltip
contentStyle={{ backgroundColor: '#f5f5f5', border: '1px solid #ddd' }}
formatter={(value, name) => {
if (name === 'Response Time (ms)') return `${value}ms`;
if (name === 'Status') return value === 1 ? 'UP' : 'DOWN';
return value;
}}
/>
<Legend />
<Area
yAxisId="left"
type="monotone"
dataKey="responseTime"
fill="#667eea"
stroke="#667eea"
name="Response Time (ms)"
fillOpacity={0.3}
/>
<Bar
yAxisId="right"
dataKey="statusNumeric"
fill="#4caf50"
name="Status"
opacity={0.5}
/>
</ComposedChart>
</ResponsiveContainer>
</div>
) : (
<div style={{ color: '#999', padding: '2rem', textAlign: 'center' }}>
No check data available
</div>
)}
{/* Statistics Summary */}
<div className="stats-grid" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))' }}>
<div className="stat">
<p className="stat-value">{resource.checks.length}</p>
<p className="stat-label">Total Checks</p>
</div>
<div className="stat">
<p className="stat-value">{resource.uptime}%</p>
<p className="stat-label">Uptime</p>
</div>
<div className="stat">
<p className="stat-value">{resource.avgResponseTime}ms</p>
<p className="stat-label">Avg Response</p>
</div>
<div className="stat">
<p className="stat-value">
{resource.checks.filter((c) => c.status === 'up').length}/
{resource.checks.length}
</p>
<p className="stat-label">Successful Checks</p>
</div>
</div>
</div>
))}
</div>
)}
</div>
);
}
function prepareChartData(checks) {
return checks.map((check) => {
const date = new Date(check.checked_at);
return {
time: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
date: date.toLocaleDateString(),
responseTime: check.response_time || 0,
status: check.status,
statusNumeric: check.status === 'up' ? 1 : 0,
statusCode: check.status_code,
};
});
}
export default History;
+67
View File
@@ -246,6 +246,73 @@ TIMEOUT=${timeout}
}
});
// Get historical check data for a resource
app.get('/api/resources/:id/history', (req, res) => {
const { id } = req.params;
const { days = 7 } = req.query;
const resource = db.prepare('SELECT * FROM resources WHERE id = ?').get(id);
if (!resource) {
return res.status(404).json({ error: 'Resource not found' });
}
const checks = db.prepare(`
SELECT
id,
status,
response_time,
status_code,
error_message,
details,
checked_at
FROM checks
WHERE resource_id = ? AND checked_at > datetime('now', ?)
ORDER BY checked_at ASC
`).all(id, `-${days} days`);
res.json({
resource,
checks,
count: checks.length,
});
});
// Get all resources' check history for dashboard
app.get('/api/history/overview', (req, res) => {
const { days = 7 } = req.query;
const resources = db.prepare('SELECT * FROM resources WHERE enabled = 1 ORDER BY name').all();
const overview = resources.map(resource => {
const checks = db.prepare(`
SELECT
status,
response_time,
checked_at
FROM checks
WHERE resource_id = ? AND checked_at > datetime('now', ?)
ORDER BY checked_at ASC
`).all(resource.id, `-${days} days`);
const upCount = checks.filter(c => c.status === 'up').length;
const uptime = checks.length > 0 ? (upCount / checks.length * 100) : 0;
const avgResponseTime = checks.length > 0
? checks.reduce((sum, c) => sum + (c.response_time || 0), 0) / checks.length
: 0;
return {
id: resource.id,
name: resource.name,
type: resource.type,
checks,
uptime: uptime.toFixed(2),
avgResponseTime: avgResponseTime.toFixed(0),
};
});
res.json(overview);
});
// Test email
app.post('/api/test-email', async (req, res) => {
const nodemailer = require('nodemailer');