mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-10 05:53:05 +09:00
fix(ui): incident rows say host offline / service down instead of 'offline over limit'
This commit is contained in:
1 parent
a3c243c952
commit
eeb2792d06
4 files changed
+83
-11
No files matched your search
@@ -0,0 +1,37 @@
|
||||
import { METRIC_LABELS } from './fleetMetrics.js';
|
||||
|
||||
const SERVICE_PREFIX = 'svc:';
|
||||
|
||||
const isNumber = (value) => typeof value === 'number' && !Number.isNaN(value);
|
||||
const hasValue = (value) => value !== null && value !== undefined && value !== '';
|
||||
|
||||
// Wording for a fleet alert row. Threshold alerts read "<metric> over limit";
|
||||
// host-offline and service-down alerts have no limit, so they never say it.
|
||||
// `detail` drops every missing fragment instead of printing "—" or "null".
|
||||
export function fleetAlertText(t, alert, subject) {
|
||||
const { metric, value, threshold } = alert;
|
||||
|
||||
if (metric === 'offline') {
|
||||
return {
|
||||
title: t('app.incidents.hostOffline', 'Host offline'),
|
||||
detail: t('app.incidents.hostOfflineDetail', '{{subject}} stopped reporting', { subject }),
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof metric === 'string' && metric.startsWith(SERVICE_PREFIX)) {
|
||||
const service = metric.slice(SERVICE_PREFIX.length);
|
||||
return {
|
||||
title: t('app.incidents.serviceDown', '{{service}} is down', { service }),
|
||||
detail: t('app.incidents.serviceDownDetail', 'on {{subject}}', { subject }),
|
||||
};
|
||||
}
|
||||
|
||||
const label = METRIC_LABELS[metric] || metric;
|
||||
const parts = [];
|
||||
if (isNumber(value)) parts.push(`${label} ${value.toFixed(1)}`);
|
||||
if (hasValue(threshold)) parts.push(t('app.incidents.limitValue', 'Limit: {{value}}', { value: threshold }));
|
||||
return {
|
||||
title: t('app.incidents.overLimitOn', '{{metric}} over limit on {{subject}}', { metric: label, subject }),
|
||||
detail: parts.join(' · ') || label,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { fleetAlertText } from './alertText.js';
|
||||
|
||||
const t = (_key, fallback, vars = {}) => fallback.replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k]);
|
||||
|
||||
test('host offline never says "over limit" or prints null', () => {
|
||||
const text = fleetAlertText(t, { metric: 'offline', value: null, threshold: null }, 'web-1');
|
||||
assert.deepEqual(text, { title: 'Host offline', detail: 'web-1 stopped reporting' });
|
||||
});
|
||||
|
||||
test('service-down alert names the service', () => {
|
||||
const text = fleetAlertText(t, { metric: 'svc:mysql', value: null, threshold: null }, 'web-1');
|
||||
assert.equal(text.title, 'mysql is down');
|
||||
assert.doesNotMatch(text.detail, /null|—|over limit/);
|
||||
});
|
||||
|
||||
test('threshold alert shows reading and limit', () => {
|
||||
const text = fleetAlertText(t, { metric: 'cpu', value: 91.234, threshold: 90 }, 'web-1');
|
||||
assert.equal(text.title, 'CPU % over limit on web-1');
|
||||
assert.equal(text.detail, 'CPU % 91.2 · Limit: 90');
|
||||
});
|
||||
|
||||
test('threshold alert without a limit omits it', () => {
|
||||
const text = fleetAlertText(t, { metric: 'disk', value: 50, threshold: null }, 'web-1');
|
||||
assert.equal(text.detail, 'Disk % 50.0');
|
||||
});
|
||||
@@ -3301,22 +3301,30 @@
|
||||
"hostAlertsFiring_one": "1 host alert firing",
|
||||
"hostAlertsFiring_other": "{{count}} host alerts firing",
|
||||
"hostLimitsLiveOnThe": "Host limits live on the",
|
||||
"hostOffline": "Host offline",
|
||||
"hostOfflineDetail": "{{subject}} stopped reporting",
|
||||
"impact": "Impact",
|
||||
"incident": "Incident {{state}}",
|
||||
"limit": "Limit",
|
||||
"limitValue": "Limit: {{value}}",
|
||||
"loadingIncidents": "Loading incidents",
|
||||
"metric": "Metric",
|
||||
"noMonitorIsDownAndNo": "No monitor is down and no host is over its limit.",
|
||||
"noUpdatesPostedYet": "No updates posted yet.",
|
||||
"nothingIsWrongRightNow": "Nothing is wrong right now",
|
||||
"overLimitOn": "{{metric}} over limit on {{subject}}",
|
||||
"postAnUpdate": "Post an update",
|
||||
"reading": "Reading",
|
||||
"resolve": "Resolve",
|
||||
"resolved": "Resolved",
|
||||
"rules": "Rules",
|
||||
"searchIncidentsAndAlerts": "Search incidents and alerts…",
|
||||
"serviceDown": "{{service}} is down",
|
||||
"serviceDownDetail": "on {{subject}}",
|
||||
"subject": "Subject",
|
||||
"thisServer": "This server",
|
||||
"timeline": "Timeline",
|
||||
"unknownServer": "Unknown server",
|
||||
"whatChanged": "What changed?",
|
||||
"whatHappened": "What happened"
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { useTableSort } from '@/hooks/useTableSort';
|
||||
import { useColumnVisibility } from '@/hooks/useColumnVisibility';
|
||||
import { useTopbarActions, useTopbarChrome } from '@/hooks/useTopbarActions';
|
||||
import { METRIC_LABELS } from '../components/monitoring/fleetMetrics';
|
||||
import { fleetAlertText } from '../components/monitoring/alertText';
|
||||
import { impactTone, INCIDENT_STATES } from '../components/monitoring/monitorShared';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toastError } from '@/utils/errorMessage';
|
||||
@@ -238,14 +238,14 @@ export default function Incidents() {
|
||||
});
|
||||
|
||||
const hostDetail = (alert) => (alert.type
|
||||
? `${alert.type} ${formatValue(alert.value)} / ${alert.threshold}`
|
||||
: 'This server');
|
||||
? `${alert.type} ${formatValue(alert.value)}${alert.threshold == null ? '' : ` / ${alert.threshold}`}`
|
||||
: t('app.incidents.thisServer', 'This server'));
|
||||
|
||||
const fromActive = activeAlerts.map((alert, index) => ({
|
||||
kind: 'alert',
|
||||
key: `active-${alert.type}-${index}`,
|
||||
title: alert.message,
|
||||
subject: 'This server',
|
||||
subject: t('app.incidents.thisServer', 'This server'),
|
||||
detail: hostDetail(alert),
|
||||
state: 'firing',
|
||||
tone: 'red',
|
||||
@@ -262,7 +262,7 @@ export default function Incidents() {
|
||||
kind: 'alert',
|
||||
key: `history-${alert.timestamp || index}-${index}`,
|
||||
title: alert.message,
|
||||
subject: 'This server',
|
||||
subject: t('app.incidents.thisServer', 'This server'),
|
||||
detail: hostDetail(alert),
|
||||
state: alert.severity,
|
||||
tone: statusKind(alert.severity),
|
||||
@@ -282,19 +282,19 @@ export default function Incidents() {
|
||||
// sources above describe the panel's own box and have no server-side
|
||||
// lifecycle to ack or resolve.
|
||||
const fromFleet = fleetAlerts.map((alert) => {
|
||||
const metric = METRIC_LABELS[alert.metric] || alert.metric;
|
||||
const subject = alert.server_name || 'Unknown server';
|
||||
const subject = alert.server_name || t('app.incidents.unknownServer', 'Unknown server');
|
||||
const { title, detail } = fleetAlertText(t, alert, subject);
|
||||
return {
|
||||
kind: 'alert',
|
||||
key: `fleet-${alert.id}`,
|
||||
alertId: alert.id,
|
||||
title: `${metric} over limit on ${subject}`,
|
||||
title,
|
||||
subject,
|
||||
detail: `${metric} ${formatValue(alert.value)} / ${alert.threshold}`,
|
||||
detail,
|
||||
state: alert.status,
|
||||
tone: alertStatusKind(alert.status),
|
||||
impact: alert.severity,
|
||||
metric,
|
||||
metric: alert.metric,
|
||||
value: alert.value,
|
||||
threshold: alert.threshold,
|
||||
when: alert.created_at,
|
||||
@@ -305,7 +305,7 @@ export default function Incidents() {
|
||||
|
||||
return [...fromActive, ...fromFleet, ...fromIncidents, ...fromHistory]
|
||||
.sort((a, b) => new Date(b.when || 0) - new Date(a.when || 0));
|
||||
}, [incidents, activeAlerts, alertHistory, fleetAlerts, monitorsById]);
|
||||
}, [incidents, activeAlerts, alertHistory, fleetAlerts, monitorsById, t]);
|
||||
|
||||
// Search only. The active/resolved split used to live here as a segment
|
||||
// filter; it is a column rule now, applied inside the table.
|
||||
|
||||
Reference in new issue
Block a user