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

@@ -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;