Files
memby/admin-ui/src/components/runtime.tsx
T

83 lines
3.1 KiB
TypeScript
Raw Normal View History

2026-08-19 14:25:44 +12:00
import { Icon } from './Icon';
import { Tag } from './ui';
import type { Tone } from '../lib/format';
import type { RuntimeHealth, RuntimeLevel, RuntimeTrend } from '../api/types';
/* The two places the process figures are read — the overview's Process card and the
Runtime page — say the same things about them, so the wording lives here rather than
twice. None of it decides anything: the verdict, its sentences and the areas to
investigate all arrive from the gateway, and this renders what it was handed. */
export const levelTone: Record<RuntimeLevel, Tone> = {
ok: 'ok',
watch: 'warn',
bad: 'bad',
};
const levelWord: Record<RuntimeLevel, string> = {
ok: 'Healthy',
watch: 'Worth watching',
bad: 'Needs attention',
};
/** RuntimeVerdict is the headline. It replaces a bare goroutine count with the answer that
* count was standing in for, and when there is something to look at it names the area
* rather than leaving an operator to work out which number matters. */
export function RuntimeVerdict({
health,
compact,
}: {
health: RuntimeHealth;
compact?: boolean;
}) {
const notes = health.notes ?? [];
return (
<div className="verdict" data-tone={levelTone[health.level] ?? 'ok'}>
<div className="verdict-head">
<Icon name={health.level === 'ok' ? 'check' : 'alert'} />
<b>{levelWord[health.level] ?? 'Unknown'}</b>
<span>{health.summary}</span>
</div>
{/* On the overview the summary is the whole of it — the card is a signpost, and the
notes belong on the page it points at. */}
{compact || notes.length === 0 ? null : (
<ul className="verdict-notes">
{notes.map((note, index) => (
<li key={`${note.area}-${index}`}>
<Tag tone={levelTone[note.level] ?? 'note'}>{note.area}</Tag>
<span>{note.message}</span>
</li>
))}
</ul>
)}
</div>
);
}
/** trendWord turns a trend into the half-sentence that sits under a figure. "Steady" is
* worth printing: it is the answer to the question the figure raises, and a tile that
* says nothing when nothing is wrong makes the operator check anyway. */
export function trendWord(trend: RuntimeTrend, format: (value: number) => string): string {
switch (trend.direction) {
case 'rising':
return `rising ${format(Math.abs(trend.perHour))} an hour`;
case 'falling':
return `falling ${format(Math.abs(trend.perHour))} an hour`;
case 'steady':
return 'steady';
default:
return 'gathering history';
}
}
/** uptime words how long the process has been up. Days matter here and seconds do not: the
* question this answers is "has it restarted", not "how long exactly". */
export function uptime(seconds: number): string {
if (!seconds || seconds < 60) return `${Math.max(0, Math.round(seconds))}s`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes} min`;
const hours = Math.floor(minutes / 60);
if (hours < 48) return `${hours}h ${minutes % 60}m`;
return `${Math.floor(hours / 24)} days`;
}