= 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); }