domain_health_status

This commit is contained in:
2026-04-28 20:38:50 -05:00
parent f097f96d06
commit 62219a372a
8 changed files with 743 additions and 4 deletions

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import {
FiRefreshCw, FiList, FiLogOut, FiSettings, FiKey, FiTrash2, FiPlus, FiInbox,
FiUsers, FiUser, FiHardDrive, FiDollarSign,
FiUsers, FiUser, FiHardDrive, FiDollarSign, FiActivity,
} from 'react-icons/fi';
import Login from './components/Login';
@@ -17,8 +17,10 @@ import AdminUsersModal from './components/AdminUsersModal';
import ChangeMyPasswordModal from './components/ChangeMyPasswordModal';
import DomainQuotaModal from './components/DomainQuotaModal';
import BillingModal from './components/BillingModal';
import HealthModal from './components/HealthModal';
import HealthBanner from './components/HealthBanner';
import { authAPI, domainsAPI, mailboxesAPI } from './services/api';
import { authAPI, domainsAPI, mailboxesAPI, healthAPI } from './services/api';
function App() {
const [user, setUser] = useState(null);
@@ -40,6 +42,10 @@ function App() {
const [showChangePw, setShowChangePw] = useState(false);
const [showDomainQuota, setShowDomainQuota] = useState(false);
const [showBilling, setShowBilling] = useState(false);
const [showHealth, setShowHealth] = useState(false);
// Persisted health status for the currently selected domain (drives the banner).
const [healthStatus, setHealthStatus] = useState(null);
const showToast = useCallback((message, type = 'success') => {
setToast({ message, type });
@@ -60,6 +66,20 @@ function App() {
setMailboxes(list);
}, []);
// Load (or re-load) the persisted health status for a domain.
// Cheap call — just reads from PostgreSQL, no checks are run.
const loadHealthStatus = useCallback(async (domain) => {
if (!domain) { setHealthStatus(null); return; }
try {
const status = await healthAPI.getStatus(domain);
setHealthStatus(status); // null if never checked, that's fine
} catch (err) {
// Silent: don't block the UI just because health status load failed.
console.warn('Failed to load health status:', err);
setHealthStatus(null);
}
}, []);
useEffect(() => {
(async () => {
try {
@@ -84,6 +104,7 @@ function App() {
if (first) {
setBusyMessage('Refreshing quotas...');
await loadMailboxes(first, true);
await loadHealthStatus(first);
}
} catch (err) {
showToast(`Failed to load: ${err.message}`, 'error');
@@ -100,6 +121,7 @@ function App() {
setBusyMessage('Loading mailboxes...');
try {
await loadMailboxes(domain, true);
await loadHealthStatus(domain);
} catch (err) {
showToast(`Failed to load mailboxes: ${err.message}`, 'error');
} finally {
@@ -115,7 +137,10 @@ function App() {
if (selectedDomain && !list.find((d) => d.domain === selectedDomain)) {
const first = list[0]?.domain || null;
setSelectedDomain(first);
if (first) await loadMailboxes(first, false);
if (first) {
await loadMailboxes(first, false);
await loadHealthStatus(first);
}
} else if (selectedDomain) {
await loadMailboxes(selectedDomain, false);
}
@@ -133,6 +158,7 @@ function App() {
setDomains([]);
setMailboxes([]);
setSelectedDomain(null);
setHealthStatus(null);
};
const handleDelete = async () => {
@@ -167,6 +193,12 @@ function App() {
}
};
// Called by HealthModal after a fresh check completes — re-load the
// persisted summary so the banner reflects the new state.
const handleHealthChecked = async () => {
if (selectedDomain) await loadHealthStatus(selectedDomain);
};
if (!bootChecked) {
return <div className="min-h-screen flex items-center justify-center text-gray-400">Loading...</div>;
}
@@ -269,6 +301,9 @@ function App() {
)}
<section className="card relative">
{/* Health banner (shows only when last check found problems) */}
<HealthBanner status={healthStatus} onOpen={() => setShowHealth(true)} />
<div className="flex items-start justify-between gap-4 mb-5 flex-wrap">
<div>
<h2 className="text-base font-semibold text-gray-900">
@@ -278,7 +313,16 @@ function App() {
Create/delete mailboxes, reset passwords, edit rules. Quotas are refreshed when you open a domain.
</p>
</div>
<div className="flex flex-row flex-nowrap gap-2">
<div className="flex flex-row flex-wrap gap-2">
<button
onClick={() => setShowHealth(true)}
disabled={!selectedDomain}
className="btn-secondary"
title="Run DNS, DMS and TLS cert checks for this domain"
>
<FiActivity className="w-4 h-4 mr-2" />
Check health
</button>
{isSuperAdmin && (
<button
onClick={() => setShowDomainQuota(true)}
@@ -441,6 +485,13 @@ function App() {
onToast={showToast}
/>
)}
<HealthModal
open={showHealth}
domain={selectedDomain}
onClose={() => setShowHealth(false)}
onCheckedReport={handleHealthChecked}
onToast={showToast}
/>
<ChangeMyPasswordModal
open={showChangePw}
onClose={() => setShowChangePw(false)}

View File

@@ -0,0 +1,56 @@
import React from 'react';
import { FiAlertTriangle, FiXCircle } from 'react-icons/fi';
/**
* Renders nothing if status is null, has no problems, or there are no
* fail/warn findings. Otherwise shows a small banner with a "View details"
* action that opens the HealthModal.
*/
const HealthBanner = ({ status, onOpen }) => {
if (!status || !status.has_problems) return null;
const fails = status.summary?.fail ?? 0;
const warns = status.summary?.warn ?? 0;
const isCritical = fails > 0;
const Icon = isCritical ? FiXCircle : FiAlertTriangle;
const wrapperColor = isCritical
? 'bg-red-50 border-red-200'
: 'bg-amber-50 border-amber-200';
const iconColor = isCritical ? 'text-red-600' : 'text-amber-600';
const buttonColor = isCritical
? 'text-red-700 hover:text-red-900 hover:bg-red-100'
: 'text-amber-700 hover:text-amber-900 hover:bg-amber-100';
let headline;
if (fails > 0 && warns > 0) {
headline = `${fails} problem${fails === 1 ? '' : 's'} and ${warns} warning${warns === 1 ? '' : 's'} detected`;
} else if (fails > 0) {
headline = `${fails} problem${fails === 1 ? '' : 's'} detected`;
} else {
headline = `${warns} warning${warns === 1 ? '' : 's'}`;
}
return (
<div className={`flex items-center justify-between gap-3 ${wrapperColor} border rounded-lg p-3 mb-4`}>
<div className="flex items-center gap-3 min-w-0">
<Icon className={`w-5 h-5 ${iconColor} flex-shrink-0`} />
<div className="min-w-0">
<p className="text-sm font-semibold text-gray-900 truncate">{headline}</p>
<p className="text-xs text-gray-600">
Last checked {new Date(status.checked_at).toLocaleString()}
</p>
</div>
</div>
<button
onClick={onOpen}
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors whitespace-nowrap ${buttonColor}`}
>
View details
</button>
</div>
);
};
export default HealthBanner;

View File

@@ -0,0 +1,151 @@
import React, { useEffect, useState } from 'react';
import { FiCheckCircle, FiAlertTriangle, FiXCircle, FiHelpCircle, FiRefreshCw } from 'react-icons/fi';
import Modal from './Modal';
import LoadingOverlay from './LoadingOverlay';
import { healthAPI } from '../services/api';
const LEVEL_STYLES = {
ok: { icon: FiCheckCircle, color: 'text-green-600', bg: 'bg-green-50', pill: 'pill-success', label: 'OK' },
warn: { icon: FiAlertTriangle, color: 'text-amber-600', bg: 'bg-amber-50', pill: 'pill-warn', label: 'Warning' },
fail: { icon: FiXCircle, color: 'text-red-600', bg: 'bg-red-50', pill: 'pill', label: 'Problem' },
unknown: { icon: FiHelpCircle, color: 'text-gray-500', bg: 'bg-gray-50', pill: 'pill', label: 'Unknown' },
};
const HealthModal = ({ open, domain, onClose, onCheckedReport, onToast }) => {
const [loading, setLoading] = useState(false);
const [report, setReport] = useState(null);
// When the modal opens, run a fresh check immediately (this is on-demand
// mode — the user clicked the "Check health" button, so they expect a
// current run, not a cached one).
useEffect(() => {
if (!open || !domain) { setReport(null); return; }
runCheck();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, domain]);
const runCheck = async () => {
setLoading(true);
try {
const data = await healthAPI.runCheck(domain);
setReport(data);
onCheckedReport?.(data);
} catch (err) {
onToast?.(`Health check failed: ${err.message}`, 'error');
} finally {
setLoading(false);
}
};
return (
<Modal
open={open}
onClose={onClose}
title="Domain health"
subtitle={domain}
size="lg"
>
<div className="relative min-h-[350px]">
{loading && <LoadingOverlay message="Running checks..." />}
{!loading && report && (
<div className="space-y-5">
{/* Overall summary */}
<OverallBanner report={report} />
{/* Per check */}
<div className="space-y-4">
{report.checks.map((check) => (
<CheckCard key={check.id} check={check} />
))}
</div>
{/* Re-run button */}
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
<span className="text-xs text-gray-500">
Last checked: {new Date(report.checked_at).toLocaleString()}
</span>
<button onClick={runCheck} className="btn-secondary">
<FiRefreshCw className="w-4 h-4 mr-2" />
Run check again
</button>
</div>
</div>
)}
</div>
</Modal>
);
};
const OverallBanner = ({ report }) => {
// Compute counts from findings to give a "X problems / Y warnings" headline.
const counts = { fail: 0, warn: 0, unknown: 0, ok: 0 };
for (const c of report.checks) {
for (const f of c.findings) counts[f.level] = (counts[f.level] ?? 0) + 1;
}
let headline, sub, style;
if (counts.fail > 0) {
headline = `${counts.fail} problem${counts.fail === 1 ? '' : 's'} detected`;
sub = counts.warn > 0 ? `Plus ${counts.warn} warning${counts.warn === 1 ? '' : 's'}.` : 'Action required.';
style = LEVEL_STYLES.fail;
} else if (counts.warn > 0) {
headline = `${counts.warn} warning${counts.warn === 1 ? '' : 's'}`;
sub = 'No critical problems, but worth a look.';
style = LEVEL_STYLES.warn;
} else {
headline = 'All checks passed';
sub = counts.unknown > 0
? `${counts.unknown} item${counts.unknown === 1 ? '' : 's'} could not be verified automatically.`
: 'Domain looks healthy.';
style = LEVEL_STYLES.ok;
}
const Icon = style.icon;
return (
<div className={`flex items-start gap-3 ${style.bg} border border-gray-200 rounded-lg p-4`}>
<Icon className={`w-6 h-6 ${style.color} flex-shrink-0 mt-0.5`} />
<div>
<h3 className="font-semibold text-gray-900">{headline}</h3>
<p className="text-sm text-gray-700 mt-0.5">{sub}</p>
</div>
</div>
);
};
const CheckCard = ({ check }) => {
const style = LEVEL_STYLES[check.level] || LEVEL_STYLES.unknown;
const Icon = style.icon;
return (
<div className="border border-gray-200 rounded-lg overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 bg-gray-50 border-b border-gray-200">
<div className="flex items-center gap-2">
<Icon className={`w-5 h-5 ${style.color}`} />
<h4 className="font-semibold text-gray-900">{check.title}</h4>
</div>
<span className={style.pill}>{style.label}</span>
</div>
<div className="divide-y divide-gray-100">
{check.findings.map((f, idx) => {
const fStyle = LEVEL_STYLES[f.level] || LEVEL_STYLES.unknown;
const FIcon = fStyle.icon;
return (
<div key={idx} className="flex items-start gap-3 px-4 py-2.5">
<FIcon className={`w-4 h-4 ${fStyle.color} flex-shrink-0 mt-0.5`} />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-800">{f.label}</div>
{f.detail && (
<div className="text-xs text-gray-600 mt-0.5 break-words">{f.detail}</div>
)}
</div>
</div>
);
})}
</div>
</div>
);
};
export default HealthModal;

View File

@@ -102,3 +102,19 @@ export const billingAPI = {
return (await api.get(`/api/billing/events${qs ? '?' + qs : ''}`)).data;
},
};
export const healthAPI = {
// Read the last persisted status (cheap; used by the banner).
// Returns null if the domain has never been checked.
getStatus: async (domain) => {
try {
return (await api.get(`/api/health/domains/${encodeURIComponent(domain)}`)).data;
} catch (err) {
if (err.statusCode === 404) return null;
throw err;
}
},
// Run all checks now and return the full report.
runCheck: async (domain) =>
(await api.post(`/api/health/domains/${encodeURIComponent(domain)}/check`)).data,
};