// status-components.jsx // Status page UI. Loaded after React + Babel + status-data.jsx. const { useState, useEffect, useMemo, useRef } = React; // ─── helpers ──────────────────────────────────────────────────────────────── const STATUS_META = { operational: { label: 'Operational', dot: 'var(--ok)', fg: 'var(--ok)' }, degraded: { label: 'Degraded Performance', dot: 'var(--warn)', fg: 'var(--warn)' }, outage: { label: 'Major Outage', dot: 'var(--bad)', fg: 'var(--bad)' }, }; const DAY_META = { '-1': { color: 'var(--bar-empty)', label: 'No data', status: 'no_data' }, '0': { color: 'var(--ok)', label: 'Operational', status: 'operational' }, '1': { color: 'var(--warn)', label: 'Degraded', status: 'degraded' }, '2': { color: 'var(--bad)', label: 'Outage', status: 'down' }, }; // Active-incident severity → visual color. Drives the service dot and bar tint within the // incident's time window. Uptime % and the operational counter stay probe-driven on purpose: // an operator can declare an incident with no underlying probe failures. const INCIDENT_META = { minor: { color: 'var(--warn)', label: 'Minor incident' }, major: { color: 'var(--bad)', label: 'Major incident' }, }; // Loss-of-signal styling. When `probesStale` is true, every service surface // (dot, per-row uptime, headline) flips to this — distinct from any // probe-derived state because "we don't know" is its own thing. const STALE_META = { color: 'var(--stale)', label: 'Stale data' }; // Severity ranking shared by the incident-overlay logic for the per-service dot // and history bars. Higher = worse. The overlay only repaints a bucket/dot when // the incident's rank is STRICTLY greater than the probe-derived rank for that // bucket — ties go to the probes, since they're observations and the incident // severity is operator opinion. `no_data` ranks 0 (treated as no claim), so an // incident overlay is allowed to fill an empty bucket. `outage` and `down` both // rank 2 because the public payload uses `outage` for service.status while the // per-bucket DAY_META uses `down` — same severity, two naming conventions. // // Not consolidated with the banner-local `rank` in OverallBanner: that table // operates on post-merged overall states, after incident severity has already // been mapped to a service-status word. Different layer, different inputs. const SEVERITY_RANK = { operational: 0, no_data: 0, minor: 1, degraded: 1, major: 2, outage: 2, down: 2, }; // Minimum interval covered by a single bucket of each history array — used to figure out // which trailing buckets fall inside an active incident's [startedAt, now] window. const RANGE_BUCKET_MS = { '1h': 60_000, '1d': 3_600_000, '7d': 86_400_000, '30d': 86_400_000, '90d': 86_400_000, }; // Visuals for each overlay severity. Incident severities reuse INCIDENT_META; // maintenance gets its own info-blue tint distinct from anything probe-derived. const OVERLAY_META = { minor: { color: 'var(--warn)', label: 'Minor incident' }, major: { color: 'var(--bad)', label: 'Major incident' }, maintenance: { color: 'var(--info)', label: 'Maintenance' }, }; // How an overlay ranks against the probe-derived bucket state. Same rule as // the active-incident overlay: an overlay only repaints a bucket when its // rank is STRICTLY greater than the probe's, so a real outage is never // visually downgraded by a maintenance announcement or a minor incident. // Maintenance ranks the same as a minor incident (1): it overrides // operational/no_data buckets but loses to a real outage probe. const OVERLAY_RANK = { minor: 1, major: 2, maintenance: 1, }; // Pick the worst overlay whose [startedAt, endedAt] intersects [bStart, bEnd]. // `overlays` items: { kind, severity, startedAt, endedAt|null }. Null endedAt // is an ongoing window — treated as ending at `nowMs`. function pickOverlayForBucket(overlays, bStart, bEnd, nowMs) { if (!overlays || overlays.length === 0) return null; let best = null; for (const o of overlays) { const oStart = new Date(o.startedAt).getTime(); const oEnd = o.endedAt ? new Date(o.endedAt).getTime() : nowMs; if (Number.isNaN(oStart) || Number.isNaN(oEnd)) continue; if (oStart < bEnd && oEnd > bStart) { const rank = OVERLAY_RANK[o.severity] ?? 0; if (!best || rank > best.rank) { best = { rank, meta: OVERLAY_META[o.severity] || OVERLAY_META.minor, kind: o.kind }; } } } return best; } const TIME_ZONE = 'Europe/Athens'; const TZ_LABEL = 'EEST'; function relTime(iso, now) { const diff = (new Date(iso) - now) / 1000; const abs = Math.abs(diff); if (abs < 60) return Math.round(abs) + 's ago'; if (abs < 3600) return Math.round(abs / 60) + 'm ago'; if (abs < 86400) return Math.round(abs / 3600) + 'h ago'; return Math.round(abs / 86400) + 'd ago'; } // App-wide date formatting. Canonical full format is `6 May, 15:30:22` for the // current year, or `6 May 2026, 15:30:22` for other years. Date-only and // time-only variants share the same building blocks so they stay consistent. const _DATE_PARTS_FMT = new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: TIME_ZONE }); const _TIME_FMT = new Intl.DateTimeFormat('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZone: TIME_ZONE }); const _YEAR_FMT = new Intl.DateTimeFormat('en-GB', { year: 'numeric', timeZone: TIME_ZONE }); function _dateParts(iso) { const parts = _DATE_PARTS_FMT.formatToParts(new Date(iso)); return { day: parts.find(p => p.type === 'day').value, month: parts.find(p => p.type === 'month').value, year: parts.find(p => p.type === 'year').value, }; } function fmtDateOnly(iso) { if (!iso) return '—'; const { day, month, year } = _dateParts(iso); return year === _YEAR_FMT.format(new Date()) ? `${day} ${month}` : `${day} ${month} ${year}`; } function fmtTimeOnly(iso) { if (!iso) return '—'; return _TIME_FMT.format(new Date(iso)); } function fmtDateTime(iso) { if (!iso) return '—'; return `${fmtDateOnly(iso)}, ${fmtTimeOnly(iso)}`; } function fmtClock(now) { return now.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZone: TIME_ZONE }); } // Maintenance window — JS counterpart of the PHP format_window helper so the // /maintenance/?id=… page can format the same string the homepage cards show. // Same-day: "16 May, 17:45 → 18:30". Cross-day: "16 May, 23:00 → 17 May, 02:00". const _HHMM_FMT = new Intl.DateTimeFormat('en-GB', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: TIME_ZONE }); const _DAYKEY_FMT = new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: TIME_ZONE }); function fmtMaintWindow(startsAt, endsAt) { if (!startsAt || !endsAt) return '—'; const start = new Date(startsAt); const end = new Date(endsAt); const sameDay = _DAYKEY_FMT.format(start) === _DAYKEY_FMT.format(end); if (sameDay) { return `${fmtDateOnly(startsAt)}, ${_HHMM_FMT.format(start)} → ${_HHMM_FMT.format(end)}`; } return `${fmtDateOnly(startsAt)}, ${_HHMM_FMT.format(start)} → ${fmtDateOnly(endsAt)}, ${_HHMM_FMT.format(end)}`; } // ─── DMU wordmark logo ────────────────────────────────────────────────────── function Logo() { function onClick(e) { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; if (!window.publicNavigate) return; e.preventDefault(); window.publicNavigate('home'); } return ( {/* The new SVG wordmarks already contain "DMU™ Status" — no sibling text needed. Two glyph variants render side by side; CSS in status.css hides whichever doesn't match the active data-theme on . */} DMU Status DMU Status ); } // ─── Header ──────────────────────────────────────────────────────────────── const THEME_STORAGE_KEY = 'dmu_theme_status'; function ThemeToggle() { // Source of truth is the dataset attribute set by the inline bootstrap in index.html. const [theme, setTheme] = useState(() => (typeof document !== 'undefined' && document.documentElement.dataset.theme === 'light') ? 'light' : 'dark' ); function toggle() { const next = theme === 'light' ? 'dark' : 'light'; // Brief class flip animates color/border/shadow swaps without bloating per-element CSS. const root = document.documentElement; root.classList.add('theme-transitioning'); root.dataset.theme = next; try { localStorage.setItem(THEME_STORAGE_KEY, next); } catch (e) {} setTheme(next); window.setTimeout(() => root.classList.remove('theme-transitioning'), 260); } const isLight = theme === 'light'; return ( ); } // Probes /api/me once on mount; renders an "Admin" link only if the response // is 200 and {admin: true}. Failure modes (401, network error, slow) silently // render nothing — the public page must keep working regardless of auth state. // The fetch is fire-and-forget; we never block the header on it. function AdminButton() { const [isAdmin, setIsAdmin] = useState(false); useEffect(() => { let cancelled = false; (async () => { try { const res = await fetch('/api/me', { credentials: 'same-origin' }); if (!res.ok || cancelled) return; const data = await res.json(); if (!cancelled && data && data.admin === true) setIsAdmin(true); } catch (_) { /* silent — public page must render regardless */ } })(); return () => { cancelled = true; }; }, []); if (!isAdmin) return null; return Admin; } function Header({ now, monitorTick }) { // Pulse the clock dot once whenever any monitor records a fresh check. // Compare against a ref so we only fire on real ticks — not on mount, and // not on unrelated re-renders from the seconds clock. const [pulse, setPulse] = useState(false); const prevTick = useRef(undefined); useEffect(() => { if (monitorTick == null) return; const isFirst = prevTick.current === undefined; if (!isFirst && prevTick.current === monitorTick) return; prevTick.current = monitorTick; if (isFirst) return; // first value is just "data loaded", not "fresh check" setPulse(true); const t = setTimeout(() => setPulse(false), 1700); return () => clearTimeout(t); }, [monitorTick]); return (
{fmtClock(now)} {TZ_LABEL}
); } // ─── Overall banner ───────────────────────────────────────────────────────── function InfoTip({ children }) { return ( {children} ); } // Rendered ABOVE OverallBanner when probesStale=true. Loss-of-signal is // warning-yellow, not red — it's "we don't know", not "outage observed". function StaleBanner({ proberLastSuccessAt, now }) { const ago = proberLastSuccessAt ? relTime(proberLastSuccessAt, now) : null; return (
Probe data is stale{ago ? ` — last successful check ${ago}` : ''}.
Service status below may not reflect current reality.
); } function OverallBanner({ services, uptime, proberLastSuccessAt, probesStale, activeMaintenance, now }) { const anyOutage = services.some(s => s.status === 'outage'); const anyDegraded = services.some(s => s.status === 'degraded'); const probeOverall = anyOutage ? 'outage' : anyDegraded ? 'degraded' : 'operational'; // Escalate the banner based on operator-declared incidents too — even if probes are clean, // an active incident should be visible at the top. Counter and uptime % stay probe-driven. const anyIncidentBad = services.some(s => s.incidentSeverity === 'major'); const anyIncidentMinor = services.some(s => s.incidentSeverity === 'minor'); const incidentOverall = anyIncidentBad ? 'outage' : anyIncidentMinor ? 'degraded' : 'operational'; // Worse-of-two so probe failures and incidents can each push the banner up but neither pulls it down. const rank = { operational: 0, maintenance: 1, degraded: 2, outage: 3 }; let probeOrIncidentOverall = rank[incidentOverall] > rank[probeOverall] ? incidentOverall : probeOverall; // Maintenance is an announcement layer: it doesn't downgrade a degraded / // outage banner, but it DOES escalate "operational" so users see at the top // that planned work is happening. Same one-way-up rule as incidents. const hasMaintenance = Array.isArray(activeMaintenance) && activeMaintenance.length > 0; if (hasMaintenance && rank.maintenance > rank[probeOrIncidentOverall]) { probeOrIncidentOverall = 'maintenance'; } // Stale wins everything else — once the prober has stopped reporting, no // probe-derived or incident-derived headline is meaningful. Spec precedence: // probesStale > incidentRank > probeRank. const overall = probesStale ? 'stale' : probeOrIncidentOverall; const meta = probesStale ? { label: STALE_META.label, dot: STALE_META.color, fg: STALE_META.color } : overall === 'maintenance' ? { label: 'Maintenance', dot: 'var(--info)', fg: 'var(--info)' } : STATUS_META[overall]; const maintCount = activeMaintenance?.length ?? 0; const heads = { operational: 'All systems operational', maintenance: 'Maintenance in progress', degraded: 'Some systems are experiencing issues', outage: 'Major outage in progress', stale: 'Status unknown — probes stale', }; const subs = { operational: 'Everything is running smoothly across every service.', maintenance: maintCount === 1 ? 'Planned maintenance is underway. Brief impact on the affected services is possible.' : `${maintCount} maintenance windows are underway. Brief impact on the affected services is possible.`, degraded: 'We\u2019re actively working on the issue. Most services remain unaffected.', outage: 'We\u2019re engaging the on-call team. Updates will follow as we learn more.', stale: `No probe data received for ${proberLastSuccessAt && now ? relTime(proberLastSuccessAt, now) : 'an extended period'}. Service status shown reflects the last successful check, not current reality.`, }; const eyebrowFreshness = proberLastSuccessAt ? `Last updated ${relTime(proberLastSuccessAt, now ?? new Date())}` : 'No probe data yet'; return (
); } // ─── Active incident with timeline ────────────────────────────────────────── function ActiveIncident({ incident, now, collapsible = true }) { const [open, setOpen] = useState(true); if (!incident) return null; // On the dedicated /incident/?id=… page we render the card statically — no // toggle, no chevron, body always visible. const isOpen = collapsible ? open : true; const HeadTag = collapsible ? 'button' : 'div'; const sevClass = incident.severity === 'major' ? 'bad' : 'warn'; // Resolved → neutral outline (no severity/status tint; the pills inside the // card already carry the resolved color). Monitoring → blue ("we're watching" // rather than "on fire"). Otherwise the severity drives the tint. const cardClass = incident.status === 'resolved' ? 'neutral' : incident.status === 'monitoring' ? 'info' : sevClass; // Current stage shown as a secondary pill alongside severity. const statusClass = incident.status === 'monitoring' ? 'info' : incident.status === 'identified' ? 'warn' : incident.status === 'resolved' ? 'ok' : 'bad'; const statusLabel = incident.status[0].toUpperCase() + incident.status.slice(1); return (
setOpen(o => !o) } : {})} >
{incident.severity === 'major' ? 'Major Incident' : 'Minor Incident'} {statusLabel}
{relTime(incident.started, now)} {collapsible && ( )}
{incident.title}
{incident.description &&

{incident.description}

}
Status{incident.status[0].toUpperCase() + incident.status.slice(1)}
Started{fmtDateTime(incident.started)}
Affects{incident.components.join(', ')}
    {(() => { const steps = ['Investigating', 'Identified', 'Monitoring', 'Resolved']; // A phase is "past" if it appears earlier in the canonical sequence // than the incident's current status — not just when an update was // posted at that label. That way Identified/Monitoring still go green // when an incident jumps straight from Investigating to Resolved. const currentIdx = steps.findIndex(s => s.toLowerCase() === incident.status); return steps.map((step, idx) => { const isPast = currentIdx > -1 && idx < currentIdx; const isCurrent = idx === currentIdx; // Per-phase class (tl-investigating / -identified / -monitoring / -resolved) // supplies the --tl-color custom property consumed by .current styling. return (
  1. {step}
  2. ); }); })()}
    {incident.updates.map((u, i) => (
  • {u.label} {fmtDateTime(u.at)} · {relTime(u.at, now)}

    {u.text}

  • ))}
); } // ─── Uptime bar grid ──────────────────────────────────────────────────────── // Pick the right bucket array for a given range key. function pickHistory(historyObj, range) { if (!historyObj) return []; if (range === '1h') return historyObj.hour || []; if (range === '1d') return historyObj.day || []; const days = historyObj['90d'] || []; if (range === '7d') return days.slice(-7); if (range === '30d') return days.slice(-30); return days; // '90d' } function UptimeRow({ service, viz, range, rowIndex = 0, probesStale = false }) { const [hovered, setHovered] = useState(null); const history = pickHistory(service.history, range); const total = history.length; // Per-row uptime is the server-derived raw-probe value for the currently // selected range — an object keyed by '1h'|'1d'|'7d'|'30d'|'90d', each value // a number or null. Bars smooth short bad runs to operational for visual // denoising; the percentage does not. Both surfaces are intentionally // separate — see how-status-works.jsx §4 for the documented tradeoff. // // The server emits null for any range with zero probes in window (a fresh // monitor with no probes ever gets null in every slot), so we don't need a // separate hasNoProbes check — the per-range null subsumes it. const uptimeForRange = service.uptime?.[range]; const hasUptimeNumber = !probesStale && typeof uptimeForRange === 'number'; const meta = STATUS_META[service.status]; // Active-incident overlay. Both the dot and the trailing bars only get repainted // with the incident severity when that severity is STRICTLY worse than the // probe-derived state — so a minor incident can never visually downgrade a // probe-observed outage, and a "no data" bucket inside the window can still // be filled in by any incident severity (no_data ranks 0). const incMeta = service.incidentSeverity ? INCIDENT_META[service.incidentSeverity] : null; const incidentRank = incMeta ? (SEVERITY_RANK[service.incidentSeverity] ?? 0) : -1; const probeStatusRank = SEVERITY_RANK[service.status] ?? 0; const incidentWinsDot = incMeta !== null && incidentRank > probeStatusRank; // Precedence: probesStale > incidentRank > probeRank. Once the prober has // gone silent, no operator overlay or probe state is meaningful for THIS dot. const dotColor = probesStale ? STALE_META.color : (incidentWinsDot ? incMeta.color : meta.dot); const dotLabel = probesStale ? STALE_META.label : (incidentWinsDot ? incMeta.label : meta.label); const bucketMs = RANGE_BUCKET_MS[range] ?? 60_000; // Per-bar overlay lookup uses the full per-service overlays array (resolved // incidents + maintenance windows, last 90 days). The active-incident // `incMeta` above remains the only signal for the dot; bars now consult the // overlay list directly so historical events tint their span too. const overlays = service.overlays || []; const nowMs = Date.now(); // Pulse the status dot once whenever the monitor records a fresh check. // We compare the incoming lastCheckedAt against a ref so we only fire on real changes // (not on mount, not on unrelated re-renders). const [pulse, setPulse] = useState(false); const prevCheckedAt = useRef(service.lastCheckedAt); useEffect(() => { if (!service.lastCheckedAt) return; if (prevCheckedAt.current === service.lastCheckedAt) return; prevCheckedAt.current = service.lastCheckedAt; setPulse(true); const t = setTimeout(() => setPulse(false), 1600); return () => clearTimeout(t); }, [service.lastCheckedAt]); // One-shot reveal for the newest bar when it transitions out of the fetching // shimmer (latest bucket was -1, now has a real value). We track the previous // value of that bucket with a ref so this only fires on the real handoff, // not on initial mount or when the range/total changes. const [landed, setLanded] = useState(false); const prevLatest = useRef(total > 0 ? history[total - 1] : null); useEffect(() => { if (total === 0) return; const cur = history[total - 1]; const prev = prevLatest.current; prevLatest.current = cur; if (prev === -1 && cur !== -1 && cur != null) { setLanded(true); const t = setTimeout(() => setLanded(false), 600); return () => clearTimeout(t); } }, [history, total]); // Tooltip label depends on the unit of each bucket. function bucketLabel(i) { const now = new Date(); if (range === '1h') { const d = new Date(now.getTime() - (total - 1 - i) * 60_000); return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); } if (range === '1d') { const d = new Date(now.getTime() - (total - 1 - i) * 3_600_000); return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); } const d = new Date(now); d.setDate(d.getDate() - (total - 1 - i)); return fmtDateOnly(d); } // Whole row is a link to the public detail page. Modifier-click and middle-click // fall through to a real navigation so "open in new tab" still works; bare left-clicks // hand off to the SPA router so we don't blow away the rest of the page state. function onRowClick(e) { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; if (!window.publicNavigate) return; e.preventDefault(); window.publicNavigate('monitor', { id: service.id }); } return ( // --row-i is read by the row's entry animation and by each bar's animation // so the bar sweep is offset by the row's stagger.
{dotLabel}
{service.lastCheckedAt ? 'Updated ' + relTime(service.lastCheckedAt, new Date()) : 'No checks yet'}
{service.name} {service.countsForUptime === false && ( This service is excluded from the total uptime calculation. )}
{service.desc}
{viz === 'bars' && (
setHovered(null)}> {history.map((d, i) => { // Incident tint only wins when its severity is strictly worse than what // the probes saw in this bucket. `no_data` ranks 0, so an overlay can // fill an empty bucket; a `down` bucket (rank 2) is never repainted // by a `minor` incident (rank 1) or by maintenance (rank 1) either. // The overlay list covers both incidents (active + resolved last // 90d) and maintenance windows for this service, so historical // events tint their span — not just the currently active one. const bEnd = nowMs - (total - 1 - i) * bucketMs; const bStart = bEnd - bucketMs; const overlay = pickOverlayForBucket(overlays, bStart, bEnd, nowMs); const probeRank = SEVERITY_RANK[DAY_META[d].status] ?? 0; const overlayWins = overlay !== null && overlay.rank > probeRank; const color = overlayWins ? overlay.meta.color : DAY_META[d].color; // The newest bucket is briefly empty between the moment its window // opens and the moment the prober writes a result into it. Treat // that specific bar as a "fetching" placeholder so the user sees a // shimmer instead of a mute "no data" gap that turns green a second // later. Skip when a winning overlay is already repainting it. const isFetching = i === total - 1 && d === -1 && !probesStale && !overlayWins; const isJustLanded = i === total - 1 && landed; return (
setHovered(i)} /> ); })} {hovered != null && (() => { const bEnd = nowMs - (total - 1 - hovered) * bucketMs; const bStart = bEnd - bucketMs; const overlay = pickOverlayForBucket(overlays, bStart, bEnd, nowMs); const probeRank = SEVERITY_RANK[DAY_META[history[hovered]].status] ?? 0; const overlayWins = overlay !== null && overlay.rank > probeRank; const tipColor = overlayWins ? overlay.meta.color : DAY_META[history[hovered]].color; const tipLabel = overlayWins ? overlay.meta.label : DAY_META[history[hovered]].label; const isFetching = hovered === total - 1 && history[hovered] === -1 && !probesStale && !overlayWins; return (
{bucketLabel(hovered)}
{isFetching ? <>Fetching status : <>{tipLabel}}
); })()}
)} {viz === 'dots' && (
{history.map((d, i) => (
))}
)} {viz === 'line' && ( )} {viz === 'pill' && ( )}
{hasUptimeNumber ? (
{uptimeForRange.toFixed(3)}%
) : (
)}
{RANGE_UPTIME_LABEL[range] ?? RANGE_UPTIME_LABEL['30d']}
); } function UptimeLine({ history }) { // Map: rolling 7-day "incident density" sparkline const w = 100, h = 28; const pts = history.map((d, i) => { const window = history.slice(Math.max(0, i - 6), i + 1); const score = window.reduce((a, b) => a + (b === 0 ? 1 : b === 1 ? 0.4 : 0), 0) / window.length; return [i / (history.length - 1) * w, h - score * h * 0.9 - 2]; }); const path = pts.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(2) + ' ' + p[1].toFixed(2)).join(' '); const fillPath = path + ` L ${w} ${h} L 0 ${h} Z`; return ( ); } function UptimePill({ history }) { const okCount = history.filter(d => d === 0).length; const pct = okCount / history.length * 100; return (
{history.map((d, i) => d !== 0 && (
))}
); } // ─── Services section ─────────────────────────────────────────────────────── const RANGES = [ { key: '1h', label: '1h' }, { key: '1d', label: '1d' }, { key: '7d', label: '7d' }, { key: '30d', label: '30d' }, { key: '90d', label: '90d' }, ]; // Per-row uptime label that tracks the selected range. The 30-day headline // at the top of the page is independent — it stays anchored at "30-day uptime" // regardless of this selection. const RANGE_UPTIME_LABEL = { '1h': '1-hour uptime', '1d': '24-hour uptime', '7d': '7-day uptime', '30d': '30-day uptime', '90d': '90-day uptime', }; const RANGE_WINDOW_LABEL = { '1h': { left: '1 hour ago', right: 'Now', sub: '60-minute' }, '1d': { left: '24 hours ago', right: 'Now', sub: '24-hour' }, '7d': { left: '7 days ago', right: 'Today', sub: '7-day' }, '30d': { left: '30 days ago', right: 'Today', sub: '30-day' }, '90d': { left: '90 days ago', right: 'Today', sub: '90-day' }, }; function ServicesSection({ services, viz, setViz, range, setRange, probesStale = false }) { const groups = useMemo(() => { const m = new Map(); services.forEach(s => { if (!m.has(s.group)) m.set(s.group, []); m.get(s.group).push(s); }); return [...m.entries()]; }, [services]); const win = RANGE_WINDOW_LABEL[range] || RANGE_WINDOW_LABEL['90d']; // A range is "available" if it contains ANY real probe data for any // service. Stricter "oldest bucket must have data" rules are too fragile — // a single missed probe at the cutoff hides the whole range, even when // there's plenty of usable data in the rest of the window. The dashboard // is more useful showing sparse data than hiding it. function rangeCovered(key) { return services.some(s => { const h = pickHistory(s.history, key); return h.some(v => v >= 0); }); } const visibleRanges = useMemo(() => RANGES.filter(r => { if (r.key === range) return true; // never hide the active button under the user return rangeCovered(r.key); }), [services, range]); // If the active range is no longer covered, fall back to the longest one that is. useEffect(() => { if (!services.length) return; if (rangeCovered(range)) return; // Prefer broader ranges first when stepping back. const fallback = [...RANGES].reverse().find(r => r.key !== range && rangeCovered(r.key)); if (fallback) setRange(fallback.key); }, [services, range, setRange]); return (

Services

Live status and {win.sub} history across every service we operate.

{visibleRanges.map(r => ( ))}
Operational Degraded Outage {probesStale && Stale data}
{win.left} {win.right}
{(() => { // Compute a flat index across groups so each row's CSS-driven entry // animation gets a continuous stagger rather than restarting per group. let rowIdx = 0; return groups.map(([groupName, list]) => (
{groupName}
{list.map(s => )}
)); })()}
); } // ─── Metrics charts ───────────────────────────────────────────────────────── // Bucket size per range key (used to label the hover tooltip on metric charts). const METRIC_STEP_MS = { '1h': 60_000, '1d': 3_600_000, '7d': 86_400_000, '30d': 86_400_000, '90d': 86_400_000, }; // Build a human-readable label for bucket index i in a window. // Buckets are oldest-first: i=0 is (count-1) intervals ago, i=count-1 is "now". function metricBucketLabel(windowKey, total, i) { const stepsAgo = total - 1 - i; const stepMs = METRIC_STEP_MS[windowKey] ?? 86_400_000; const d = new Date(Date.now() - stepsAgo * stepMs); if (windowKey === '1h' || windowKey === '1d') { return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); } return fmtDateOnly(d); } function fmtMetricValue(v, unit) { if (v === null || v === undefined) return '—'; if (unit === '%') return v.toFixed(2) + '%'; if (unit === 'ms') return Math.round(v) + ' ms'; return v.toLocaleString(); } function Chart({ data, label, unit, color, windowKey, windowLabel, description, yMin, yMax, variant = 'line' }) { const [hover, setHover] = useState(null); const w = 600, h = 140; // Find points where we have real values (null = no data in that bucket). const valid = data.map((v, i) => v == null ? null : { i, v }); const validValues = valid.filter(p => p !== null).map(p => p.v); const hasData = validValues.length > 0; const dataMax = hasData ? Math.max(...validValues) : 1; const dataMin = hasData ? Math.min(...validValues) : 0; // Honour fixed bounds when supplied; otherwise auto-scale to the data + add headroom. const min = (yMin != null) ? yMin : dataMin; const max = (yMax != null) ? yMax : dataMax; const rangeSize = Math.max(0.0001, max - min); const topPad = (yMax != null) ? 0 : rangeSize * 0.075; const bottomPad = (yMin != null) ? 0 : rangeSize * 0.075; const span = rangeSize + topPad + bottomPad; const offset = min - bottomPad; function pointFor(v, i) { return [ data.length > 1 ? (i / (data.length - 1)) * w : w / 2, h - ((v - offset) / span) * (h - 24) - 12, ]; } const pts = data.map((v, i) => v == null ? null : pointFor(v, i)); // Build line segments and fill segments, splitting around null buckets so we // don't draw straight lines across gaps in the data. const segments = []; let cur = []; for (const p of pts) { if (p == null) { if (cur.length) { segments.push(cur); cur = []; } } else { cur.push(p); } } if (cur.length) segments.push(cur); const linePath = segments.map(seg => seg.map((p, i) => (i === 0 ? 'M' : 'L') + p[0].toFixed(2) + ' ' + p[1].toFixed(2)).join(' ') ).join(' '); const fillPath = segments.filter(seg => seg.length >= 2).map(seg => { const start = seg[0], end = seg[seg.length - 1]; let s = 'M' + start[0].toFixed(2) + ' ' + h; seg.forEach(p => { s += ' L ' + p[0].toFixed(2) + ' ' + p[1].toFixed(2); }); s += ' L ' + end[0].toFixed(2) + ' ' + h + ' Z'; return s; }).join(' '); // Latest non-null value for the header readout. const cur_idx = [...valid].reverse().find(p => p !== null); const headValue = hover != null ? data[hover] : (cur_idx ? cur_idx.v : null); function onMove(e) { if (!hasData) return; const r = e.currentTarget.getBoundingClientRect(); const x = (e.clientX - r.left) / r.width; const i = Math.round(x * (data.length - 1)); setHover(Math.max(0, Math.min(data.length - 1, i))); } const gid = 'g-' + label.replace(/\W/g, '') + '-' + windowKey; const hoverPt = (hover != null && pts[hover]) ? pts[hover] : null; return (
{label} {description && {description}}
{fmtMetricValue(headValue, unit)}
{windowLabel}
setHover(null)}> {[0.25, 0.5, 0.75].map(g => ( ))} {hasData && variant === 'line' && } {hasData && variant === 'line' && } {hasData && variant === 'bar' && data.map((v, i) => { if (v == null || v <= 0) return null; const [cx, cy] = pointFor(v, i); const barW = (data.length > 1 ? (w / (data.length - 1)) : w) * 0.6; return ( ); })} {hoverPt && ( )} {hover != null && (
{metricBucketLabel(windowKey, data.length, hover)}
{fmtMetricValue(data[hover], unit)}
)} {!hasData && (
No data yet for this window.
)}
); } function MetricsSection({ metrics, range }) { if (!metrics) return null; const data = metrics[range]; if (!data) return null; return (

Metrics

Aggregate probe performance across public monitors. Matches the range you've picked above.

); } // ─── Scheduled maintenance ────────────────────────────────────────────────── // Surfaces maintenance windows the operator has marked as currently underway. // Renders one ActiveIncident-style card per window so the visual treatment // matches the rest of the page (collapsible head, meta grid, timeline, // updates list). One
per row keeps the page's stagger animation // working — each card is a direct child of .shell with its own delay slot. const MNT_TIMELINE_STEPS = ['Scheduled', 'In Progress', 'Verifying', 'Completed']; const _mntSlug = (label) => label.toLowerCase().replace(/\s+/g, '-'); function ActiveMaintenance({ maintenance = [], now }) { if (maintenance.length === 0) return null; return ( {maintenance.map(m => )} ); } function MaintenanceCard({ m, now, collapsible = true }) { const [open, setOpen] = useState(true); // Same toggle pattern as ActiveIncident: head becomes a static
on the // dedicated /maintenance/?id=… page so there's no expand/collapse there. const isOpen = collapsible ? open : true; const HeadTag = collapsible ? 'button' : 'div'; const statusLabel = m.status === 'inprogress' ? 'In Progress' : m.status === 'verifying' ? 'Verifying' : m.status[0].toUpperCase() + m.status.slice(1); // Map the DB status to its label inside MNT_TIMELINE_STEPS — used to decide // which step is "current" and which are "past" (green). const currentStep = m.status === 'completed' ? 'Completed' : m.status === 'verifying' ? 'Verifying' : m.status === 'inprogress' ? 'In Progress' : 'Scheduled'; const currentIdx = MNT_TIMELINE_STEPS.indexOf(currentStep); // Completed → neutral outline (same treatment as resolved incidents). // Otherwise info-blue, signalling "active maintenance". const cardClass = m.status === 'completed' ? 'neutral' : 'info'; // Second pill (status) tracks the timeline current-step color so the head // visually mirrors where the maintenance is in its lifecycle. const statusPillColor = m.status === 'completed' ? 'ok' : m.status === 'verifying' ? 'info' : m.status === 'inprogress' ? 'warn' : 'info'; return (
setOpen(o => !o) } : {})} >
Maintenance {statusLabel}
{relTime(m.startsAt, now)} {collapsible && ( )}
{m.title}
{m.description &&

{m.description}

}
Status{statusLabel}
Window{m.window}
Affects{(m.affects || []).join(', ') || '—'}
    {MNT_TIMELINE_STEPS.map((step, idx) => { const isPast = currentIdx > -1 && idx < currentIdx; const isCurrent = idx === currentIdx; return (
  1. {step}
  2. ); })}
{m.updates && m.updates.length > 0 && (
    {m.updates.map((u, i) => (
  • {u.label} {fmtDateTime(u.at)} · {relTime(u.at, now)}

    {u.text}

  • ))}
)}
); } function MaintenanceSection({ maintenance = [] }) { if (maintenance.length === 0) return null; return (

Scheduled maintenance

Upcoming planned work. We post these at least 72 hours in advance.

    {maintenance.map(m => (
  • {m.title}
    {m.window}
    {m.impact}
    {m.affects.map(a => {a})}
  • ))}
); } // ─── Past incidents ───────────────────────────────────────────────────────── function PastIncidents({ incidents = [] }) { // Group by calendar month (Y-m-d strings parsed deterministically to avoid // any local-vs-UTC day shift). Incidents arrive newest-first from the API, // so the resulting month order is also newest-first. const groups = useMemo(() => { const map = new Map(); incidents.forEach(inc => { const [year, month, day] = inc.date.split('-').map(s => parseInt(s, 10)); const key = `${year}-${String(month).padStart(2, '0')}`; const monthDate = new Date(year, month - 1, 1); const label = monthDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); const monthShort = monthDate.toLocaleDateString('en-US', { month: 'short' }); if (!map.has(key)) map.set(key, { label, monthShort, items: [] }); map.get(key).items.push({ ...inc, _day: day }); }); return [...map.values()]; }, [incidents]); if (incidents.length === 0) return null; return (

Incident history

Past 90 days.

{incidents.length} {incidents.length === 1 ? 'incident' : 'incidents'}
{groups.map(g => (
{g.label} {g.items.length}
    {g.items.map(p => { const sevClass = p.severity === 'major' ? 'bad' : p.severity === 'maintenance' ? 'info' : 'warn'; // The public API prefixes past-incident ids with 'p' so they // don't collide with active-incident ids in this same payload. // Strip it before hitting /api/incidents.php?id=N on the detail page. const numericId = typeof p.id === 'string' && p.id.startsWith('p') ? p.id.slice(1) : p.id; const onClick = e => { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; if (!window.publicNavigate) return; e.preventDefault(); window.publicNavigate('incident', { id: numericId }); }; return (
  • {p.title}
    {fmtDateOnly(p.date)} · {p.duration}
    {p.severity[0].toUpperCase() + p.severity.slice(1)} Resolved
  • ); })}
))}
); } // ─── Maintenance history ──────────────────────────────────────────────────── // Same structure as PastIncidents — grouped by month, each row a link to the // dedicated detail page. The row gets a "Maintenance" + "Completed" pill pair // so it visually distinguishes itself from past incidents above. function MaintenanceHistory({ maintenance = [] }) { const groups = useMemo(() => { const map = new Map(); maintenance.forEach(m => { const [year, month, day] = m.date.split('-').map(s => parseInt(s, 10)); const key = `${year}-${String(month).padStart(2, '0')}`; const monthDate = new Date(year, month - 1, 1); const label = monthDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); const monthShort = monthDate.toLocaleDateString('en-US', { month: 'short' }); if (!map.has(key)) map.set(key, { label, monthShort, items: [] }); map.get(key).items.push({ ...m, _day: day }); }); return [...map.values()]; }, [maintenance]); if (maintenance.length === 0) return null; return (

Maintenance history

Completed planned work, past 90 days.

{maintenance.length} {maintenance.length === 1 ? 'window' : 'windows'}
{groups.map(g => (
{g.label} {g.items.length}
    {g.items.map(p => { // API prefixes maintenance ids with 'm' to avoid colliding with // incidents; strip before hitting /api/maintenance.php?id=N. const numericId = typeof p.id === 'string' && p.id.startsWith('m') ? p.id.slice(1) : p.id; const onClick = e => { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; if (!window.publicNavigate) return; e.preventDefault(); window.publicNavigate('maintenance', { id: numericId }); }; return (
  • {p.title}
    {fmtDateOnly(p.date)} · {p.duration}
    Maintenance Completed
  • ); })}
))}
); } // ─── Footer ───────────────────────────────────────────────────────────────── function Footer() { const year = new Date().getFullYear(); return ( ); } // ─── Standalone page views (SPA routes) ───────────────────────────────────── // Renders the dedicated incident detail route (/incident/?id=N) inside the // public shell. Accepts `id` as a prop so the SPA router can pass it directly; // falls back to the URL on direct loads. function IncidentPage({ id: idProp }) { const [incident, setIncident] = useState(null); const [error, setError] = useState(null); const [now, setNow] = useState(new Date()); const id = idProp != null ? idProp : new URLSearchParams(window.location.search).get('id'); useEffect(() => { const i = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(i); }, []); useEffect(() => { if (!id) { setError('Missing id'); return; } setIncident(null); setError(null); apiFetch('/api/incidents.php?id=' + encodeURIComponent(id)) .then(setIncident) .catch(err => setError(err.message)); }, [id]); // Reflect the loaded incident in the browser tab title (and in // bookmarks / shared links). The static in incident/index.html // stays as "Incident Detail — DMU Status" so crawlers and the // first-paint state have something sensible; this overrides it // once the live data lands. useEffect(() => { if (incident && incident.title) { document.title = incident.title + ' — DMU Status'; } else if (error) { document.title = 'Incident not found — DMU Status'; } }, [incident, error]); function onBack(e) { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; e.preventDefault(); window.publicNavigate && window.publicNavigate('home'); } return ( <React.Fragment> <Header now={now} /> <main className="shell"> <div className="incident-page-nav"> <a href="/" onClick={onBack}>← Back to status</a> </div> {error && ( <section className="card"> <h2>Incident not found</h2> <p className="muted" style={{ marginTop: 6 }}>{error}</p> </section> )} {!error && !incident && ( <p className="muted" style={{ padding: '24px 0' }}>Loading…</p> )} {incident && <ActiveIncident incident={incident} now={now} collapsible={false} />} <Footer /> </main> </React.Fragment> ); } // Same shape as IncidentPage but for the maintenance detail route. The admin // endpoint is publicly readable; we reshape its `components` field into // `affects` and pre-format the window string client-side so we don't need a // separate public endpoint. function MaintenancePage({ id: idProp }) { const [m, setM] = useState(null); const [error, setError] = useState(null); const [now, setNow] = useState(new Date()); const id = idProp != null ? idProp : new URLSearchParams(window.location.search).get('id'); useEffect(() => { const i = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(i); }, []); useEffect(() => { if (!id) { setError('Missing id'); return; } setM(null); setError(null); apiFetch('/api/maintenance.php?id=' + encodeURIComponent(id)) .then(raw => { setM({ id: 'm' + raw.id, title: raw.title, description: raw.description, status: raw.status, startsAt: raw.startsAt, endsAt: raw.endsAt, window: fmtMaintWindow(raw.startsAt, raw.endsAt), affects: raw.components, updates: raw.updates, }); }) .catch(err => setError(err.message)); }, [id]); function onBack(e) { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; e.preventDefault(); window.publicNavigate && window.publicNavigate('home'); } return ( <React.Fragment> <Header now={now} /> <main className="shell"> <div className="incident-page-nav"> <a href="/" onClick={onBack}>← Back to status</a> </div> {error && ( <section className="card"> <h2>Maintenance not found</h2> <p className="muted" style={{ marginTop: 6 }}>{error}</p> </section> )} {!error && !m && ( <p className="muted" style={{ padding: '24px 0' }}>Loading…</p> )} {m && <MaintenanceCard m={m} now={now} collapsible={false} />} <Footer /> </main> </React.Fragment> ); } // ─── Monitor detail page (SPA route /monitor/?id=N) ───────────────────────── // Public-safe monitor detail view. Backs /monitor/?id=N. Mirrors the admin // MonitorDetailView layout (hero + stats grid + history bars + details + recent // checks table) but is driven by /api/monitor_public.php, which intentionally // withholds anything not already implied by the homepage (target, type, region, // owner, threshold, per-check response_ms). Component itself doesn't enforce // the redaction — it just renders whatever the API hands back. const MONITOR_RANGE_LABELS = { '1h': '60-minute', '1d': '24-hour', '7d': '7-day', '30d': '30-day', '90d': '90-day', }; function fmtMonInterval(sec) { if (sec == null) return '—'; if (sec < 60) return `every ${sec}s`; if (sec % 60 === 0 && sec < 3600) return `every ${sec / 60}m`; if (sec % 3600 === 0) return `every ${sec / 3600}h`; return `every ${Math.round(sec / 60)}m`; } function MonStatCard({ label, value, unit, sub, color }) { return ( <div className="mon-stat"> <div className="mon-stat-lbl">{label}</div> <div className="mon-stat-val" style={color ? { color } : undefined}> {value} {unit && <span className="mon-stat-unit">{unit}</span>} </div> {sub && <div className="mon-stat-sub">{sub}</div>} </div> ); } function MonitorHistory({ history, overlays = [], range, setRange }) { const [hovered, setHovered] = useState(null); const buckets = pickHistory(history, range); const total = buckets.length; // Mirror the UptimeRow handoff: while the newest bucket is -1 we're waiting // on a probe, so show the shimmer + spinner tooltip. The moment it flips to // a real value, play a one-shot reveal so it doesn't pop into place. const [landed, setLanded] = useState(false); const prevLatest = useRef(total > 0 ? buckets[total - 1] : null); useEffect(() => { if (total === 0) return; const cur = buckets[total - 1]; const prev = prevLatest.current; prevLatest.current = cur; if (prev === -1 && cur !== -1 && cur != null) { setLanded(true); const t = setTimeout(() => setLanded(false), 600); return () => clearTimeout(t); } }, [buckets, total]); // Same overlay rule as UptimeRow: per-bar lookup against the full overlays // array (resolved incidents + maintenance windows, last 90d). An overlay // only repaints a bucket when its rank is STRICTLY greater than the probe // state — real outages are never visually downgraded. const bucketMs = RANGE_BUCKET_MS[range] ?? 60_000; const nowMs = Date.now(); function bucketLabel(i) { const now = new Date(); if (range === '1h') { const d = new Date(now.getTime() - (total - 1 - i) * 60_000); return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); } if (range === '1d') { const d = new Date(now.getTime() - (total - 1 - i) * 3_600_000); return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); } const d = new Date(now); d.setDate(d.getDate() - (total - 1 - i)); return fmtDateOnly(d); } // Show the range whenever it contains any real probe data — see the note // on the homepage's rangeCovered. Showing sparse bars beats hiding the // whole range over a missed probe at the cutoff. function rangeCovered(key) { const h = pickHistory(history, key); return h.some(v => v >= 0); } const visibleRanges = RANGES.filter(r => r.key === range || rangeCovered(r.key)); const win = RANGE_WINDOW_LABEL[range] || RANGE_WINDOW_LABEL['90d']; return ( <section className="card mon-history-card"> <div className="card-head"> <div> <h2>Uptime history</h2> <p className="muted">{MONITOR_RANGE_LABELS[range] || range} window, smoothed bucket view.</p> </div> <div className="seg-row"> <div className="seg"> {visibleRanges.map(r => ( <button key={r.key} className={range === r.key ? 'on' : ''} onClick={() => setRange(r.key)}>{r.label}</button> ))} </div> </div> </div> <div className="srv-legend"> <div className="legend-l"> <span><span className="dot" style={{ background: 'var(--ok)' }} /> Operational</span> <span><span className="dot" style={{ background: 'var(--warn)' }} /> Degraded</span> <span><span className="dot" style={{ background: 'var(--bad)' }} /> Outage</span> </div> <div className="legend-r"> <span className="muted">{win.left}</span> <span className="legend-line" /> <span className="muted">{win.right}</span> </div> </div> <div className="mon-bars-wrap"> <div className="bars mon-bars" onMouseLeave={() => setHovered(null)}> {buckets.map((d, i) => { const bEnd = nowMs - (total - 1 - i) * bucketMs; const bStart = bEnd - bucketMs; const overlay = pickOverlayForBucket(overlays, bStart, bEnd, nowMs); const probeRank = SEVERITY_RANK[DAY_META[d].status] ?? 0; const overlayWins = overlay !== null && overlay.rank > probeRank; const color = overlayWins ? overlay.meta.color : DAY_META[d].color; const isFetching = i === total - 1 && d === -1 && !overlayWins; const isJustLanded = i === total - 1 && landed; return ( <div key={i} className={`bar bar-${d}${overlayWins ? ' bar-inc' : ''}${isFetching ? ' bar-fetching' : ''}${isJustLanded ? ' bar-just-landed' : ''}`} style={{ '--bar-color': color, '--bar-i': i }} onMouseEnter={() => setHovered(i)} /> ); })} {hovered != null && (() => { const bEnd = nowMs - (total - 1 - hovered) * bucketMs; const bStart = bEnd - bucketMs; const overlay = pickOverlayForBucket(overlays, bStart, bEnd, nowMs); const probeRank = SEVERITY_RANK[DAY_META[buckets[hovered]].status] ?? 0; const overlayWins = overlay !== null && overlay.rank > probeRank; const tipColor = overlayWins ? overlay.meta.color : DAY_META[buckets[hovered]].color; const tipLabel = overlayWins ? overlay.meta.label : DAY_META[buckets[hovered]].label; const isFetching = hovered === total - 1 && buckets[hovered] === -1 && !overlayWins; return ( <div className="bar-tip" style={{ left: `calc(${(hovered + 0.5) / total * 100}% )` }}> <div className="bar-tip-d">{bucketLabel(hovered)}</div> <div className="bar-tip-s"> {isFetching ? <><span className="bar-tip-spinner" />Fetching status</> : <><span className="dot" style={{ background: tipColor }} />{tipLabel}</>} </div> </div> ); })()} </div> </div> </section> ); } const MONITOR_CHECKS_PAGE_SIZE = 20; function MonitorPage({ id: idProp }) { const [data, setData] = useState(null); const [error, setError] = useState(null); const [now, setNow] = useState(new Date()); const [range, setRange] = useState('1h'); // Persisted across the 30s refresh — new checks land at index 0, shifting // later rows down a slot but leaving the user's chosen page in place. const [checksPage, setChecksPage] = useState(0); const id = idProp != null ? idProp : new URLSearchParams(window.location.search).get('id'); useEffect(() => { const i = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(i); }, []); // Same polling cadence as the home page so the bars + recent checks stay live. useEffect(() => { if (!id) { setError('Missing id'); return; } let alive = true; function load() { apiFetch('/api/monitor_public.php?id=' + encodeURIComponent(id)) .then(d => { if (alive) { setData(d); setError(null); } }) .catch(err => { if (alive) setError(err.message); }); } load(); const t = setInterval(load, 30000); return () => { alive = false; clearInterval(t); }; }, [id]); // Reflect the loaded monitor in the browser tab title (and in // bookmarks / shared links). The static <title> in monitor/index.html // stays as "Service Monitor — DMU Status" for crawlers and the // first-paint state; this overrides it once the live data lands. useEffect(() => { const name = data && data.monitor && data.monitor.name; if (name) { document.title = name + ' — DMU Status'; } else if (error) { document.title = 'Monitor not found — DMU Status'; } }, [data, error]); function onBack(e) { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; e.preventDefault(); window.publicNavigate && window.publicNavigate('home'); } const m = data?.monitor; const stats = data?.stats; const meta = m ? STATUS_META[m.status] : null; return ( <React.Fragment> <Header now={now} /> <main className="shell"> <div className="incident-page-nav"> <a href="/" onClick={onBack}>← Back to status</a> </div> {error && ( <section className="card"> <h2>Monitor not found</h2> <p className="muted" style={{ marginTop: 6 }}>{error}</p> </section> )} {!error && !data && ( <p className="muted" style={{ padding: '24px 0' }}>Loading…</p> )} {m && ( <React.Fragment> <section className="card mon-hero"> <div className="mon-hero-row"> <span className="info-tip srv-dot-tip mon-hero-dot-tip" tabIndex={0} aria-label={`${meta.label}, ${m.lastCheckedAt ? 'updated ' + relTime(m.lastCheckedAt, now) : 'never checked'}`} > <span className="srv-dot mon-hero-dot" style={{ background: meta.dot, color: meta.dot }} /> <span className="info-tip-bubble srv-dot-bubble"> <div className="srv-dot-tip-row"> <span className="srv-dot" style={{ background: meta.dot, color: meta.dot }} /> <div> <div className="srv-dot-tip-title">{meta.label}</div> <div className="srv-dot-tip-meta"> {m.lastCheckedAt ? 'Updated ' + relTime(m.lastCheckedAt, now) : 'No checks yet'} </div> </div> </div> </span> </span> <h1 className="mon-hero-title">{m.name}</h1> </div> {m.description && <p className="mon-hero-desc">{m.description}</p>} <div className="mon-hero-meta"> {m.group && ( <React.Fragment> <span className="muted">{m.group}</span> <span className="mon-hero-sep">·</span> </React.Fragment> )} <span className="muted"> {m.lastCheckedAt ? `Last checked ${relTime(m.lastCheckedAt, now)}` : 'No checks yet'} </span> <span className="mon-hero-sep">·</span> <span className="muted">Probed {fmtMonInterval(m.interval)}</span> {m.countsForUptime === false && ( <React.Fragment> <span className="mon-hero-sep">·</span> <span className="muted">Excluded from overall uptime</span> </React.Fragment> )} </div> </section> <section className="card"> <div className="mon-stats"> <MonStatCard label="Uptime · 24h" value={stats.uptime24h != null ? stats.uptime24h.toFixed(2) : '—'} unit={stats.uptime24h != null ? '%' : ''} sub={`${stats.totalChecks24h} ${stats.totalChecks24h === 1 ? 'check' : 'checks'}`} color="var(--ok)" /> <MonStatCard label="Uptime · 7d" value={stats.uptime7d != null ? stats.uptime7d.toFixed(2) : '—'} unit={stats.uptime7d != null ? '%' : ''} sub="rolling window" color="var(--ok)" /> <MonStatCard label="Uptime · 30d" value={stats.uptime30d != null ? stats.uptime30d.toFixed(2) : '—'} unit={stats.uptime30d != null ? '%' : ''} sub="rolling window" color="var(--ok)" /> <MonStatCard label="Avg response · 24h" value={stats.avgResponseMs24h != null ? Math.round(stats.avgResponseMs24h) : '—'} unit={stats.avgResponseMs24h != null ? 'ms' : ''} sub="across all probes" color="var(--accent)" /> <MonStatCard label="Failures · 24h" value={stats.failures24h} sub={stats.failures24h === 0 ? 'all clear' : 'down events'} color={stats.failures24h > 0 ? 'var(--bad)' : 'var(--text)'} /> <MonStatCard label="Last check" value={m.lastCheckedAt ? relTime(m.lastCheckedAt, now) : '—'} sub={m.lastCheckedAt ? fmtTimeOnly(m.lastCheckedAt) : 'no probe yet'} color="var(--info)" /> </div> </section> <MonitorHistory history={m.history} overlays={m.overlays} range={range} setRange={setRange} /> {(() => { const all = data.recentChecks; const total = all.length; const totalPages = Math.max(1, Math.ceil(total / MONITOR_CHECKS_PAGE_SIZE)); // Clamp in case the dataset shrank — e.g. a row was removed in admin // and the user is sitting on what used to be the last page. const activePage = Math.min(checksPage, totalPages - 1); const start = activePage * MONITOR_CHECKS_PAGE_SIZE; const slice = all.slice(start, start + MONITOR_CHECKS_PAGE_SIZE); const showPager = total > MONITOR_CHECKS_PAGE_SIZE; return ( <section className="card mon-checks-card"> <div className="card-head"> <div> <h2>Recent checks</h2> <p className="muted"> {total === 0 ? 'No probes recorded yet.' : showPager ? `Showing ${start + 1}–${start + slice.length} of ${total}.` : `Latest ${total} probe${total === 1 ? '' : 's'}.`} </p> </div> </div> {total === 0 ? ( <p className="muted">The prober runs {fmtMonInterval(m.interval)} — check back shortly.</p> ) : ( <React.Fragment> <table className="tbl mon-checks-tbl"> <thead> <tr> <th>Time</th> <th style={{ width: 140 }}>Status</th> <th style={{ width: 90 }}>HTTP</th> <th>Error</th> </tr> </thead> <tbody> {slice.map((c, i) => { const pillClass = c.status === 'up' ? 'ok' : c.status === 'degraded' ? 'warn' : c.status === 'down' ? 'bad' : 'info'; const statusLabel = c.status === 'up' ? 'Operational' : c.status[0].toUpperCase() + c.status.slice(1); return ( <tr key={start + i}> <td className="cell-mono">{c.checkedAt ? fmtDateTime(c.checkedAt) : '—'}</td> <td> <span className={`pill pill-sm pill-${pillClass}`}> <span className="dot" />{statusLabel} </span> </td> <td className="cell-mono">{c.httpCode ?? <span className="muted">—</span>}</td> <td className="muted">{c.error || '—'}</td> </tr> ); })} </tbody> </table> {showPager && ( <div className="pager"> <button className="btn btn-ghost btn-sm" disabled={activePage === 0} onClick={() => setChecksPage(p => Math.max(0, p - 1))} > ← Previous </button> <span className="muted pager-status">Page {activePage + 1} of {totalPages}</span> <button className="btn btn-ghost btn-sm" disabled={activePage >= totalPages - 1} onClick={() => setChecksPage(p => Math.min(totalPages - 1, p + 1))} > Next → </button> </div> )} </React.Fragment> )} </section> ); })()} </React.Fragment> )} <Footer /> </main> </React.Fragment> ); } Object.assign(window, { Header, OverallBanner, ActiveIncident, ActiveMaintenance, MaintenanceCard, ServicesSection, MetricsSection, MaintenanceSection, PastIncidents, MaintenanceHistory, Footer, IncidentPage, MaintenancePage, MonitorPage, fmtMaintWindow, });