From aeb8c0137061bbdc2926f9048aba325f66612c9b Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Wed, 5 Aug 2026 17:58:29 -0400 Subject: [PATCH] Show the terse control list when the help block is collapsed --- Plugin/unraid/include/docs.php | 91 +++++++++++++++++++++++++------ Plugin/unraid/pages/scheduler.php | 21 +++++-- 2 files changed, 90 insertions(+), 22 deletions(-) diff --git a/Plugin/unraid/include/docs.php b/Plugin/unraid/include/docs.php index 26822c9..10eac4d 100644 --- a/Plugin/unraid/include/docs.php +++ b/Plugin/unraid/include/docs.php @@ -106,6 +106,79 @@ function vv_docs_render(string $rel, array $vars, ?callable $keep = null): strin // here, after escaping. Injecting into the markdown before rendering, as this file used to // do, meant both Parsedown's safe mode and the
 fallback escaped the tags and printed them
 // as literal text. The feature never worked; nothing called it, so nothing reported it.
+// Inline formatting for one line of markdown. Standalone rather than a closure because both the
+// full renderer and the brief list below use it — two copies would drift, and the whole point of
+// these docs is that the panel and the assistant cannot disagree.
+//
+// Escape first, then format: the only HTML in the output is HTML this function put there.
+function vv_docs_inline(string $s, array $vars): string {
+    $s = htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
+    // `$VAR` → the live conf value. Unresolved names render in their own class rather than
+    // silently reading as prose, so a stale reference in a doc is visible as a defect.
+    $s = preg_replace_callback('/`\$([A-Z0-9_]+)`/', function ($m) use ($vars) {
+        return isset($vars[$m[1]])
+            ? '' . htmlspecialchars($vars[$m[1]]) . ''
+            : '$' . $m[1] . '';
+    }, $s);
+    $s = preg_replace('/`([^`]+)`/', '$1', $s);
+    $s = preg_replace('/\*\*([^*]+)\*\*/', '$1', $s);
+    $s = preg_replace('/(?$1', $s);
+    // Links are restricted to http/https and relative paths — a doc is trusted, but these files
+    // sync between hosts, so javascript: must not be reachable through one.
+    $s = preg_replace('/\[([^\]]+)\]\((https?:\/\/[^\s)]+|[^\s):]+)\)/', '$1', $s);
+    return $s;
+}
+
+// Terse one-line-per-control list — the shape this help had before it became a document.
+// Reads the same markdown as the full render, so the collapsed and expanded views cannot drift.
+//
+// Only table rows and bullets survive. Paragraphs are dropped on purpose: a list you sweep with
+// your eye stops working the moment there is prose between the rows, and that is precisely what
+// made the original comb well. Section titles become the full-width divider the old list used.
+function vv_docs_brief(string $rel, array $vars, ?callable $keep = null): string {
+    $base = realpath(SCRIPTS_DIR);
+    $path = realpath(SCRIPTS_DIR . '/' . $rel);
+    if ($base === false || $path === false)   return '';
+    if (!str_starts_with($path, $base . '/')) return '';
+    if (strtolower(pathinfo($path, PATHINFO_EXTENSION)) !== 'md') return '';
+
+    $out  = '';
+    $skip = $keep !== null && !$keep(null);
+    $rowN = 0;                       // per-table row counter — row 0 is the header, not content
+
+    foreach (explode("\n", str_replace("\r\n", "\n", (string)@file_get_contents($path))) as $line) {
+        $t = trim($line);
+
+        if (preg_match('/^(#{1,6})\s+(.*)$/', $t, $m)) {
+            $rowN = 0;
+            $skip = $keep !== null && !$keep(strlen($m[1]) === 1 ? null : $m[2]);
+            // "Reference — the controls on a job row" reads as "the controls on a job row" once
+            // the whole list is reference material.
+            if (!$skip && strlen($m[1]) > 1) {
+                $h = preg_replace('/^Reference\s*—\s*/u', '', $m[2]);
+                $out .= '
  • ' . vv_docs_inline($h, $vars) . "
  • \n"; + } + continue; + } + if ($skip || $t === '') { if ($t === '') $rowN = 0; continue; } + + if (preg_match('/^[-*]\s+(.*)$/', $t, $m)) { + $out .= '
  • ' . vv_docs_inline($m[1], $vars) . "
  • \n"; + continue; + } + + if (strpos($t, '|') !== false && substr_count($t, '|') >= 2) { + if (preg_match('/^\|?[\s:-]*-[\s|:-]*\|/', $t)) continue; // the --- separator + $cells = array_map('trim', explode('|', trim($t, '| '))); + if ($rowN++ === 0) continue; // header row + $term = array_shift($cells); + $out .= '
  • ' . vv_docs_inline($term, $vars) . ' — ' + . vv_docs_inline(implode(' · ', array_filter($cells)), $vars) . "
  • \n"; + } + } + return $out; +} + function vv_docs_markdown(string $md, array $vars, ?callable $keep = null): string { if (file_exists(PARSEDOWN_PATH)) { require_once PARSEDOWN_PATH; @@ -114,23 +187,7 @@ function vv_docs_markdown(string $md, array $vars, ?callable $keep = null): stri return $pd->text($md); } - $inline = function (string $s) use ($vars): string { - $s = htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); - // `$VAR` → the live conf value. Unresolved names render in their own class rather than - // silently reading as prose, so a stale reference in a doc is visible as a defect. - $s = preg_replace_callback('/`\$([A-Z0-9_]+)`/', function ($m) use ($vars) { - return isset($vars[$m[1]]) - ? '' . htmlspecialchars($vars[$m[1]]) . '' - : '$' . $m[1] . ''; - }, $s); - $s = preg_replace('/`([^`]+)`/', '$1', $s); - $s = preg_replace('/\*\*([^*]+)\*\*/', '$1', $s); - $s = preg_replace('/(?$1', $s); - // Links are restricted to http/https and relative paths — a doc is trusted, but these - // files sync between hosts, so javascript: must not be reachable through one. - $s = preg_replace('/\[([^\]]+)\]\((https?:\/\/[^\s)]+|[^\s):]+)\)/', '$1', $s); - return $s; - }; + $inline = fn(string $s): string => vv_docs_inline($s, $vars); $out = ''; $list = null; // 'ul' | 'ol' | null diff --git a/Plugin/unraid/pages/scheduler.php b/Plugin/unraid/pages/scheduler.php index bfe7ea1..04533e6 100644 --- a/Plugin/unraid/pages/scheduler.php +++ b/Plugin/unraid/pages/scheduler.php @@ -508,16 +508,21 @@ $runningScripts = array_unique($runningScripts); also what the AI tab retrieves, so the panel you read and the answer the assistant gives are the same text and cannot drift — the same argument that makes this page parse script PURPOSE blocks instead of restating them. - Everything lives inside the collapsible body, and collapsed means gone. An - earlier version kept the reference tables permanently visible below the fold - line: 33 table rows that pushed Next Runs, Recent Errors, Activity, Locks and - the rest off the bottom of a fixed-height panel. Reference material must not - outrank the live state of the machine on the page you watch it from. --> + Two views of one file, swapped rather than stacked. Collapsed shows the terse + one-line-per-control list this help used to be — short enough that Next Runs and + the error blocks stay on screen, which an earlier always-visible tier of 33 + table rows was not. Expanding replaces it with the full document. + .vv-sug-brief is the inverse of .vv-sug-body; vvToggleSug() and + vvRestoreSugStates() drive both. -->
    + @@ -2104,6 +2109,10 @@ function vvToggleSug(header) { body.style.display = open ? 'none' : ''; chevron.textContent = open ? '▸' : '▾'; const block = header.closest('[data-save-key]'); + // Optional collapsed-state summary: shown exactly when the body is hidden. Lets a block put + // something terse on screen while shut instead of nothing, without a second toggle to manage. + const brief = block && block.querySelector('.vv-sug-brief'); + if (brief) brief.style.display = open ? '' : 'none'; if (block) localStorage.setItem('vv-sug-' + block.dataset.saveKey, open ? '0' : '1'); } @@ -2117,6 +2126,8 @@ function vvRestoreSugStates() { const open = saved === '1'; body.style.display = open ? '' : 'none'; if (chevron) chevron.textContent = open ? '▾' : '▸'; + const brief = block.querySelector('.vv-sug-brief'); + if (brief) brief.style.display = open ? 'none' : ''; }); }