NX-10x: Reliability, error handling, runtime UX hardening, and migration safety gate (NX-101, NX-102, NX-103, NX-104) #32
@@ -76,6 +76,10 @@ function didMetricSeriesChange(prev = [], next = []) {
|
||||
return prevLast?.ts !== nextLast?.ts || Number(prevLast?.value) !== Number(nextLast?.value);
|
||||
}
|
||||
|
||||
function isTargetUnreachableError(err) {
|
||||
return err?.code === "target_unreachable" || err?.status === 503;
|
||||
}
|
||||
|
||||
async function loadMetric(targetId, metric, range, tokens, refresh) {
|
||||
const { from, to } = toQueryRange(range);
|
||||
return apiFetch(
|
||||
@@ -99,6 +103,7 @@ export function TargetDetailPage() {
|
||||
const [targetMeta, setTargetMeta] = useState(null);
|
||||
const [owners, setOwners] = useState([]);
|
||||
const [groupTargets, setGroupTargets] = useState([]);
|
||||
const [offlineState, setOfflineState] = useState(null);
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const refreshRef = useRef(refresh);
|
||||
@@ -114,22 +119,16 @@ export function TargetDetailPage() {
|
||||
setLoading(true);
|
||||
}
|
||||
try {
|
||||
const [connections, xacts, cache, locksTable, activityTable, overviewData, targetInfo, ownerRows, allTargets] = await Promise.all([
|
||||
const [connections, xacts, cache, targetInfo, ownerRows, allTargets] = await Promise.all([
|
||||
loadMetric(id, "connections_total", range, tokens, refreshRef.current),
|
||||
loadMetric(id, "xacts_total", range, tokens, refreshRef.current),
|
||||
loadMetric(id, "cache_hit_ratio", range, tokens, refreshRef.current),
|
||||
apiFetch(`/targets/${id}/locks`, {}, tokens, refreshRef.current),
|
||||
apiFetch(`/targets/${id}/activity`, {}, tokens, refreshRef.current),
|
||||
apiFetch(`/targets/${id}/overview`, {}, tokens, refreshRef.current),
|
||||
apiFetch(`/targets/${id}`, {}, tokens, refreshRef.current),
|
||||
apiFetch(`/targets/${id}/owners`, {}, tokens, refreshRef.current),
|
||||
apiFetch("/targets", {}, tokens, refreshRef.current),
|
||||
]);
|
||||
if (!active) return;
|
||||
setSeries({ connections, xacts, cache });
|
||||
setLocks(locksTable);
|
||||
setActivity(activityTable);
|
||||
setOverview(overviewData);
|
||||
setTargetMeta(targetInfo);
|
||||
setOwners(ownerRows);
|
||||
const groupId = targetInfo?.tags?.monitor_group_id;
|
||||
@@ -141,6 +140,34 @@ export function TargetDetailPage() {
|
||||
} else {
|
||||
setGroupTargets([]);
|
||||
}
|
||||
try {
|
||||
const [locksTable, activityTable, overviewData] = await Promise.all([
|
||||
apiFetch(`/targets/${id}/locks`, {}, tokens, refreshRef.current),
|
||||
apiFetch(`/targets/${id}/activity`, {}, tokens, refreshRef.current),
|
||||
apiFetch(`/targets/${id}/overview`, {}, tokens, refreshRef.current),
|
||||
]);
|
||||
if (!active) return;
|
||||
setLocks(locksTable);
|
||||
setActivity(activityTable);
|
||||
setOverview(overviewData);
|
||||
setOfflineState(null);
|
||||
} catch (liveErr) {
|
||||
if (!active) return;
|
||||
if (isTargetUnreachableError(liveErr)) {
|
||||
setLocks([]);
|
||||
setActivity([]);
|
||||
setOverview(null);
|
||||
setOfflineState({
|
||||
message:
|
||||
"Target is currently unreachable. Check host/port, network route, SSL mode, and database availability.",
|
||||
host: liveErr?.details?.host || targetInfo?.host || "-",
|
||||
port: liveErr?.details?.port || targetInfo?.port || "-",
|
||||
requestId: liveErr?.requestId || null,
|
||||
});
|
||||
} else {
|
||||
throw liveErr;
|
||||
}
|
||||
}
|
||||
setError("");
|
||||
} catch (e) {
|
||||
if (active) setError(String(e.message || e));
|
||||
@@ -281,6 +308,17 @@ export function TargetDetailPage() {
|
||||
<span className="muted">Responsible users:</span>
|
||||
{owners.length > 0 ? owners.map((item) => <span key={item.user_id} className="owner-pill">{item.email}</span>) : <span className="muted">none assigned</span>}
|
||||
</div>
|
||||
{offlineState && (
|
||||
<div className="card target-offline-card">
|
||||
<h3>Target Offline</h3>
|
||||
<p>{offlineState.message}</p>
|
||||
<div className="target-offline-meta">
|
||||
<span><strong>Host:</strong> {offlineState.host}</span>
|
||||
<span><strong>Port:</strong> {offlineState.port}</span>
|
||||
{offlineState.requestId ? <span><strong>Request ID:</strong> {offlineState.requestId}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{uiMode === "easy" && overview && easySummary && (
|
||||
<>
|
||||
<div className={`card easy-status ${easySummary.health}`}>
|
||||
|
||||
@@ -2022,6 +2022,29 @@ select:-webkit-autofill {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.target-offline-card {
|
||||
border-color: #a85757;
|
||||
background: linear-gradient(130deg, #2c1724 0%, #1f1f38 100%);
|
||||
}
|
||||
|
||||
.target-offline-card h3 {
|
||||
margin: 0 0 8px;
|
||||
color: #fecaca;
|
||||
}
|
||||
|
||||
.target-offline-card p {
|
||||
margin: 0 0 10px;
|
||||
color: #fde2e2;
|
||||
}
|
||||
|
||||
.target-offline-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: #d4d4f5;
|
||||
}
|
||||
|
||||
.chart-tooltip {
|
||||
background: #0f1934ee;
|
||||
border: 1px solid #2f4a8b;
|
||||
|
||||
Reference in New Issue
Block a user