Public: anyone can read this; the crew can change it.
/system's btop panels draw §5's card, row and badge, and the .bt-* classes leave the markup #4186
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4166-system-s-btop-panels-draw-5-s-card-row-a → main 1 commit fork
2 files
changed +39
−15
src/web/pages/system.ts +24 −15
| @@ -24,7 +24,7 @@ import { notFoundPage, signInFirst } from "../chrome.ts"; | ||
| 24 | 24 | import { html, raw, type Html } from "../html.ts"; |
| 25 | 25 | import { page } from "../layout.ts"; |
| 26 | 26 | import { systemLiveJs } from "../system-live.ts"; |
| 27 | import { card, empty, pill } from "../ui.ts"; | |
| 27 | import { badge, card, empty, meter, pill, row } from "../ui.ts"; | |
| 28 | 28 | |
| 29 | 29 | /** A group's memory, and a cell's beside its cap and who holds the cap. */ |
| 30 | 30 | const memoryOf = (g: { rssBytes: number; capBytes: number | null; capBy: "kernel" | "watch" | null }): Html => |
| @@ -38,20 +38,29 @@ const coresOf = (n: number | null): string => (n === null ? "—" : n.toFixed(2) | ||
| 38 | 38 | const rateOf = (n: number | null): string => (n === null ? "—" : `${bytes(n)}/s`); |
| 39 | 39 | |
| 40 | 40 | /** One line of the machine card: a name, a meter, its figure and (for a core) its history. */ |
| 41 | const meterRow = (name: string, value: number | null, max: number, figure: string, history = ""): Html => | |
| 42 | html`<div class="bt-row"><span class="bt-name" title="${name}">${name}</span> | |
| 43 | <meter class="bt-bar" aria-label="${name} ${figure}" min="0" max="${String(max)}" high="${String(max * 0.85)}" value="${String(value ?? 0)}">${figure}</meter> | |
| 44 | <span class="bt-val">${figure}</span>${history ? html`<span class="bt-hist" aria-hidden="true">${history}</span>` : null}</div>`; | |
| 41 | const meterRow = (name: string, value: number | null, max: number, figure: string, history = ""): Html => { | |
| 42 | const share = max > 0 ? ((value ?? 0) / max) * 100 : 0; | |
| 43 | return row({ | |
| 44 | title: name, | |
| 45 | titleAttr: name, | |
| 46 | meta: html`${meter(share, `${name} ${figure}`, share >= 85 ? "attn" : undefined)}${history ? html` <span class="mono" aria-hidden="true">${history}</span>` : null}`, | |
| 47 | trail: html`<span class="mono">${figure}</span>`, | |
| 48 | }); | |
| 49 | }; | |
| 50 | ||
| 51 | /** A name and one figure, no bar: load, a disk's I/O, a link's rates. */ | |
| 52 | const figureRow = (name: string, figure: string, data: Record<string, string> = {}): Html => | |
| 53 | row({ title: name, trail: html`<span class="mono">${figure}</span>`, data }); | |
| 45 | 54 | |
| 46 | 55 | /** Processes grouped forge, crew, check, app, host; the client re-sorts them by CPU or memory (`system-live.ts`). */ |
| 47 | 56 | const KIND_ORDER = ["forge", "crew", "check", "app", "host"] as const; |
| 48 | 57 | function procTable(v: SystemView): Html { |
| 49 | 58 | const groups = KIND_ORDER.flatMap((k) => v.groups.filter((g) => g.kind === k)); |
| 50 | return html`<div class="table-wrap"><table class="table bt-procs" aria-label="Processes by what launched them"> | |
| 59 | return html`<div class="table-wrap"><table class="table" data-dense aria-label="Processes by what launched them"> | |
| 51 | 60 | <thead><tr><th scope="col">pid</th><th scope="col">command</th><th scope="col" class="num">cpu</th><th scope="col" class="num">mem</th><th scope="col" class="num">age</th></tr></thead> |
| 52 | 61 | ${groups.map( |
| 53 | 62 | (g) => html`<tbody data-kind="${g.kind}" data-cpu="${String(g.cpuCores ?? 0)}" data-mem="${String(g.rssBytes)}"> |
| 54 | <tr class="bt-group"><th scope="colgroup" colspan="2">${g.label} <span class="muted">${String(g.procs)} procs</span></th> | |
| 63 | <tr data-group><th scope="colgroup" colspan="2"><strong>${g.label}</strong> ${badge(`${String(g.procs)} procs`, { variant: "secondary" })}</th> | |
| 55 | 64 | <td class="num">${coresOf(g.cpuCores)}</td><td class="num">${memoryOf(g)}</td><td class="num">${span(g.oldestSeconds)}</td></tr> |
| 56 | 65 | ${g.top.map( |
| 57 | 66 | (p) => html`<tr data-cpu="${String(p.cpuCores ?? 0)}" data-mem="${String(p.rssBytes)}"><td class="mono">${String(p.pid)}</td><td class="mono">${p.comm}</td> |
| @@ -112,15 +121,15 @@ export function regions(v: SystemView): Record<string, string> { | ||
| 112 | 121 | ? html`${meterRow("mem", v.memory.usedBytes, v.memory.totalBytes, `${bytes(v.memory.usedBytes)} / ${bytes(v.memory.totalBytes)}`)} |
| 113 | 122 | ${meterRow("swap", v.memory.swapUsedBytes, v.memory.swapTotalBytes, v.memory.swapTotalBytes === 0 ? "none" : `${bytes(v.memory.swapUsedBytes)} / ${bytes(v.memory.swapTotalBytes)}`)}` |
| 114 | 123 | : null} |
| 115 | ${v.load ? html`<div class="bt-row"><span class="bt-name">load</span><span class="bt-val">${v.load.map((l) => l.toFixed(2)).join(" ")}</span></div>` : null}`, | |
| 124 | ${v.load ? figureRow("load", v.load.map((l) => l.toFixed(2)).join(" ")) : null}`, | |
| 116 | 125 | temps: html`${v.temps.map((t) => `${t.chip} ${t.label} ${t.celsius.toFixed(0)}°C`).join(" · ")}`, |
| 117 | 126 | "sys-disks": card( |
| 118 | html`<div class="bt-grid"> | |
| 127 | html`<ul class="list"> | |
| 119 | 128 | ${v.disks.map((d) => meterRow(d.mount, d.usedBytes, d.totalBytes, `${bytes(d.usedBytes)} / ${bytes(d.totalBytes)} ${d.fs}`))} |
| 120 | ${v.io.map((d) => html`<div class="bt-row" data-io="${d.name}"><span class="bt-name">${d.name}</span><span class="bt-val">R ${rateOf(d.readBytesPerSecond)} W ${rateOf(d.writeBytesPerSecond)}</span></div>`)} | |
| 121 | ${v.net.map((n) => html`<div class="bt-row" data-net="${n.name}"><span class="bt-name">${n.name}</span><span class="bt-val">↓ ${rateOf(n.rxBytesPerSecond)} ↑ ${rateOf(n.txBytesPerSecond)}</span></div>`)} | |
| 122 | </div>`, | |
| 123 | { head: html`<h2>Disks and network</h2>` }, | |
| 129 | ${v.io.map((d) => figureRow(d.name, `R ${rateOf(d.readBytesPerSecond)} W ${rateOf(d.writeBytesPerSecond)}`, { io: d.name }))} | |
| 130 | ${v.net.map((n) => figureRow(n.name, `↓ ${rateOf(n.rxBytesPerSecond)} ↑ ${rateOf(n.txBytesPerSecond)}`, { net: n.name }))} | |
| 131 | </ul>`, | |
| 132 | { head: html`<h2>Disks and network</h2>`, pad: false }, | |
| 124 | 133 | ), |
| 125 | 134 | "sys-procs": procTable(v), |
| 126 | 135 | }; |
| @@ -171,8 +180,8 @@ export function systemPage(ctx: Ctx): Response { | ||
| 171 | 180 | <div id="sys-dup">${at("sys-dup")}</div> |
| 172 | 181 | ${card( |
| 173 | 182 | html`<div id="summary" class="row-meta">${at("summary")}</div> |
| 174 | <div class="bt-grid" id="cores" aria-label="Per-core CPU, percent, with its last readings">${at("cores")}</div> | |
| 175 | <div class="bt-grid" id="mem">${at("mem")}</div> | |
| 183 | <ul class="list" id="cores" aria-label="Per-core CPU, percent, with its last readings">${at("cores")}</ul> | |
| 184 | <ul class="list" id="mem">${at("mem")}</ul> | |
| 176 | 185 | ${caps} |
| 177 | 186 | <p class="small mono" id="temps">${at("temps")}</p>`, |
| 178 | 187 | { head: html`<h2>Machine</h2>` }, |
test/system-btop.test.ts +15
| @@ -101,6 +101,21 @@ describe("/system as btop", () => { | ||
| 101 | 101 | } |
| 102 | 102 | }); |
| 103 | 103 | |
| 104 | test("the panels are drawn with §5's rows, meters and card, and no .bt-* class remains", () => { | |
| 105 | const r = regions(view); | |
| 106 | const all = Object.values(r).join("\n"); | |
| 107 | for (const old of ["bt-bar", "bt-grid", "bt-group", "bt-hist", "bt-name", "bt-procs", "bt-row", "bt-val"]) expect(all).not.toContain(old); | |
| 108 | expect(r["cores"]).toContain('class="row"'); | |
| 109 | expect(r["cores"]).toContain('class="row-title"'); | |
| 110 | expect(r["cores"]).toContain("<meter"); | |
| 111 | expect(r["mem"]).toContain("load"); | |
| 112 | expect(r["sys-disks"]).toContain('class="card"'); | |
| 113 | expect(r["sys-disks"]).toContain('data-io="nvme0n1"'); | |
| 114 | expect(r["sys-disks"]).toContain('data-net="eth0"'); | |
| 115 | expect(r["sys-procs"]).toContain('class="table"'); | |
| 116 | expect(r["sys-procs"]).toContain("2 procs"); | |
| 117 | }); | |
| 118 | ||
| 104 | 119 | test("diskstats keep whole devices only", () => { |
| 105 | 120 | const text = " 259 0 nvme0n1 10 0 8 0 20 0 16 0\n 259 1 nvme0n1p1 1 0 2 0 1 0 2 0\n 7 0 loop0 1 0 2 0 0 0 0 0\n"; |
| 106 | 121 | expect(parseDiskstats(text, (n) => n === "nvme0n1")).toEqual([{ name: "nvme0n1", read: 8 * 512, write: 16 * 512 }]); |
new above
offline