Server statistics, multi-view record commands, and rc.3 fixes
Added:
- Server statistics: named's memory and CPU (from /proc), cache size,
sockets and client counts against their limits (statistics channel,
rndc status), host memory and load, and free disk space. Memory and
CPU are kept over time as gauges (sum and sample count, averaged in
timelines). Statistics page Server section, jarl stats server,
GET /stats/server.
- jarl record add|set|delete|disable|enable --view a,b | all: the same
change in each view, applied as one changeset.
- Blocking can be paused until resumed (jarl block pause indefinitely,
"Until I resume", {"indefinitely": true}).
Changed:
- List options (allow-transfer, forwarders, ...) get their braces added
when given without them.
- The dashboard lists named-checkconf's warnings about the running
configuration; the CLI prints them after an apply.
- Stats page: tile and card rows fill their width, and the queries and
failed answers charts share margins so their time axes line up.
- Dashboard: options have their own tile.
- 24-hour times in the web UI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b097e10bef
commit
7be0762f48
48 files changed
+1346
-128
No files matched your search
@@ -125,10 +125,19 @@ export function legend(series) {
|
||||
h("li", null, h("span", { class: "key line s" + s.slot }), s.label)));
|
||||
}
|
||||
|
||||
// endLabelWidth is the room lineChart needs for the end labels of series.
|
||||
export function endLabelWidth(series) {
|
||||
return series.length > 1 && series.length <= 4 ? Math.max(...series.map((s) => s.label.length)) * 7 + 12 : 8;
|
||||
}
|
||||
|
||||
// lineChart draws series over time. series: [{label, values, slot}] (slot
|
||||
// = categorical color 1..3); times: bucket starts (Unix seconds); step in
|
||||
// seconds. With area, the first series gets a 10% wash under its line.
|
||||
export function lineChart({ series, times, step, area = false, height = 220, endLabels = true }) {
|
||||
// gutter {left, right} sets the least room for the axis labels and the end
|
||||
// labels: charts shown one above the other pass the same gutter, so their
|
||||
// time axes line up. format formats values (axis and readout); a null
|
||||
// value is a gap in the line (no data for that time).
|
||||
export function lineChart({ series, times, step, area = false, height = 220, endLabels = true, gutter = {}, format = compact }) {
|
||||
const root = h("div", { class: "chart", tabindex: "0", "aria-label": series.map((s) => s.label).join(", ") + " over time" });
|
||||
const tip = tooltip();
|
||||
root.append(tip);
|
||||
@@ -139,12 +148,12 @@ export function lineChart({ series, times, step, area = false, height = 220, end
|
||||
if (!w || w === width) return;
|
||||
width = w;
|
||||
root.querySelector("svg")?.remove();
|
||||
const max = Math.max(1, ...series.flatMap((s) => s.values));
|
||||
const max = Math.max(1, ...series.flatMap((s) => s.values).filter((v) => v != null));
|
||||
const yTicks = niceTicks(max, 4);
|
||||
const yMax = yTicks[yTicks.length - 1];
|
||||
const labelW = Math.max(...yTicks.map((t) => compact(t).length)) * 7 + 10;
|
||||
const endW = endLabels && series.length > 1 && series.length <= 4 ? Math.max(...series.map((s) => s.label.length)) * 7 + 12 : 8;
|
||||
const m = { top: 10, right: endW, bottom: 24, left: labelW };
|
||||
const labelW = Math.max(...yTicks.map((t) => format(t).length)) * 7 + 10;
|
||||
const endW = endLabels ? endLabelWidth(series) : 8;
|
||||
const m = { top: 10, right: Math.max(endW, gutter.right || 0), bottom: 24, left: Math.max(labelW, gutter.left || 0) };
|
||||
const pw = Math.max(40, w - m.left - m.right), ph = height - m.top - m.bottom;
|
||||
const t0 = times[0], t1 = times[times.length - 1] + step;
|
||||
const x = (t) => m.left + ((t - t0) / Math.max(1, t1 - t0)) * pw;
|
||||
@@ -154,7 +163,7 @@ export function lineChart({ series, times, step, area = false, height = 220, end
|
||||
// Grid and axes.
|
||||
for (const t of yTicks) {
|
||||
s.append(svg("line", { class: t === 0 ? "axis" : "grid", x1: m.left, x2: m.left + pw, y1: y(t), y2: y(t) }));
|
||||
s.append(svg("text", { class: "tick", x: m.left - 6, y: y(t) + 4, "text-anchor": "end" }, compact(t)));
|
||||
s.append(svg("text", { class: "tick", x: m.left - 6, y: y(t) + 4, "text-anchor": "end" }, format(t)));
|
||||
}
|
||||
const { ticks, step: tstep } = timeTicks(t0, t1, Math.max(2, Math.floor(pw / 90)));
|
||||
for (const t of ticks) {
|
||||
@@ -162,17 +171,21 @@ export function lineChart({ series, times, step, area = false, height = 220, end
|
||||
}
|
||||
// Series.
|
||||
series.forEach((sr, k) => {
|
||||
const pts = sr.values.map((v, i) => [cx(i), y(v)]);
|
||||
const d = pts.map((p, i) => (i ? "L" : "M") + p[0].toFixed(1) + " " + p[1].toFixed(1)).join("");
|
||||
if (area && k === 0 && pts.length) {
|
||||
// A gap (null) starts a new piece of line.
|
||||
const pts = sr.values.map((v, i) => (v == null ? null : [cx(i), y(v)]));
|
||||
const d = pts.map((p, i) => (p ? (i && pts[i - 1] ? "L" : "M") + p[0].toFixed(1) + " " + p[1].toFixed(1) : "")).join("");
|
||||
if (area && k === 0 && pts.length && pts.every(Boolean)) {
|
||||
s.append(svg("path", { class: "area s" + sr.slot, d: d + `L${pts[pts.length - 1][0].toFixed(1)} ${y(0)}L${pts[0][0].toFixed(1)} ${y(0)}Z` }));
|
||||
}
|
||||
s.append(svg("path", { class: "line s" + sr.slot, d }));
|
||||
if (pts.length === 1) s.append(svg("circle", { class: "dot s" + sr.slot, cx: pts[0][0], cy: pts[0][1], r: 4 }));
|
||||
// A point without neighbours has no line: show it as a dot.
|
||||
pts.forEach((p, i) => {
|
||||
if (p && !pts[i - 1] && !pts[i + 1]) s.append(svg("circle", { class: "dot s" + sr.slot, cx: p[0], cy: p[1], r: 3 }));
|
||||
});
|
||||
});
|
||||
// Direct end labels when there is room and they don't collide.
|
||||
if (m.right > 8) {
|
||||
const ends = series.map((sr) => ({ label: sr.label, y: y(sr.values[sr.values.length - 1] || 0) })).sort((a, b) => a.y - b.y);
|
||||
const ends = series.map((sr) => ({ label: sr.label, y: y(sr.values.findLast((v) => v != null) || 0) })).sort((a, b) => a.y - b.y);
|
||||
if (ends.every((e, i) => i === 0 || e.y - ends[i - 1].y >= 13)) {
|
||||
for (const e of ends) s.append(svg("text", { class: "end-label", x: m.left + pw + 6, y: e.y + 4 }, e.label));
|
||||
}
|
||||
@@ -205,13 +218,13 @@ export function lineChart({ series, times, step, area = false, height = 220, end
|
||||
geom.cross.setAttribute("visibility", "visible");
|
||||
series.forEach((sr, k) => {
|
||||
geom.dots[k].setAttribute("cx", xpos);
|
||||
geom.dots[k].setAttribute("cy", geom.y(sr.values[i]));
|
||||
geom.dots[k].setAttribute("visibility", "visible");
|
||||
geom.dots[k].setAttribute("cy", geom.y(sr.values[i] ?? 0));
|
||||
geom.dots[k].setAttribute("visibility", sr.values[i] == null ? "hidden" : "visible");
|
||||
});
|
||||
tip.replaceChildren(
|
||||
h("div", { class: "tip-head" }, bucketLabel(times[i], step)),
|
||||
...series.map((sr) => h("div", { class: "tip-row" },
|
||||
h("span", { class: "key line s" + sr.slot }), h("strong", null, sr.values[i].toLocaleString()), " ", h("span", { class: "muted" }, sr.label))));
|
||||
h("span", { class: "key line s" + sr.slot }), h("strong", null, sr.values[i] == null ? "–" : format === compact ? sr.values[i].toLocaleString() : format(sr.values[i])), " ", h("span", { class: "muted" }, sr.label))));
|
||||
tip.hidden = false;
|
||||
// Keep the readout inside the chart, on the far side of the pointer.
|
||||
const left = xpos > root.clientWidth / 2 ? xpos - tip.offsetWidth - 12 : xpos + 12;
|
||||
@@ -242,12 +255,13 @@ export function lineChart({ series, times, step, area = false, height = 220, end
|
||||
|
||||
// timeTable is the table view of a line chart (busiest buckets first is
|
||||
// not useful here: keep time order, newest first).
|
||||
export function timeTable(series, times, step) {
|
||||
export function timeTable(series, times, step, format) {
|
||||
const cell = (v) => (v == null ? "–" : format ? format(v) : v.toLocaleString());
|
||||
const rows = times.map((t, i) => ({ t, i })).reverse();
|
||||
return h("div", { class: "table-wrap scroll" }, h("table", null,
|
||||
h("thead", null, h("tr", null, h("th", null, "Time"), series.map((s) => h("th", { class: "num" }, s.label)))),
|
||||
h("tbody", null, rows.map((r) => h("tr", null, h("td", null, bucketLabel(r.t, step)),
|
||||
series.map((s) => h("td", { class: "num" }, s.values[r.i].toLocaleString())))))));
|
||||
series.map((s) => h("td", { class: "num" }, cell(s.values[r.i]))))))));
|
||||
}
|
||||
|
||||
// heatmap draws average activity by weekday (rows, Monday first) and hour
|
||||
|
||||
Reference in new issue
Block a user