323 lines
16 KiB
PHP
323 lines
16 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.
|
|
//
|
|
// EXPORTS
|
|
// Declaration vv_mon_board(), vv_mon_board_cards(), vv_mon_board_total()
|
|
// — the one array every other function here reads. Change the layout by editing
|
|
// that declaration, never by editing what follows.
|
|
// Geometry vv_mon_rung_min(), vv_mon_rung_rows(), vv_mon_rung_rowdiv(),
|
|
// vv_mon_rung_overflows(), vv_mon_absent_class()
|
|
// — derive the column ladder and each card's span from the declaration.
|
|
// vv_mon_rung_overflows() is the one that asks whether a rung is already taller
|
|
// than one screen, which is when the row-height cap stops helping and starts
|
|
// clipping.
|
|
// Emitters vv_mon_board_css(), vv_mon_absorb_css(), vv_mon_autorows_css(),
|
|
// vv_mon_board_js()
|
|
// — vv_mon_autorows_css() is the hatch for an overflowing rung: rows sized to
|
|
// content, and the overflow clamps lifted off the cards so they can use it.
|
|
// Validation vv_mon_board_check()
|
|
// — the only one that reports rather than renders; see OPERATIONAL SAFEGUARDS.
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
|
|
// 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 — and the line between two behaviours, not one
|
|
// rule. A rung with no more rows than this fits a screen: the row-height cap divides the viewport
|
|
// by the rung's own row count, or by this when the rung has more, so the board fills the screen
|
|
// and no card grows past its share. A rung with MORE rows than this cannot fit however its rows
|
|
// are sized, so the cap is dropped there and rows size to content instead — see
|
|
// vv_mon_rung_overflows(). Capping a board that scrolls regardless buys nothing and clips every
|
|
// card to pay for it. This board is 4 rows at eight columns and 8 at four, so both cases are live.
|
|
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);
|
|
}
|
|
|
|
// Whether this rung is already taller than one screen. The row-height cap means "no card taller
|
|
// than 1/VV_MON_ROWS_PER_SCREEN of the screen", which is only worth paying for on a rung that
|
|
// fits a screen — there it is what keeps the board from scrolling. On a rung with more rows than
|
|
// the cap the page scrolls no matter what, so the cap buys nothing and only cuts each card's
|
|
// content off. At four columns this board is eight rows: an 11" tablet in landscape was being
|
|
// given 126px cards on a board two screens tall.
|
|
function vv_mon_rung_overflows(int $cols): bool {
|
|
return vv_mon_rung_rows($cols) > VV_MON_ROWS_PER_SCREEN;
|
|
}
|
|
|
|
// Rows sized by content instead of by the cap, for a rung that overflows the screen anyway.
|
|
// Mirrors the phone hatch in css/varaverk.css: the cards have to give up overflow:hidden too, or
|
|
// they keep clipping at a height nothing is constraining any more.
|
|
function vv_mon_autorows_css(): string {
|
|
return '#vv-monitor{grid-auto-rows:auto;}'
|
|
. '#vv-monitor .vv-card{overflow:visible;}'
|
|
. '#vv-monitor .vv-card>div{overflow-y:visible;min-height:auto;}';
|
|
}
|
|
|
|
// 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";
|
|
if (vv_mon_rung_overflows($top)) $out .= vv_mon_autorows_css() . "\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;}";
|
|
}
|
|
|
|
if (vv_mon_rung_overflows($cols)) $out .= vv_mon_autorows_css();
|
|
$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);
|
|
}
|