u
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
/node_modules/
|
/node_modules/
|
||||||
/tse_events.*
|
/tse_events.*
|
||||||
/.env
|
/.env
|
||||||
|
monitor.db*
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user