This commit is contained in:
seb
2026-07-07 20:05:23 +02:00
parent 43475ebff3
commit 61fee8069d
2 changed files with 96 additions and 97 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/node_modules/ /node_modules/
/tse_events.* /tse_events.*
/.env /.env
monitor.db*

View File

@@ -2,8 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
import { import {
ThemeProvider, CssBaseline, Box, Container, Grid, ThemeProvider, CssBaseline, Box, Container, Grid,
Typography, ToggleButton, ToggleButtonGroup, Typography, ToggleButton, ToggleButtonGroup,
Chip, CircularProgress, Alert, IconButton, Tooltip, Chip, CircularProgress, Alert, IconButton, Tooltip
Select, MenuItem, FormControl
} from '@mui/material'; } from '@mui/material';
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import { theme } from './theme.js'; import { theme } from './theme.js';
@@ -16,34 +15,23 @@ import { fetchSummary, fetchHourly, fetchOutages, fetchLive, fetchEndpoints } fr
const RANGE_DAYS = { '24h': 1, '7d': 7, '30d': 30 }; const RANGE_DAYS = { '24h': 1, '7d': 7, '30d': 30 };
const REFRESH_MS = 60_000; const REFRESH_MS = 60_000;
export default function App() { function EndpointDashboard({ endpoint, range }) {
const [endpoints, setEndpoints] = useState([]);
const [selectedEndpoint, setSelectedEndpoint] = useState('');
const [summary, setSummary] = useState(null); const [summary, setSummary] = useState(null);
const [hourly, setHourly] = useState([]); const [hourly, setHourly] = useState([]);
const [outages, setOutages] = useState([]); const [outages, setOutages] = useState([]);
const [live, setLive] = useState(null); const [live, setLive] = useState(null);
const [range, setRange] = useState('7d');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [lastRefresh, setLastRefresh] = useState(null); const [lastRefresh, setLastRefresh] = useState(null);
useEffect(() => {
fetchEndpoints().then(eps => {
setEndpoints(eps);
if (eps.length > 0) setSelectedEndpoint(eps[0].id);
}).catch(e => setError(e.message));
}, []);
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
if (!selectedEndpoint) return;
const days = RANGE_DAYS[range] ?? 7; const days = RANGE_DAYS[range] ?? 7;
try { try {
const [s, h, o, l] = await Promise.all([ const [s, h, o, l] = await Promise.all([
fetchSummary(selectedEndpoint), fetchSummary(endpoint.id),
fetchHourly(selectedEndpoint, days), fetchHourly(endpoint.id, days),
fetchOutages(selectedEndpoint, 50), fetchOutages(endpoint.id, 50),
fetchLive(selectedEndpoint), fetchLive(endpoint.id),
]); ]);
setSummary(s); setHourly(h); setOutages(o); setLive(l); setSummary(s); setHourly(h); setOutages(o); setLive(l);
setError(null); setError(null);
@@ -53,7 +41,7 @@ export default function App() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [range, selectedEndpoint]); }, [range, endpoint.id]);
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
@@ -64,6 +52,76 @@ export default function App() {
const isOnline = live && (Date.now() - new Date(live.ts).getTime()) < 180_000; const isOnline = live && (Date.now() - new Date(live.ts).getTime()) < 180_000;
return (
<Box sx={{ mb: 8, p: 3, bgcolor: 'background.paper', borderRadius: 2, border: '1px solid', borderColor: 'divider' }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<Typography variant="h6" sx={{ mr: 2, fontWeight: 700 }}>
{endpoint.id}
<Typography component="span" variant="caption" color="text.secondary" sx={{ ml: 1, fontWeight: 400 }}>
({endpoint.url})
</Typography>
</Typography>
{live && (
<Chip
label={isOnline ? '● ONLINE' : '○ STALE'}
color={isOnline ? 'success' : 'warning'}
size="small"
sx={{ fontWeight: 700, mr: 2 }}
/>
)}
<Box sx={{ flexGrow: 1 }} />
{lastRefresh && (
<Typography variant="caption" color="text.secondary" sx={{ mr: 2 }}>
updated {lastRefresh.toLocaleTimeString()}
</Typography>
)}
<Tooltip title="Refresh now">
<IconButton size="small" onClick={refresh} disabled={loading}>
<RefreshIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
{error && <Alert severity="error" sx={{ mb: 3 }}>API error: {error}</Alert>}
{loading && !summary ? (
<Box sx={{ display: 'flex', justifyContent: 'center', my: 5 }}>
<CircularProgress />
</Box>
) : (
<Grid container spacing={3}>
<Grid size={12}>
<SummaryCards summary={summary} />
</Grid>
<Grid size={12}>
<AvailabilityChart data={hourly} />
</Grid>
<Grid size={12}>
<ResponseTimeChart data={hourly} />
</Grid>
<Grid size={12}>
<OutageTable outages={outages} />
</Grid>
</Grid>
)}
</Box>
);
}
export default function App() {
const [endpoints, setEndpoints] = useState([]);
const [range, setRange] = useState('7d');
const [error, setError] = useState(null);
useEffect(() => {
fetchEndpoints()
.then(setEndpoints)
.catch(e => setError(e.message));
}, []);
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
@@ -78,68 +136,10 @@ export default function App() {
}}> }}>
<Container maxWidth="xl"> <Container maxWidth="xl">
<Box sx={{ py: 1.5, display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}> <Box sx={{ py: 1.5, display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}>
<Typography variant="h5" sx={{ letterSpacing: '-0.5px' }}> <Typography variant="h5" sx={{ flexGrow: 1, letterSpacing: '-0.5px' }}>
TSE Monitor Monitor
</Typography> </Typography>
{endpoints.length > 0 && (
<FormControl size="small" sx={{ minWidth: 120 }}>
<Select
value={selectedEndpoint}
onChange={(e) => setSelectedEndpoint(e.target.value)}
sx={{ color: 'white', '& .MuiSelect-icon': { color: 'white' }, '& .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.3)' } }}
>
{endpoints.map(ep => (
<MenuItem key={ep.id} value={ep.id}>{ep.id}</MenuItem>
))}
</Select>
</FormControl>
)}
<Box sx={{ flexGrow: 1 }} />
{live && (
<>
<Chip
label={isOnline ? '● ONLINE' : '○ STALE'}
color={isOnline ? 'success' : 'warning'}
size="small"
sx={{ fontWeight: 700 }}
/>
</>
)}
{lastRefresh && (
<Typography variant="caption" color="text.secondary">
updated {lastRefresh.toLocaleTimeString()}
</Typography>
)}
<Tooltip title="Refresh now">
<IconButton size="small" onClick={refresh} disabled={loading}>
<RefreshIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Container>
</Box>
<Container maxWidth="xl" sx={{ mt: 4 }}>
{error && <Alert severity="error" sx={{ mb: 3 }}>API error: {error}</Alert>}
{loading && !summary ? (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 10 }}>
<CircularProgress />
</Box>
) : (
<Grid container spacing={3}>
{/* Summary cards */}
<Grid size={12}>
<SummaryCards summary={summary} />
</Grid>
{/* Time-range toggle */}
<Grid size={12} sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<ToggleButtonGroup <ToggleButtonGroup
value={range} exclusive size="small" value={range} exclusive size="small"
onChange={(_, v) => v && setRange(v)} onChange={(_, v) => v && setRange(v)}
@@ -148,24 +148,22 @@ export default function App() {
<ToggleButton key={v} value={v} sx={{ px: 2 }}>{v}</ToggleButton> <ToggleButton key={v} value={v} sx={{ px: 2 }}>{v}</ToggleButton>
))} ))}
</ToggleButtonGroup> </ToggleButtonGroup>
</Grid> </Box>
</Container>
</Box>
{/* Availability chart */} <Container maxWidth="xl" sx={{ mt: 4 }}>
<Grid size={12}> {error && <Alert severity="error" sx={{ mb: 3 }}>Failed to load endpoints: {error}</Alert>}
<AvailabilityChart data={hourly} />
</Grid>
{/* Response-time chart */} {!error && endpoints.length === 0 && (
<Grid size={12}> <Box sx={{ display: 'flex', justifyContent: 'center', mt: 10 }}>
<ResponseTimeChart data={hourly} /> <CircularProgress />
</Grid> </Box>
{/* Outage table */}
<Grid size={12}>
<OutageTable outages={outages} />
</Grid>
</Grid>
)} )}
{endpoints.map(ep => (
<EndpointDashboard key={ep.id} endpoint={ep} range={range} />
))}
</Container> </Container>
</Box> </Box>
</ThemeProvider> </ThemeProvider>