Highlight critical utilization on the system page (v0.9.6)

The Current cell of CPU/RAM/disk used turns orange from 80% of the
total and red from 90%. UiFormats.utilizationClass and the mirrored
utilizationClass in gittally.js apply the same thresholds; unavailable
metrics (n/a) are never highlighted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-10 19:08:23 +02:00
co-authored by Claude Fable 5
parent 428d1a06cb
commit f2d4dbfda0
6 changed files with 171 additions and 4 deletions
@@ -65,6 +65,29 @@ object UiFormats {
}
fun timeOfDay(instant: Instant): String = timeOfDayFormat.format(instant)
/**
* CSS class highlighting a critical utilization: `metric-warn` from 80% of [total],
* `metric-crit` from 90%, empty below or when either value is unavailable.
* `gittally.js` (`utilizationClass`) must apply the same thresholds.
*/
fun utilizationClass(
used: Double?,
total: Double?,
): String {
if (used == null || total == null || !used.isFinite() || !total.isFinite() || total <= 0) {
return ""
}
val ratio = used / total
return when {
ratio >= UTILIZATION_CRIT -> "metric-crit"
ratio >= UTILIZATION_WARN -> "metric-warn"
else -> ""
}
}
private const val UTILIZATION_WARN = 0.80
private const val UTILIZATION_CRIT = 0.90
}
/** One row of the latest/history build tables; [latestGreenUrl] only on the branches view. */
@@ -161,12 +184,15 @@ data class MetricRowView(
val min: String,
val max: String,
val avg: String,
/** `metric-warn`/`metric-crit` when the current value is a critical share of its total. */
val currentClass: String = "",
) {
companion object {
fun from(
key: String,
label: String,
aggregate: MetricAggregate?,
total: Double? = null,
) = MetricRowView(
key = key,
label = label,
@@ -174,6 +200,7 @@ data class MetricRowView(
min = UiFormats.metric(aggregate?.min),
max = UiFormats.metric(aggregate?.max),
avg = UiFormats.metric(aggregate?.avg),
currentClass = UiFormats.utilizationClass(aggregate?.current, total),
)
}
}
@@ -191,11 +218,11 @@ data class SystemMetricsView(
SystemMetricsView(
rows =
listOf(
MetricRowView.from("cpuUsed", "CPU used (cores)", metrics.cpuUsed),
MetricRowView.from("cpuUsed", "CPU used (cores)", metrics.cpuUsed, metrics.cpuCount.toDouble()),
MetricRowView.from("cpuIdle", "CPU idle (cores)", metrics.cpuIdle),
MetricRowView.from("ramUsedGib", "RAM used (GiB)", metrics.ramUsedGib),
MetricRowView.from("ramUsedGib", "RAM used (GiB)", metrics.ramUsedGib, metrics.ramTotalGib),
MetricRowView.from("ramFreeGib", "RAM free (GiB)", metrics.ramFreeGib),
MetricRowView.from("diskUsedGib", "Disk used (GiB)", metrics.diskUsedGib),
MetricRowView.from("diskUsedGib", "Disk used (GiB)", metrics.diskUsedGib, metrics.diskTotalGib),
MetricRowView.from("diskFreeGib", "Disk free (GiB)", metrics.diskFreeGib),
MetricRowView.from("repoSizeGib", "Repo size (GiB)", metrics.repoSizeGib),
),
+3
View File
@@ -95,6 +95,9 @@ tbody.is-stale { opacity: 0.55; }
/* system metrics */
#system-table { min-width: 640px; }
.num { text-align: right; font-variant-numeric: tabular-nums; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; }
/* critical utilization of the current value: warn from 80% of the total, critical from 90% */
.metric-warn { background: var(--interrupted-bg); color: var(--interrupted-text); font-weight: 700; }
.metric-crit { background: var(--failed-bg); color: var(--failed-text); font-weight: 700; }
th.num { font-family: inherit; font-size: 12px; }
.meta { margin: 14px 0 0; color: var(--muted); font-size: 13px; display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: 6px; }
+27
View File
@@ -433,6 +433,24 @@ function initCurrentBuilds() {
// ---- system metrics ----------------------------------------------------------
/** The total each utilization metric is compared against for the critical highlighting. */
const UTILIZATION_TOTALS = { cpuUsed: "cpuCount", ramUsedGib: "ramTotalGib", diskUsedGib: "diskTotalGib" };
/** Same thresholds as UiFormats.utilizationClass: warn from 80% of the total, critical from 90%. */
function utilizationClass(used, total) {
if (used == null || total == null || !(total > 0)) {
return "";
}
const ratio = used / total;
if (ratio >= 0.90) {
return "metric-crit";
}
if (ratio >= 0.80) {
return "metric-warn";
}
return "";
}
/** The metric rows are fixed, so only the cell texts are updated — never rebuilt. */
function initSystemTable() {
const table = document.getElementById("system-table");
@@ -454,6 +472,15 @@ function initSystemTable() {
row.querySelectorAll("[data-field]").forEach((cell) => {
cell.textContent = formatMetric(aggregate ? aggregate[cell.dataset.field] : null);
});
const currentCell = row.querySelector('[data-field="current"]');
if (currentCell) {
const totalField = UTILIZATION_TOTALS[row.dataset.metric];
const cssClass = totalField
? utilizationClass(aggregate ? aggregate.current : null, metrics[totalField])
: "";
currentCell.classList.toggle("metric-warn", cssClass === "metric-warn");
currentCell.classList.toggle("metric-crit", cssClass === "metric-crit");
}
});
setText("info-cpu-count", metrics.cpuCount != null ? metrics.cpuCount + " cores" : "n/a");
setText("info-ram-total", metrics.ramTotalGib != null ? formatMetric(metrics.ramTotalGib) + " GiB" : "n/a");
+1 -1
View File
@@ -19,7 +19,7 @@
<tbody id="system-rows">
<tr th:each="row : ${metrics.rows}" th:attr="data-metric=${row.key}">
<td data-label="Metric" th:text="${row.label}">CPU used (cores)</td>
<td class="num" data-label="Current" data-field="current" th:text="${row.current}">0.42</td>
<td class="num" data-label="Current" data-field="current" th:classappend="${row.currentClass}" th:text="${row.current}">0.42</td>
<td class="num" data-label="Min" data-field="min" th:text="${row.min}">0.10</td>
<td class="num" data-label="Max" data-field="max" th:text="${row.max}">3.20</td>
<td class="num" data-label="Avg" data-field="avg" th:text="${row.avg}">0.80</td>