From 67510b8fadb5c479404335ea7a37af9ce1ca08cd Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Tue, 11 Aug 2026 20:47:49 -0400 Subject: [PATCH] Draw the Scheduler's settings with the same renderer as everything else It drew its own, so 329 of its 704 fields were text boxes that should have been switches, numbers or lists, and 48 credentials rendered legibly. --- Plugin/unraid/include/confui.php | 80 +++++++++++++++++--- Plugin/unraid/pages/scheduler.php | 122 +++++++++++------------------- 2 files changed, 117 insertions(+), 85 deletions(-) diff --git a/Plugin/unraid/include/confui.php b/Plugin/unraid/include/confui.php index c0e7f37..8c2005c 100644 --- a/Plugin/unraid/include/confui.php +++ b/Plugin/unraid/include/confui.php @@ -131,17 +131,40 @@ function vv_conf_ui_assets(): void { `; } + // The gutter is seeded here and then kept in step by the input/scroll handlers in wire(). + // It cannot be produced once and left: the numbers follow the textarea's value, which is the + // thing the operator is about to change. function linesCtl(f) { const v = String(f.value == null ? '' : f.value); const n = v.split('\n').length; - const gutter = Array.from({ length: n }, (_, i) => i + 1).join('\n'); return `
-
${gutter}
-
`; } + function gutterFor(v) { + const n = String(v).split('\n').length; + let s = ''; + for (let i = 1; i <= n; i++) s += i + '\n'; + return s; + } + + // Kept together, because they are two halves of one illusion: the gutter is a separate element + // that has to be renumbered when the text changes and scrolled when the text scrolls, or the + // numbers drift away from the lines they belong to. + function syncGutter(ta) { + const g = ta.parentElement && ta.parentElement.querySelector('.vv-cf-lines'); + if (!g) return; + g.textContent = gutterFor(ta.value); + g.scrollTop = ta.scrollTop; + } + function scrollGutter(ta) { + const g = ta.parentElement && ta.parentElement.querySelector('.vv-cf-lines'); + if (g) g.scrollTop = ta.scrollTop; + } + function textCtl(f) { return `
${esc(f.desc)}
` : ''; - return `
+ return `
+ ${n}
${esc(f.key)}
@@ -167,8 +196,11 @@ function vv_conf_ui_assets(): void {
`; } - function groupHtml(g) { - const fields = (g.fields || []).map(fieldHtml).join(''); + // Takes the counter by reference so numbering continues across groups. Returning it would have + // worked too, but every caller would then be responsible for threading it correctly and one of + // them eventually would not. + function groupHtml(g, counter) { + const fields = (g.fields || []).map(f => fieldHtml(f, ++counter.n)).join(''); if (!fields) return ''; return `
${esc(g.subsection || g.name || '')} @@ -186,11 +218,32 @@ function vv_conf_ui_assets(): void { } window.VvConfUI = { + // The markup on its own, for a caller composing it into something larger — the Scheduler's + // advanced view wraps it in a Config block alongside the header and README sections. Whoever + // uses this owes the DOM a hydrate() afterwards. + html(groups) { + const counter = { n: 0 }; + return (groups || []).map(g => groupHtml(g, counter)).join(''); + }, + + // Everything that can only be done once the markup is in the document. Separate from html() + // because a caller that built a bigger string still needs this half, and separate from wire() + // because wire() is idempotent per container while this must run after every re-render. + hydrate(into) { + const box = typeof into === 'string' ? document.getElementById(into) : into; + if (!box) return; + // Seeded from the rendered DOM rather than at build time, so a textarea the browser sized + // differently than expected still gets a gutter matching what is on screen. + box.querySelectorAll('.vv-cf-array').forEach(syncGutter); + return box; + }, + render(into, groups) { const box = typeof into === 'string' ? document.getElementById(into) : into; if (!box) return; - const html = (groups || []).map(groupHtml).join(''); + const html = this.html(groups); box.innerHTML = html || '

Nothing configurable here.

'; + this.hydrate(box); return box; }, @@ -232,7 +285,16 @@ function vv_conf_ui_assets(): void { if (onChange) onChange(); }; - box.addEventListener('input', e => { if (e.target.dataset.ctl) refresh(e.target); }); + box.addEventListener('input', e => { + if (!e.target.dataset.ctl) return; + if (e.target.classList.contains('vv-cf-array')) syncGutter(e.target); + refresh(e.target); + }); + // Capture: scroll does not bubble, so a listener on the container never sees a textarea + // scrolling inside it any other way. + box.addEventListener('scroll', e => { + if (e.target.classList && e.target.classList.contains('vv-cf-array')) scrollGutter(e.target); + }, true); box.addEventListener('change', e => { const el = e.target; if (!el.dataset.ctl) return; diff --git a/Plugin/unraid/pages/scheduler.php b/Plugin/unraid/pages/scheduler.php index f9b6bf3..5afec36 100644 --- a/Plugin/unraid/pages/scheduler.php +++ b/Plugin/unraid/pages/scheduler.php @@ -68,6 +68,10 @@ require_once dirname(__DIR__) . '/include/ai_profiles.php'; // The chat component itself now, not only the store: the assistant panel in the right-hand pane // is an instance of it rather than a second implementation. require_once dirname(__DIR__) . '/include/ai_chat.php'; +// The shared settings renderer. This page used to draw conf fields itself, which meant every +// boolean was a text box and every credential was legible — 329 of its 704 fields deserved a +// better control than the one they got. +require_once dirname(__DIR__) . '/include/confui.php'; // Live values for the `$VAR` markers in pages/readme/*.md. Conf variables, plus the derived // path constants — those are not conf keys, but they are exactly what a reader needs resolved @@ -1083,6 +1087,7 @@ Still the same two servers, two households, the same media stack running itself. vv_ai_profiles_script(); vv_ai_chat_store_script(); vv_ai_chat_assets(); + vv_conf_ui_assets(); ?>