Declare the Monitor board in one place, so a card's width and position stop living in eighteen inline styles and four media queries

This commit is contained in:
Gmer4Lfe
2026-08-23 00:44:30 -04:00
parent bd13bdc6e1
commit fd1ab58598
4 changed files with 444 additions and 65 deletions
+11 -1
View File
@@ -103,7 +103,17 @@ once, and the endpoint, the cache writer, and the page all pick it up together.
|-------|-------|------| |-------|-------|------|
| `pages/` | 11 | One per WebGUI tab — monitor, docker, arrs, fallback, watchdog, rsync, partnership, scheduler, auth, settings, setup | | `pages/` | 11 | One per WebGUI tab — monitor, docker, arrs, fallback, watchdog, rsync, partnership, scheduler, auth, settings, setup |
| `api/` | 50 | JSON endpoints the pages poll, plus action endpoints (run a script, stop a job, toggle a flag) | | `api/` | 50 | JSON endpoints the pages poll, plus action endpoints (run a script, stop a job, toggle a flag) |
| `include/` | 16 | Shared builders and helpers — `vv_monitor_*`, `vv_arrs_*`, `vv_docker_*`, config read/write, auth | | `include/` | 27 | Shared builders and helpers — `vv_monitor_*`, `vv_arrs_*`, `vv_docker_*`, config read/write, auth |
**The Monitor board is declared, not laid out.** `include/monitor_board.php` holds one array
naming every card on the Monitor grid, its width and its order, and generates the whole layout
from it — the column ladder, the span clamps at each width, the row-height cap, and the
compensation when a conditional card is absent. Column counts are 8/4/2/1 and spans are 1/2/4/8,
which is what lets the board re-cut itself at any width with no holes and no hand-placed card.
Breakpoints are arithmetic over `VV_MON_CARD_FLOOR`, never chosen by eye. `pages/monitor.php`
carries the card bodies and nothing about where they go; moving a card is moving a line in that
array. The page cross-checks the declaration against the cards that actually rendered and says so
in the browser if they disagree.
**Caching.** Several endpoints serve from `$VV_CACHE_DIR` (`/tmp/varaverk/api`, tmpfs) rather than hitting live **Caching.** Several endpoints serve from `$VV_CACHE_DIR` (`/tmp/varaverk/api`, tmpfs) rather than hitting live
APIs on every page view, refreshed by `Tools/api_cache_writer.sh`. `?live=1` bypasses the APIs on every page view, refreshed by `Tools/api_cache_writer.sh`. `?live=1` bypasses the
+79 -34
View File
@@ -190,7 +190,32 @@ body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5r
.vv-nb-settings { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; .vv-nb-settings { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
padding: 5px 10px; background: #101010; border-bottom: 1px solid #1a1a1a; } padding: 5px 10px; background: #101010; border-bottom: 1px solid #1a1a1a; }
/* ── Monitor: locked row height + scrollable cards ────────────────────────── */ /* ── Monitor: the board ───────────────────────────────────────────────────────
Only the half of the layout that is the same at every width lives here. Which cards exist,
how wide each one is, what order they sit in, how many columns the board has at this width
and how tall a row may be are all generated by include/monitor_board.php and arrive as
--vv-cols, --vv-sp and --vv-rowdiv. Do not put a column count or a breakpoint in this file;
they are arithmetic over the card floor, and that arithmetic has one home.
minmax(0, 1fr), not 1fr: bare 1fr means minmax(auto, 1fr), and that auto floor resolves to the
larger of min-content and min-width — one card holding an unbreakable string would push its
column past its share and drag the grid out of the container. The row axis carried this guard
for years; the column axis never did.
min-width: 0 on the cards for the same reason, overriding the 200px .vv-card floor the flex
layouts on other pages need. On this board the ladder is what guarantees a card's width, so a
card defending its own floor can only overflow the track the ladder just sized for it. */
#vv-monitor {
display: grid;
grid-template-columns: repeat(var(--vv-cols, 8), minmax(0, 1fr));
gap: 12px;
width: 100%;
box-sizing: border-box;
}
#vv-monitor > .vv-card {
grid-column: span var(--vv-sp, 1);
min-width: 0;
}
/* Cards on the monitor grid are flex columns — h3 pins, body scrolls */ /* Cards on the monitor grid are flex columns — h3 pins, body scrolls */
#vv-monitor .vv-card { #vv-monitor .vv-card {
@@ -213,29 +238,38 @@ body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5r
/* Dynamic row heights capped per screen tier — rows size to content, never exceed the cap. /* Dynamic row heights capped per screen tier — rows size to content, never exceed the cap.
minmax(0, Xpx): track is content-driven but capped; align-items:stretch makes all cards minmax(0, Xpx): track is content-driven but capped; align-items:stretch makes all cards
in a row fill the track, so short cards (Pools, Watchdog) match tall ones (Array). in a row fill the track, so short cards (Pools, Watchdog) match tall ones (Array).
Breakpoints are viewport height (after browser chrome), not screen height. */ Breakpoints are viewport height (after browser chrome), not screen height.
The divisor is --vv-rowdiv, not a literal 4. It used to be 4 because the board happened to be
four rows at eight columns, so the two numbers were the same by accident and only one of them
moved when the column count changed. monitor_board.php derives it as min(rows at this rung,
VV_MON_ROWS_PER_SCREEN): the cap means "no card taller than this fraction of the screen", and a
board short enough to fit divides by its own row count and fills the screen instead. */
/* ~720p (viewport ≤ 700px) */
@media (min-width: 481px) and (max-height: 700px) {
#vv-monitor { grid-auto-rows: minmax(0, calc((100vh - 160px) / 4)); }
}
/* ~1080p (viewport 7011100px) */ /* ~1080p (viewport 7011100px) */
@media (min-width: 481px) and (min-height: 701px) and (max-height: 1100px) { @media (min-height: 701px) and (max-height: 1100px) {
#vv-monitor { grid-auto-rows: minmax(0, calc((100vh - 240px) / 4)); } #vv-monitor { grid-auto-rows: minmax(0, calc((100vh - 240px) / var(--vv-rowdiv, 4))); }
} }
/* ~1440p (viewport 11011450px) — calibrated on 15" 1440p display */ /* ~1440p (viewport 11011450px) — calibrated on 15" 1440p display */
@media (min-width: 481px) and (min-height: 1101px) and (max-height: 1450px) { @media (min-height: 1101px) and (max-height: 1450px) {
#vv-monitor { grid-auto-rows: minmax(0, calc((100vh - 335px) / 4)); } #vv-monitor { grid-auto-rows: minmax(0, calc((100vh - 335px) / var(--vv-rowdiv, 4))); }
} }
/* ~4K (viewport > 1450px) */ /* ~4K (viewport > 1450px) */
@media (min-width: 481px) and (min-height: 1451px) { @media (min-height: 1451px) {
#vv-monitor { grid-auto-rows: minmax(0, calc((100vh - 500px) / 4)); } #vv-monitor { grid-auto-rows: minmax(0, calc((100vh - 500px) / var(--vv-rowdiv, 4))); }
} }
/* Mobile: natural heights, let page scroll */ /* Natural heights, page scrolls. Height is what makes a phone in landscape unusable, not width:
@media (max-width: 480px) { this hatch was keyed to max-width:480 alone, so portrait (393px wide) got content-sized rows and
#vv-monitor { grid-auto-rows: auto !important; } worked, while landscape (851x393) missed it and inherited the four-row cap — 58px cards with
#vv-monitor .vv-card { overflow: visible !important; } overflow:hidden and scrollbars disabled. Nothing was visible and nothing said so.
The width half is the two-column rung from monitor_board.php (below 696px). It is written as a
literal here because a media query cannot read a custom property; if VV_MON_CARD_FLOOR changes,
this number and the AI row's two below are the only three places that have to follow it. */
@media (max-width: 695px), (max-height: 700px) {
#vv-monitor { grid-auto-rows: auto; }
#vv-monitor .vv-card { overflow: visible; }
#vv-monitor .vv-card > div { overflow-y: visible; min-height: auto; } #vv-monitor .vv-card > div { overflow-y: visible; min-height: auto; }
} }
@@ -251,13 +285,17 @@ body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5r
default stays start so a card added later has to say so. */ default stays start so a card added later has to say so. */
#vv-monitor-ai { #vv-monitor-ai {
display: grid; display: grid;
grid-template-columns: repeat(8, 1fr); grid-template-columns: repeat(8, minmax(0, 1fr));
gap: 12px; gap: 12px;
margin-top: 12px; margin-top: 12px;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
align-items: start; align-items: start;
} }
/* Same reason as the board above: the ladder guarantees the width, so a card defending the 200px
.vv-card floor can only overflow the track it was given. At the four-column rung's own minimum
(696px) a column is 160px, and that floor would have pushed this row out of the page. */
#vv-monitor-ai > .vv-card { min-width: 0; }
/* Explicit placement, two rows. The left column carries AI over Tokens; Conversations and the /* Explicit placement, two rows. The left column carries AI over Tokens; Conversations and the
Assistant span both rows, so the Assistant sets the height and the two stacked cards divide Assistant span both rows, so the Assistant sets the height and the two stacked cards divide
@@ -354,8 +392,14 @@ body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5r
#vv-ai-tokens-card .vv-ai-tok-head:first-child { margin-top: 0; } #vv-ai-tokens-card .vv-ai-tok-head:first-child { margin-top: 0; }
/* Medium width — 4 columns, mirroring #vv-monitor's own breakpoint. The 1/2/5 spans do not /* Medium width — 4 columns, mirroring #vv-monitor's own breakpoint. The 1/2/5 spans do not
survive the narrower grid: 5 of 4 would silently overflow the track. */ survive the narrower grid: 5 of 4 would silently overflow the track.
@media (max-width: 1024px) {
1383 and 695 below are the four- and two-column rungs from include/monitor_board.php, minus
one. This row keeps its own placement — the 1/2/5 split is not a power of two and does not want
to be — but it cannot keep its own breakpoints: at any width where these two disagree the board
above is four columns while this row is still eight, and the page visibly stops being one
board. If VV_MON_CARD_FLOOR moves, these move with it. */
@media (max-width: 1383px) {
#vv-monitor-ai { grid-template-columns: repeat(4, 1fr) !important; } #vv-monitor-ai { grid-template-columns: repeat(4, 1fr) !important; }
#vv-ai-stats-card { grid-column: 1 / span 1 !important; grid-row: 1 !important; } #vv-ai-stats-card { grid-column: 1 / span 1 !important; grid-row: 1 !important; }
#vv-ai-tokens-card { grid-column: 1 / span 1 !important; grid-row: 2 !important; } #vv-ai-tokens-card { grid-column: 1 / span 1 !important; grid-row: 2 !important; }
@@ -365,10 +409,14 @@ body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5r
#vv-ai-assistant-card { grid-column: 1 / span 4 !important; grid-row: 3 !important; } #vv-ai-assistant-card { grid-column: 1 / span 4 !important; grid-row: 3 !important; }
} }
/* Phone — single column, natural heights, page scrolls. Matching heights is meaningless once /* Narrow — single column, natural heights, page scrolls. Matching heights is meaningless once
the cards are stacked, and the inner scroll has to go with it: on a page that already scrolls, the cards are stacked, and the inner scroll has to go with it: on a page that already scrolls,
a scroll region inside it is a trap for a thumb. */ a scroll region inside it is a trap for a thumb.
@media (max-width: 480px) {
695, where the board above drops to two columns, rather than the 480 this used to use. There is
no two-column layout for this row and there should not be: its 1/1/3/4 split at four columns is
already the narrowest arrangement in which the Assistant transcript is readable. */
@media (max-width: 695px) {
#vv-monitor-ai { grid-template-columns: 1fr !important; } #vv-monitor-ai { grid-template-columns: 1fr !important; }
/* grid-row has to be released along with grid-column. The explicit rows above are the whole /* grid-row has to be released along with grid-column. The explicit rows above are the whole
reason Tokens sits under AI, and left in place on a single-column grid they would stack reason Tokens sits under AI, and left in place on a single-column grid they would stack
@@ -389,14 +437,14 @@ body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5r
.vv-cpu-core { min-width: 7px !important; } .vv-cpu-core { min-width: 7px !important; }
} }
@media (max-width: 1024px) { /* The four-column rung. Nothing about placement is here any more: the column count and every
#vv-monitor { grid-template-columns: repeat(4, 1fr) !important; } span come from the generated ladder, and the six !important overrides this block used to carry
#vv-docker { grid-column: span 4 !important; } existed only to out-shout the inline styles the cards no longer have. Two of them
/* Reset explicit placements so cards reflow in the 4-col grid */ (#vv-storage-card, #vv-array-card) were resetting hand-placed column indexes — 3/span 2 and
#vv-docker-folders { grid-column: span 4 !important; } 5/span 4 — which is the pair that made the grid invent implicit columns at any count but eight.
#vv-parity-card { grid-column: auto !important; } What is left is the one thing that genuinely belongs to width: at this rung the Streams card is
#vv-storage-card { grid-column: auto !important; } half the board rather than a quarter, and its right-hand chips no longer fit. */
#vv-array-card { grid-column: auto !important; } @media (max-width: 1383px) {
/* Streams header: hide right chip group entirely, keep server badges + media type */ /* Streams header: hide right chip group entirely, keep server badges + media type */
.vv-stream-right { display: none; } .vv-stream-right { display: none; }
} }
@@ -441,10 +489,7 @@ body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5r
.vv-cpu-cores { gap: 2px !important; } .vv-cpu-cores { gap: 2px !important; }
.vv-cpu-core { min-width: 6px !important; } .vv-cpu-core { min-width: 6px !important; }
/* Monitor single-column — explicit placement cards need override too */ /* The single-column rung is generated — see include/monitor_board.php. */
#vv-monitor { grid-template-columns: 1fr !important; }
#vv-monitor > .vv-card { grid-column: 1 / -1 !important; }
#vv-docker-folders { grid-column: 1 / -1 !important; }
} }
/* Shared footer (Save Schedule left, info right) — same min-height so log card ends level with script cards */ /* Shared footer (Save Schedule left, info right) — same min-height so log card ends level with script cards */
+280
View File
@@ -0,0 +1,280 @@
<?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);
}
+72 -28
View File
@@ -87,6 +87,7 @@
// api/ai.php the AI row's turns and conversation store // api/ai.php the AI row's turns and conversation store
// ═══════════════════════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════════════════════
require_once dirname(__DIR__) . '/include/monitor.php'; require_once dirname(__DIR__) . '/include/monitor.php';
require_once dirname(__DIR__) . '/include/monitor_board.php';
require_once dirname(__DIR__) . '/include/ai_chat.php'; require_once dirname(__DIR__) . '/include/ai_chat.php';
require_once dirname(__DIR__) . '/include/docs.php'; require_once dirname(__DIR__) . '/include/docs.php';
if (vv_ai_ui_on()) vv_ai_chat_assets(); if (vv_ai_ui_on()) vv_ai_chat_assets();
@@ -104,14 +105,24 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-api-banner" style="display:none;border-radius:4px;padding:5px 10px;margin-bottom:8px;font-size:11px;"></div> <div id="vv-api-banner" style="display:none;border-radius:4px;padding:5px 10px;margin-bottom:8px;font-size:11px;"></div>
<div id="vv-monitor" style="display:grid;grid-template-columns:repeat(8,1fr);gap:12px;width:100%;box-sizing:border-box;"> <!-- Placement for every card below comes from include/monitor_board.php — spans, order, the
column ladder and the row-height cap. Nothing here carries a grid-column of its own, which is
the point: an inline style outranks the whole stylesheet, so the cards that used to carry one
could only be overridden by shouting !important at them from four media queries. -->
<style><?= vv_mon_board_css() ?></style>
<!-- Row 1: System | Power | CPU | Memory | Network --> <div id="vv-board-warn" style="display:none;border-radius:4px;padding:5px 10px;margin-bottom:8px;font-size:11px;background:#2a1414;border:1px solid #5a1e1e;color:#e0a0a0;"></div>
<div class="vv-card" id="vv-system" style="grid-column:span 1;">
<div id="vv-monitor">
<!-- Board group 'Host' — membership and width are declared in include/monitor_board.php.
Listing them here too would be a second answer to the same question, and the one that
drifts: the group below is only what the board happens to render at eight columns. -->
<div class="vv-card" id="vv-system">
<div id="vv-system-body">Loading...</div> <div id="vv-system-body">Loading...</div>
</div> </div>
<div class="vv-card" id="vv-ups-card" style="grid-column:span 1;"> <div class="vv-card" id="vv-ups-card">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<svg width="9" height="14" viewBox="0 0 11 18" fill="none" style="opacity:0.4;flex-shrink:0;"> <svg width="9" height="14" viewBox="0 0 11 18" fill="none" style="opacity:0.4;flex-shrink:0;">
@@ -123,7 +134,7 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-ups-body">Loading...</div> <div id="vv-ups-body">Loading...</div>
</div> </div>
<div class="vv-card" id="vv-cpu" style="grid-column:span 2;"> <div class="vv-card" id="vv-cpu">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="3" y="3" width="6" height="6" rx="0.8"/><line x1="4.5" y1="3" x2="4.5" y2="1.2"/><line x1="7.5" y1="3" x2="7.5" y2="1.2"/><line x1="4.5" y1="9" x2="4.5" y2="10.8"/><line x1="7.5" y1="9" x2="7.5" y2="10.8"/><line x1="3" y1="4.5" x2="1.2" y2="4.5"/><line x1="3" y1="7.5" x2="1.2" y2="7.5"/><line x1="9" y1="4.5" x2="10.8" y2="4.5"/><line x1="9" y1="7.5" x2="10.8" y2="7.5"/></svg></span> <span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="3" y="3" width="6" height="6" rx="0.8"/><line x1="4.5" y1="3" x2="4.5" y2="1.2"/><line x1="7.5" y1="3" x2="7.5" y2="1.2"/><line x1="4.5" y1="9" x2="4.5" y2="10.8"/><line x1="7.5" y1="9" x2="7.5" y2="10.8"/><line x1="3" y1="4.5" x2="1.2" y2="4.5"/><line x1="3" y1="7.5" x2="1.2" y2="7.5"/><line x1="9" y1="4.5" x2="10.8" y2="4.5"/><line x1="9" y1="7.5" x2="10.8" y2="7.5"/></svg></span>
@@ -134,7 +145,7 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-cpu-body">Loading...</div> <div id="vv-cpu-body">Loading...</div>
</div> </div>
<div class="vv-card" id="vv-memory" style="grid-column:span 2;"> <div class="vv-card" id="vv-memory">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="14" height="8" viewBox="0 0 14 8" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="0.6" y="2.5" width="12.8" height="3" rx="0.5"/><line x1="3" y1="2.5" x2="3" y2="0.8"/><line x1="5.5" y1="2.5" x2="5.5" y2="0.8"/><line x1="8" y1="2.5" x2="8" y2="0.8"/><line x1="10.5" y1="2.5" x2="10.5" y2="0.8"/><line x1="3" y1="5.5" x2="3" y2="7.2"/><line x1="5.5" y1="5.5" x2="5.5" y2="7.2"/><line x1="8" y1="5.5" x2="8" y2="7.2"/><line x1="10.5" y1="5.5" x2="10.5" y2="7.2"/></svg></span> <span class="vv-ico"><svg width="14" height="8" viewBox="0 0 14 8" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="0.6" y="2.5" width="12.8" height="3" rx="0.5"/><line x1="3" y1="2.5" x2="3" y2="0.8"/><line x1="5.5" y1="2.5" x2="5.5" y2="0.8"/><line x1="8" y1="2.5" x2="8" y2="0.8"/><line x1="10.5" y1="2.5" x2="10.5" y2="0.8"/><line x1="3" y1="5.5" x2="3" y2="7.2"/><line x1="5.5" y1="5.5" x2="5.5" y2="7.2"/><line x1="8" y1="5.5" x2="8" y2="7.2"/><line x1="10.5" y1="5.5" x2="10.5" y2="7.2"/></svg></span>
@@ -145,7 +156,7 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-memory-body">Loading...</div> <div id="vv-memory-body">Loading...</div>
</div> </div>
<div class="vv-card" id="vv-network" style="grid-column:span 2;"> <div class="vv-card" id="vv-network">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="13" height="12" viewBox="0 0 13 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="4.5" y="0.5" width="4" height="3" rx="0.7"/><rect x="0.5" y="8" width="4" height="3" rx="0.7"/><rect x="8.5" y="8" width="4" height="3" rx="0.7"/><line x1="6.5" y1="3.5" x2="6.5" y2="6"/><line x1="6.5" y1="6" x2="2.5" y2="6"/><line x1="2.5" y1="6" x2="2.5" y2="8"/><line x1="6.5" y1="6" x2="10.5" y2="6"/><line x1="10.5" y1="6" x2="10.5" y2="8"/></svg></span> <span class="vv-ico"><svg width="13" height="12" viewBox="0 0 13 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="4.5" y="0.5" width="4" height="3" rx="0.7"/><rect x="0.5" y="8" width="4" height="3" rx="0.7"/><rect x="8.5" y="8" width="4" height="3" rx="0.7"/><line x1="6.5" y1="3.5" x2="6.5" y2="6"/><line x1="6.5" y1="6" x2="2.5" y2="6"/><line x1="2.5" y1="6" x2="2.5" y2="8"/><line x1="6.5" y1="6" x2="10.5" y2="6"/><line x1="10.5" y1="6" x2="10.5" y2="8"/></svg></span>
@@ -156,8 +167,8 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-network-body">Loading...</div> <div id="vv-network-body">Loading...</div>
</div> </div>
<!-- Row 2: Scripts | Fallback | Partner | Containers & VMs --> <!-- Board group 'Workload' -->
<div class="vv-card" id="vv-scripts-card" style="grid-column:span 1;"> <div class="vv-card" id="vv-scripts-card">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="1" width="10" height="10" rx="1.5"/><line x1="1" y1="4.5" x2="11" y2="4.5"/><polyline points="3.5,7 5,8 3.5,9"/><line x1="6" y1="9" x2="8.5" y2="9"/></svg></span> <span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="1" width="10" height="10" rx="1.5"/><line x1="1" y1="4.5" x2="11" y2="4.5"/><polyline points="3.5,7 5,8 3.5,9"/><line x1="6" y1="9" x2="8.5" y2="9"/></svg></span>
@@ -168,7 +179,7 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-scripts-body">Loading...</div> <div id="vv-scripts-body">Loading...</div>
</div> </div>
<div class="vv-card" id="vv-fallback" style="grid-column:span 1;"> <div class="vv-card" id="vv-fallback">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="10" height="12" viewBox="0 0 10 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><path d="M5 0.8L9.2 2.8V6.2C9.2 9 5 11.2 5 11.2C5 11.2 0.8 9 0.8 6.2V2.8Z"/></svg></span> <span class="vv-ico"><svg width="10" height="12" viewBox="0 0 10 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><path d="M5 0.8L9.2 2.8V6.2C9.2 9 5 11.2 5 11.2C5 11.2 0.8 9 0.8 6.2V2.8Z"/></svg></span>
@@ -178,7 +189,7 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-fallback-body">Loading...</div> <div id="vv-fallback-body">Loading...</div>
</div> </div>
<div class="vv-card" id="vv-partner" style="grid-column:span 2;"> <div class="vv-card" id="vv-partner">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="0.5" y="1.5" width="4" height="7" rx="0.8"/><rect x="9.5" y="1.5" width="4" height="7" rx="0.8"/><line x1="4.5" y1="5" x2="9.5" y2="5"/><circle cx="7" cy="5" r="1" fill="#888" stroke="none"/></svg></span> <span class="vv-ico"><svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="0.5" y="1.5" width="4" height="7" rx="0.8"/><rect x="9.5" y="1.5" width="4" height="7" rx="0.8"/><line x1="4.5" y1="5" x2="9.5" y2="5"/><circle cx="7" cy="5" r="1" fill="#888" stroke="none"/></svg></span>
@@ -189,7 +200,7 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-partner-body">Loading...</div> <div id="vv-partner-body">Loading...</div>
</div> </div>
<div class="vv-card" id="vv-docker-folders" style="grid-column:span 4;"> <div class="vv-card" id="vv-docker-folders">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="0.5" width="12" height="2.5" rx="0.5"/><rect x="1" y="3.8" width="12" height="2.5" rx="0.5"/><rect x="1" y="7" width="12" height="2.5" rx="0.5"/></svg></span> <span class="vv-ico"><svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="0.5" width="12" height="2.5" rx="0.5"/><rect x="1" y="3.8" width="12" height="2.5" rx="0.5"/><rect x="1" y="7" width="12" height="2.5" rx="0.5"/></svg></span>
@@ -200,8 +211,8 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-docker-folders-body">Loading...</div> <div id="vv-docker-folders-body">Loading...</div>
</div> </div>
<!-- Row 3: Rsync | GPU 0 | GPU 1 | Transcode | Streams --> <!-- Board group 'Media' -->
<div class="vv-card" id="vv-rsync-card" style="grid-column:span 1;"> <div class="vv-card" id="vv-rsync-card">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><polyline points="2,4 10,4 8,2"/><polyline points="10,8 2,8 4,10"/></svg></span> <span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><polyline points="2,4 10,4 8,2"/><polyline points="10,8 2,8 4,10"/></svg></span>
@@ -211,7 +222,7 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-rsync-body">Loading...</div> <div id="vv-rsync-body">Loading...</div>
</div> </div>
<div class="vv-card" id="vv-gpu-card" style="grid-column:span 1;"> <div class="vv-card" id="vv-gpu-card">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="15" height="10" viewBox="0 0 16 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="0.8" y="0.8" width="14.4" height="7" rx="1.2"/><rect x="2.5" y="2.5" width="3.5" height="3.5" rx="0.5"/><line x1="8" y1="3" x2="13" y2="3"/><line x1="8" y1="5" x2="11" y2="5"/><rect x="3" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="6.5" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="10" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/></svg></span> <span class="vv-ico"><svg width="15" height="10" viewBox="0 0 16 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="0.8" y="0.8" width="14.4" height="7" rx="1.2"/><rect x="2.5" y="2.5" width="3.5" height="3.5" rx="0.5"/><line x1="8" y1="3" x2="13" y2="3"/><line x1="8" y1="5" x2="11" y2="5"/><rect x="3" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="6.5" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="10" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/></svg></span>
@@ -222,7 +233,7 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-gpu-body">Loading...</div> <div id="vv-gpu-body">Loading...</div>
</div> </div>
<div class="vv-card" id="vv-gpu1-card" style="grid-column:span 1;"> <div class="vv-card" id="vv-gpu1-card">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="15" height="10" viewBox="0 0 16 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="0.8" y="0.8" width="14.4" height="7" rx="1.2"/><rect x="2.5" y="2.5" width="3.5" height="3.5" rx="0.5"/><line x1="8" y1="3" x2="13" y2="3"/><line x1="8" y1="5" x2="11" y2="5"/><rect x="3" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="6.5" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="10" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/></svg></span> <span class="vv-ico"><svg width="15" height="10" viewBox="0 0 16 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="0.8" y="0.8" width="14.4" height="7" rx="1.2"/><rect x="2.5" y="2.5" width="3.5" height="3.5" rx="0.5"/><line x1="8" y1="3" x2="13" y2="3"/><line x1="8" y1="5" x2="11" y2="5"/><rect x="3" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="6.5" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="10" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/></svg></span>
@@ -233,7 +244,7 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-gpu1-body">Loading...</div> <div id="vv-gpu1-body">Loading...</div>
</div> </div>
<div class="vv-card" id="vv-transcode" style="grid-column:span 1;"> <div class="vv-card" id="vv-transcode">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="14" height="12" viewBox="0 0 14 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="0.5" y="1" width="4" height="10" rx="0.8"/><line x1="0.5" y1="3.5" x2="2.3" y2="3.5"/><line x1="0.5" y1="8.5" x2="2.3" y2="8.5"/><line x1="2.7" y1="3.5" x2="4.5" y2="3.5"/><line x1="2.7" y1="8.5" x2="4.5" y2="8.5"/><line x1="6" y1="6" x2="9.5" y2="6"/><polyline points="8.2,4.3 10.2,6 8.2,7.7"/><rect x="11" y="1" width="2.5" height="10" rx="0.5"/></svg></span> <span class="vv-ico"><svg width="14" height="12" viewBox="0 0 14 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="0.5" y="1" width="4" height="10" rx="0.8"/><line x1="0.5" y1="3.5" x2="2.3" y2="3.5"/><line x1="0.5" y1="8.5" x2="2.3" y2="8.5"/><line x1="2.7" y1="3.5" x2="4.5" y2="3.5"/><line x1="2.7" y1="8.5" x2="4.5" y2="8.5"/><line x1="6" y1="6" x2="9.5" y2="6"/><polyline points="8.2,4.3 10.2,6 8.2,7.7"/><rect x="11" y="1" width="2.5" height="10" rx="0.5"/></svg></span>
@@ -247,7 +258,7 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
every poll, and a control that is re-rendered underneath a click is a control that loses every poll, and a control that is re-rendered underneath a click is a control that loses
one. Hidden until a partner is known — on a single-host install the two buttons would be one. Hidden until a partner is known — on a single-host install the two buttons would be
two names for the same view, which is how the Media Stack tab handles it too. --> two names for the same view, which is how the Media Stack tab handles it too. -->
<div class="vv-card" id="vv-streams" style="grid-column:span 4;"> <div class="vv-card" id="vv-streams">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="11" height="12" viewBox="0 0 11 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><polygon points="1.5,1 1.5,11 10,6"/></svg></span> <span class="vv-ico"><svg width="11" height="12" viewBox="0 0 11 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><polygon points="1.5,1 1.5,11 10,6"/></svg></span>
@@ -261,8 +272,8 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-streams-body">Loading...</div> <div id="vv-streams-body">Loading...</div>
</div> </div>
<!-- Row 4: Watchdog | Parity | Pools | Array --> <!-- Board group 'Storage' -->
<div class="vv-card" id="vv-watchdog-card" style="grid-column:span 1;"> <div class="vv-card" id="vv-watchdog-card">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="14" height="9" viewBox="0 0 14 8" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><path d="M1 4C3.2 1 5 0.5 7 0.5C9 0.5 10.8 1 13 4C10.8 7 9 7.5 7 7.5C5 7.5 3.2 7 1 4Z"/><circle cx="7" cy="4" r="1.5"/></svg></span> <span class="vv-ico"><svg width="14" height="9" viewBox="0 0 14 8" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><path d="M1 4C3.2 1 5 0.5 7 0.5C9 0.5 10.8 1 13 4C10.8 7 9 7.5 7 7.5C5 7.5 3.2 7 1 4Z"/><circle cx="7" cy="4" r="1.5"/></svg></span>
@@ -272,7 +283,7 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-watchdog-body">Loading...</div> <div id="vv-watchdog-body">Loading...</div>
</div> </div>
<div class="vv-card" id="vv-parity-card" style="grid-column:span 1;"> <div class="vv-card" id="vv-parity-card">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="5"/><polyline points="3.5,6 5.5,8 8.5,4"/></svg></span> <span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="5"/><polyline points="3.5,6 5.5,8 8.5,4"/></svg></span>
@@ -283,7 +294,7 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-parity-body">Loading...</div> <div id="vv-parity-body">Loading...</div>
</div> </div>
<div class="vv-card" id="vv-storage-card" style="grid-column:3/span 2;"> <div class="vv-card" id="vv-storage-card">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><ellipse cx="6" cy="3" rx="4.5" ry="1.5"/><line x1="1.5" y1="3" x2="1.5" y2="9"/><line x1="10.5" y1="3" x2="10.5" y2="9"/><ellipse cx="6" cy="9" rx="4.5" ry="1.5"/></svg></span> <span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><ellipse cx="6" cy="3" rx="4.5" ry="1.5"/><line x1="1.5" y1="3" x2="1.5" y2="9"/><line x1="10.5" y1="3" x2="10.5" y2="9"/><ellipse cx="6" cy="9" rx="4.5" ry="1.5"/></svg></span>
@@ -295,7 +306,7 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-storage-body">Loading...</div> <div id="vv-storage-body">Loading...</div>
</div> </div>
<div class="vv-card" id="vv-array-card" style="grid-column:5/span 4;"> <div class="vv-card" id="vv-array-card">
<h3> <h3>
<span style="display:flex;align-items:center;gap:5px;"> <span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="14" height="12" viewBox="0 0 14 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="0.7" y="0.7" width="12.6" height="3" rx="0.8"/><rect x="0.7" y="4.5" width="12.6" height="3" rx="0.8"/><rect x="0.7" y="8.3" width="12.6" height="3" rx="0.8"/><circle cx="11.5" cy="2.2" r="0.8" fill="#888" stroke="none"/><circle cx="11.5" cy="6" r="0.8" fill="#888" stroke="none"/><circle cx="11.5" cy="9.8" r="0.8" fill="#888" stroke="none"/></svg></span> <span class="vv-ico"><svg width="14" height="12" viewBox="0 0 14 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="0.7" y="0.7" width="12.6" height="3" rx="0.8"/><rect x="0.7" y="4.5" width="12.6" height="3" rx="0.8"/><rect x="0.7" y="8.3" width="12.6" height="3" rx="0.8"/><circle cx="11.5" cy="2.2" r="0.8" fill="#888" stroke="none"/><circle cx="11.5" cy="6" r="0.8" fill="#888" stroke="none"/><circle cx="11.5" cy="9.8" r="0.8" fill="#888" stroke="none"/></svg></span>
@@ -309,6 +320,36 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
</div> </div>
<script>
// The board is declared in one file and rendered in another, so the two can drift: a card added
// to the markup nobody declared gets order 0 and lands first, and a declaration with no card
// leaves the row it belonged to one span short. Neither shows as an error — the board just comes
// out subtly wrong — so both are checked here against what actually rendered, alongside anything
// vv_mon_board_check() already found wrong with the declaration itself.
window.VV_BOARD = <?= vv_mon_board_js() ?>;
(function vvBoardGuard() {
const grid = document.getElementById('vv-monitor');
const warn = document.getElementById('vv-board-warn');
if (!grid || !warn) return;
const rendered = Array.from(grid.children)
.filter(el => el.classList.contains('vv-card'))
.map(el => el.id);
const undeclared = rendered.filter(id => !window.VV_BOARD.ids.includes(id));
const missing = window.VV_BOARD.ids.filter(id => !rendered.includes(id));
const problems = window.VV_BOARD.problems.slice();
if (undeclared.length) problems.push('cards in the markup that the board does not declare: ' + undeclared.join(', '));
if (missing.length) problems.push('cards the board declares that did not render: ' + missing.join(', '));
if (!problems.length) return;
warn.textContent = 'Monitor board: ' + problems.join(' · ');
warn.style.display = 'block';
console.warn('[varaverk] monitor board', problems);
})();
</script>
<?php if (vv_ai_ui_on()): ?> <?php if (vv_ai_ui_on()): ?>
<!-- ── AI row — its own grid, deliberately ────────────────────────────────────── <!-- ── AI row — its own grid, deliberately ──────────────────────────────────────
Same eight columns and the same gap, so it reads as the last row of the one above. It is a Same eight columns and the same gap, so it reads as the last row of the one above. It is a
@@ -2015,14 +2056,17 @@ function vvPollMonitor(live) {
renderGpuCard(gpuList[0], 'vv-gpu-body', 'vv-gpu-label', 'GPU'); renderGpuCard(gpuList[0], 'vv-gpu-body', 'vv-gpu-label', 'GPU');
renderGpuCard(gpuList[1], 'vv-gpu1-body', 'vv-gpu1-label', 'GPU 1'); renderGpuCard(gpuList[1], 'vv-gpu1-body', 'vv-gpu1-label', 'GPU 1');
// Row 3 is 8 columns wide: Rsync(1) + GPU0(1) + GPU1(1) + Transcode(1) + Streams(4). // On a single-GPU host the second GPU card is hidden, and its column has to go somewhere or
// On a single-GPU host the second card is hidden and Transcode widens back to 2 so the // the row is one span short at every rung. Which card absorbs it is declared on the board
// row still fills exactly — otherwise it would leave a one-column hole. // ('absorbs' => 'vv-gpu1-card'); all this does is say the card is gone. The width itself is
// set by generated CSS, per rung — this used to write style.gridColumn='span 2' directly,
// which an inline style made unclampable, so a one-GPU phone got a span of 2 in a
// one-column grid and scrolled sideways.
const gpu1Card = document.getElementById('vv-gpu1-card'); const gpu1Card = document.getElementById('vv-gpu1-card');
const transcodeCard = document.getElementById('vv-transcode');
const twoGpus = gpuList.length > 1; const twoGpus = gpuList.length > 1;
if (gpu1Card) gpu1Card.style.display = twoGpus ? '' : 'none'; if (gpu1Card) gpu1Card.style.display = twoGpus ? '' : 'none';
if (transcodeCard) transcodeCard.style.gridColumn = twoGpus ? 'span 1' : 'span 2'; const gpu1Absent = window.VV_BOARD?.absent?.['vv-gpu1-card'];
if (gpu1Absent) document.getElementById('vv-monitor')?.classList.toggle(gpu1Absent, !twoGpus);
// ── Scripts ───────────────────────────────────────────────────────────── // ── Scripts ─────────────────────────────────────────────────────────────
vvLastScripts = d.scripts ?? {}; vvLastScripts = d.scripts ?? {};