Files
Varaverk/Plugin/unraid/include/monitor_board.php
T

281 lines
13 KiB
PHP

<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// The Monitor board declared as data. One array below decides which cards exist, what width
// each one takes, and what order they sit in. Everything about the page's layout — the column
// ladder, the span clamps at each rung, the row-height cap, the compensation when a card is
// absent — is generated from that array by vv_mon_board_css().
//
// pages/monitor.php holds the card bodies and nothing about where they go.
//
// DESIGN PRINCIPLES
// Powers of two, all the way down.
// Column counts are 8, 4, 2, 1 and every span is 1, 2, 4 or 8. That pairing is what makes
// the board tile with no holes at every width without a single hand-placed card: 32 span
// units divide into 8 columns as 4 rows, into 4 as 8 rows, into 2 as 16, into 1 as 32.
// A rung of 6 or 10 columns does not divide a span of 4, which is why the previous ladder
// needed a per-breakpoint override for every wide card and still left holes.
//
// Breakpoints are arithmetic, not taste.
// A column count is viable exactly when the cards still fit:
//
// window >= N * card-floor + (N-1) * gap + wrap-padding
//
// vv_mon_rung_min() is that line. Change VV_MON_CARD_FLOOR and every breakpoint moves with
// it. No number in the generated CSS is chosen by eye.
//
// Order comes from this file, not from document order.
// Each card is emitted with a CSS `order`, which grid auto-placement honours. Moving a card
// on the board is moving a line in this array; the markup in pages/monitor.php never has to
// be cut and pasted, which is how the old board accumulated hand-placed columns.
//
// A card that disappears has to give its width to someone.
// Only one card on this board is conditional — the second GPU, hidden by JS on a one-GPU
// host. Absent, its row summed to 7 of 8 and the board carried a hole on HOST2 that nothing
// reported. 'absorbs' names the card whose width is taken over, and the JS toggles one class
// on the container to apply it.
//
// OPERATIONAL SAFEGUARDS
// Row sums are checked, not assumed. vv_mon_board_check() verifies every declared row adds to
// the top rung and that every span is a power of two; failures are handed to the page, which
// reports them in the browser rather than rendering a quietly broken board.
//
// The declaration and the markup are cross-checked at runtime. vv_mon_board_js() ships the
// declared ids to the page, which compares them against the cards actually present and flags
// either direction — a card in the markup nobody declared, or a declaration with no card.
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// The narrowest a card may be drawn. Every breakpoint is a function of this number — see
// vv_mon_rung_min(). 160 rather than the 200 the cards carried before: at 200 the 8-column rung
// needed 1704px, which put a 1720px half-screen 16px inside the margin and dropped a 1327px
// tablet to 4 columns. 160 moves that rung to 1384 and gives both real headroom.
const VV_MON_CARD_FLOOR = 160;
// Below three columns the constraint stops being the card and starts being the content: a label
// and its value need about 240px before they wrap into nonsense. A phone in portrait is 393px, so
// this is what keeps it at one column instead of two 180px cards.
const VV_MON_PHONE_FLOOR = 240;
const VV_MON_GAP = 12; // grid gap, must match the gap in #vv-monitor
const VV_MON_PAD = 20; // horizontal padding the Unraid page wrapper takes off the viewport
// The column ladder, widest first.
const VV_MON_RUNGS = [8, 4, 2, 1];
// Cap on how many rows may share one screen height. The row-height cap divides the viewport by
// this, so a card can never grow past a quarter of the screen and push the rest off it. Rungs
// with fewer rows than this divide by their own row count instead and fill the screen.
const VV_MON_ROWS_PER_SCREEN = 4;
// ── The board ─────────────────────────────────────────────────────────────────────────────────
// Grouped by the row each group forms at the top rung. The grouping is documentation and a
// checkable invariant — every group must sum to 8 — not placement: cards are auto-placed in the
// flattened order below, which is what lets a narrower rung re-cut them into different rows.
function vv_mon_board(): array {
return [
// The host itself.
'Host' => [
['id' => 'vv-system', 'span' => 1],
['id' => 'vv-ups-card', 'span' => 1],
['id' => 'vv-cpu', 'span' => 2],
['id' => 'vv-memory', 'span' => 2],
['id' => 'vv-network', 'span' => 2],
],
// What is running on it, and who is covering it.
'Workload' => [
['id' => 'vv-scripts-card', 'span' => 1],
['id' => 'vv-fallback', 'span' => 1],
['id' => 'vv-partner', 'span' => 2],
['id' => 'vv-docker-folders', 'span' => 4],
],
// The media path, from the sync that feeds it to the sessions coming out of it.
'Media' => [
['id' => 'vv-rsync-card', 'span' => 1],
['id' => 'vv-gpu-card', 'span' => 1],
['id' => 'vv-gpu1-card', 'span' => 1],
// Takes the second GPU's column on a one-GPU host — see DESIGN PRINCIPLES.
['id' => 'vv-transcode', 'span' => 1, 'absorbs' => 'vv-gpu1-card'],
['id' => 'vv-streams', 'span' => 4],
],
// Storage and the things that watch it.
'Storage' => [
['id' => 'vv-watchdog-card', 'span' => 1],
['id' => 'vv-parity-card', 'span' => 1],
['id' => 'vv-storage-card', 'span' => 2],
['id' => 'vv-array-card', 'span' => 4],
],
];
}
// The board in placement order, one flat list.
function vv_mon_board_cards(): array {
$flat = [];
foreach (vv_mon_board() as $row => $cards) {
foreach ($cards as $card) {
$card['row'] = $row;
$flat[] = $card;
}
}
return $flat;
}
function vv_mon_board_total(): int {
$total = 0;
foreach (vv_mon_board_cards() as $card) $total += $card['span'];
return $total;
}
// The narrowest window at which $cols columns still hold cards of at least the floor width.
function vv_mon_rung_min(int $cols): int {
$floor = ($cols <= 2) ? VV_MON_PHONE_FLOOR : VV_MON_CARD_FLOOR;
return $cols * $floor + ($cols - 1) * VV_MON_GAP + VV_MON_PAD;
}
// How many rows the board occupies at a given column count, and the divisor the row-height cap
// uses there. They differ once the board is taller than one screen: at 4 columns the board is 8
// rows and the page is meant to scroll, with each card still a readable quarter-screen tall.
// Counted by walking the same sparse auto-placement the browser does, not as total spans over
// columns. Those two agree only while nothing is clamped: at two columns a span-4 card occupies
// two units, not four, so the board is 13 rows there and the division says 16.
function vv_mon_rung_rows(int $cols): int {
$row = 0;
$col = 0;
foreach (vv_mon_board_cards() as $card) {
$span = min($card['span'], $cols);
if ($col + $span > $cols) { $row++; $col = 0; }
$col += $span;
if ($col >= $cols) { $row++; $col = 0; }
}
return $col === 0 ? $row : $row + 1;
}
function vv_mon_rung_rowdiv(int $cols): int {
return min(vv_mon_rung_rows($cols), VV_MON_ROWS_PER_SCREEN);
}
// The class the page puts on #vv-monitor when a card is not rendered, so the generated absorb
// rules can fire. Derived from the id on both sides — PHP writes the rule, JS writes the class —
// so the two can never drift apart by a typo.
function vv_mon_absent_class(string $id): string {
return 'vv-absent-' . $id;
}
// Problems with the declaration itself, as human-readable lines. Empty means the board is sound.
function vv_mon_board_check(): array {
$problems = [];
$top = VV_MON_RUNGS[0];
$seen = [];
foreach (vv_mon_board() as $row => $cards) {
$sum = 0;
foreach ($cards as $card) {
$span = $card['span'];
if ($span < 1 || ($span & ($span - 1)) !== 0) {
$problems[] = "{$card['id']}: span $span is not a power of two";
}
if ($span > $top) {
$problems[] = "{$card['id']}: span $span exceeds the $top-column board";
}
if (isset($seen[$card['id']])) {
$problems[] = "{$card['id']}: declared twice";
}
$seen[$card['id']] = true;
$sum += $span;
}
if ($sum !== $top) {
$problems[] = "row '$row' sums to $sum, not $top";
}
}
foreach (vv_mon_board_cards() as $card) {
if (isset($card['absorbs']) && !isset($seen[$card['absorbs']])) {
$problems[] = "{$card['id']}: absorbs '{$card['absorbs']}', which is not on the board";
}
}
return $problems;
}
// ── Generated CSS ─────────────────────────────────────────────────────────────────────────────
// Everything positional. The static half — display:grid, the gap, the row-height tiers — lives in
// css/varaverk.css and reads --vv-cols, --vv-sp and --vv-rowdiv from here.
function vv_mon_board_css(): string {
$cards = vv_mon_board_cards();
$top = VV_MON_RUNGS[0];
$out = '';
$out .= "#vv-monitor{--vv-cols:$top;--vv-rowdiv:" . vv_mon_rung_rowdiv($top) . ";}\n";
$order = 0;
foreach ($cards as $card) {
$order++;
$out .= "#{$card['id']}{order:$order;--vv-sp:{$card['span']};}\n";
}
$out .= vv_mon_absorb_css($top);
foreach (VV_MON_RUNGS as $cols) {
if ($cols === $top) continue;
// One below the next rung up: the band this column count owns ends where that one begins.
$max = vv_mon_rung_min($cols * 2) - 1;
$out .= "@media (max-width:{$max}px){";
$out .= "#vv-monitor{--vv-cols:$cols;--vv-rowdiv:" . vv_mon_rung_rowdiv($cols) . ";}";
if ($cols === 1) {
// Every span at once, and through the child selector rather than per id: it outranks
// the per-card rules above on specificity, so nothing can leave a span behind and
// make the grid invent an implicit column.
$out .= "#vv-monitor>.vv-card{--vv-sp:1;}";
} else {
$wide = [];
foreach ($cards as $card) {
if ($card['span'] > $cols) $wide[] = '#' . $card['id'];
}
if ($wide) $out .= implode(',', $wide) . "{--vv-sp:$cols;}";
}
$out .= vv_mon_absorb_css($cols);
$out .= "}\n";
}
return $out;
}
// Absorb rules for one rung. Higher specificity than both the per-card span and the one-column
// child selector, which is why every rung has to restate them — at one column an unclamped
// absorb would be the only span left above 1.
function vv_mon_absorb_css(int $cols): string {
$out = '';
foreach (vv_mon_board_cards() as $card) {
if (!isset($card['absorbs'])) continue;
$donor = null;
foreach (vv_mon_board_cards() as $other) {
if ($other['id'] === $card['absorbs']) { $donor = $other; break; }
}
if ($donor === null) continue;
$span = min($card['span'] + $donor['span'], $cols);
$cls = vv_mon_absent_class($donor['id']);
$out .= "#vv-monitor.$cls #{$card['id']}{--vv-sp:$span;}";
}
return $out;
}
// What the page's runtime guard needs: the declared ids in order, the absent-class names for the
// conditional cards, and any problem the declaration already knows about.
function vv_mon_board_js(): string {
$ids = [];
$absent = [];
foreach (vv_mon_board_cards() as $card) {
$ids[] = $card['id'];
if (isset($card['absorbs'])) {
$absent[$card['absorbs']] = vv_mon_absent_class($card['absorbs']);
}
}
return json_encode([
'ids' => $ids,
'absent' => $absent,
'problems' => vv_mon_board_check(),
], JSON_UNESCAPED_SLASHES);
}