Show the terse control list when the help block is collapsed

This commit is contained in:
Gmer4Lfe
2026-08-05 17:58:29 -04:00
parent 2ad3a1bfed
commit aeb8c01370
2 changed files with 90 additions and 22 deletions
+74 -17
View File
@@ -106,6 +106,79 @@ function vv_docs_render(string $rel, array $vars, ?callable $keep = null): strin
// here, after escaping. Injecting <code> into the markdown before rendering, as this file used to // here, after escaping. Injecting <code> into the markdown before rendering, as this file used to
// do, meant both Parsedown's safe mode and the <pre> fallback escaped the tags and printed them // do, meant both Parsedown's safe mode and the <pre> fallback escaped the tags and printed them
// as literal text. The feature never worked; nothing called it, so nothing reported it. // 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]])
? '<code class="vv-live-var">' . htmlspecialchars($vars[$m[1]]) . '</code>'
: '<code class="vv-unknown-var">$' . $m[1] . '</code>';
}, $s);
$s = preg_replace('/`([^`]+)`/', '<code>$1</code>', $s);
$s = preg_replace('/\*\*([^*]+)\*\*/', '<strong>$1</strong>', $s);
$s = preg_replace('/(?<![\w*])\*([^*]+)\*(?![\w*])/', '<em>$1</em>', $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):]+)\)/', '<a href="$2">$1</a>', $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 .= '<li class="vv-info-sep">' . vv_docs_inline($h, $vars) . "</li>\n";
}
continue;
}
if ($skip || $t === '') { if ($t === '') $rowN = 0; continue; }
if (preg_match('/^[-*]\s+(.*)$/', $t, $m)) {
$out .= '<li>' . vv_docs_inline($m[1], $vars) . "</li>\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 .= '<li><strong>' . vv_docs_inline($term, $vars) . '</strong> — '
. vv_docs_inline(implode(' · ', array_filter($cells)), $vars) . "</li>\n";
}
}
return $out;
}
function vv_docs_markdown(string $md, array $vars, ?callable $keep = null): string { function vv_docs_markdown(string $md, array $vars, ?callable $keep = null): string {
if (file_exists(PARSEDOWN_PATH)) { if (file_exists(PARSEDOWN_PATH)) {
require_once 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); return $pd->text($md);
} }
$inline = function (string $s) use ($vars): string { $inline = fn(string $s): string => vv_docs_inline($s, $vars);
$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]])
? '<code class="vv-live-var">' . htmlspecialchars($vars[$m[1]]) . '</code>'
: '<code class="vv-unknown-var">$' . $m[1] . '</code>';
}, $s);
$s = preg_replace('/`([^`]+)`/', '<code>$1</code>', $s);
$s = preg_replace('/\*\*([^*]+)\*\*/', '<strong>$1</strong>', $s);
$s = preg_replace('/(?<![\w*])\*([^*]+)\*(?![\w*])/', '<em>$1</em>', $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):]+)\)/', '<a href="$2">$1</a>', $s);
return $s;
};
$out = ''; $out = '';
$list = null; // 'ul' | 'ol' | null $list = null; // 'ul' | 'ol' | null
+16 -5
View File
@@ -508,16 +508,21 @@ $runningScripts = array_unique($runningScripts);
also what the AI tab retrieves, so the panel you read and the answer the 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 assistant gives are the same text and cannot drift — the same argument that
makes this page parse script PURPOSE blocks instead of restating them. makes this page parse script PURPOSE blocks instead of restating them.
Everything lives inside the collapsible body, and collapsed means gone. An Two views of one file, swapped rather than stacked. Collapsed shows the terse
earlier version kept the reference tables permanently visible below the fold one-line-per-control list this help used to be — short enough that Next Runs and
line: 33 table rows that pushed Next Runs, Recent Errors, Activity, Locks and the error blocks stay on screen, which an earlier always-visible tier of 33
the rest off the bottom of a fixed-height panel. Reference material must not table rows was not. Expanding replaces it with the full document.
outrank the live state of the machine on the page you watch it from. --> .vv-sug-brief is the inverse of .vv-sug-body; vvToggleSug() and
vvRestoreSugStates() drive both. -->
<div class="vv-sug-body vv-info-body"> <div class="vv-sug-body vv-info-body">
<div class="vv-doc"> <div class="vv-doc">
<?= vv_docs_render('Plugin/unraid/pages/readme/scheduler-readme.md', $_vv_doc_vars) ?> <?= vv_docs_render('Plugin/unraid/pages/readme/scheduler-readme.md', $_vv_doc_vars) ?>
</div> </div>
</div> </div>
<ul class="vv-info-cols vv-sug-brief vv-info-body" style="display:none">
<?= vv_docs_brief('Plugin/unraid/pages/readme/scheduler-readme.md', $_vv_doc_vars,
fn($h) => is_string($h) && str_starts_with($h, 'Reference —')) ?>
</ul>
</div> </div>
<!-- ── Next Runs ── --> <!-- ── Next Runs ── -->
@@ -2104,6 +2109,10 @@ function vvToggleSug(header) {
body.style.display = open ? 'none' : ''; body.style.display = open ? 'none' : '';
chevron.textContent = open ? '▸' : '▾'; chevron.textContent = open ? '▸' : '▾';
const block = header.closest('[data-save-key]'); 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'); if (block) localStorage.setItem('vv-sug-' + block.dataset.saveKey, open ? '0' : '1');
} }
@@ -2117,6 +2126,8 @@ function vvRestoreSugStates() {
const open = saved === '1'; const open = saved === '1';
body.style.display = open ? '' : 'none'; body.style.display = open ? '' : 'none';
if (chevron) chevron.textContent = open ? '▾' : '▸'; if (chevron) chevron.textContent = open ? '▾' : '▸';
const brief = block.querySelector('.vv-sug-brief');
if (brief) brief.style.display = open ? 'none' : '';
}); });
} }