Put each page's settings on that page

A watchdog threshold was reachable only from the bottom of a list of a hundred
and twenty on another tab.
This commit is contained in:
Gmer4Lfe
2026-08-12 19:55:51 -04:00
parent 8ca52c64b9
commit e64d26af6c
13 changed files with 284 additions and 151 deletions
+1 -143
View File
@@ -87,20 +87,6 @@ vv_conf_ui_assets();
font-size:12px;padding:5px 8px;outline:none;width:100%;box-sizing:border-box;
font-family:monospace; }
.vv-set-inp:focus{ border-color:#444; }
/* The All-settings header. A row rather than a heading because it carries the disclosure, the
filter and the save together — the filter is only meaningful while the card is open, and the
save only while something is dirty, so both appear with what they act on. */
.vv-cf-all-h { display:flex;align-items:center;gap:10px;cursor:pointer;user-select:none;
flex-wrap:wrap; }
.vv-cf-all-c { color:#333;font-size:9px;transition:transform .12s; }
.vv-cf-all-h.open .vv-cf-all-c { transform:rotate(90deg); }
.vv-cf-all-f { max-width:260px;cursor:text; }
/* Scrolls itself rather than growing the page past the viewport — 124 sections is far taller
than a screen, and the Save button belongs where it can still be reached. */
#vv-cf-all { max-height:60vh;overflow-y:auto;margin-top:10px;padding-right:4px; }
/* Hidden by the filter. display:none rather than removal, so the controls keep their state and
a field edited before filtering is still dirty and still saved. */
.vv-cf-group.vv-filtered { display:none; }
</style>
<div style="max-width:740px;margin:0 auto;">
@@ -238,29 +224,8 @@ vv_conf_ui_assets();
<pre class="vv-set-out" id="vv-api-out"></pre>
</div>
<!-- Everything else the conf holds. The cards above are purpose-built for the few settings that
needed a workflow — a migration, a renewal, a webhook test. This is the rest: a third of the
configuration had no control anywhere, so the answer to "where do I change X" was "open a
file over SSH", which is not an answer a settings page should give.
<?php vv_conf_ui_card('vv-cf-all', '*', 'All settings'); ?>
Collapsed and loaded on first open. 124 sections is real work to assemble and the card is
shut almost every time the page is drawn.
Five sections are deliberately absent — the machine's identity and the roots everything
derives from. See VV_UI_SECTION_EXCLUDE for which, and why each one. -->
<div class="vv-set-card">
<div class="vv-cf-all-h" id="vv-cf-all-t">
<span class="vv-cf-all-c">▶</span>
<span class="vv-set-hdr" style="margin-bottom:0;">All settings</span>
<input type="text" class="vv-set-inp vv-cf-all-f" id="vv-cf-all-filter"
placeholder="filter by name or setting…" style="display:none">
<span class="vv-set-sum" id="vv-cf-all-sum" style="margin-left:auto"></span>
<button class="vv-set-btn primary" id="vv-cf-all-save" type="button"
style="display:none" disabled>Save</button>
</div>
<div id="vv-cf-all-body" style="display:none">
<div id="vv-cf-all"><p class="vv-cf-empty">opens when you do</p></div>
</div>
</div>
</div>
@@ -530,111 +495,4 @@ function vvApiRenew() {
// Auto-check on load
vvApiCheck();
// ── All settings ─────────────────────────────────────────────────────────────
// Every conf section that is not structurally excluded, drawn by the same renderer the AI tab
// and the Scheduler use and written through the same guarded path. Nothing about which sections
// exist or what control each field gets is decided here — it is read from the conf.
const VV_CF_ALL_API = '/plugins/varaverk/api/confform.php';
let _vvCfAllLoaded = false;
function vvCfAllToggle() {
const head = document.getElementById('vv-cf-all-t');
const body = document.getElementById('vv-cf-all-body');
const open = body.style.display === 'none';
body.style.display = open ? '' : 'none';
head.classList.toggle('open', open);
document.getElementById('vv-cf-all-filter').style.display = open ? '' : 'none';
document.getElementById('vv-cf-all-save').style.display = open ? '' : 'none';
// First open only. Reloading on every toggle would discard edits in progress the moment
// someone collapsed the card to look at something else.
if (open) vvCfAllLoad();
}
function vvCfAllLoad() {
if (_vvCfAllLoaded) return;
_vvCfAllLoaded = true;
fetch(VV_CF_ALL_API + '?sections=*')
.then(r => r.json())
.then(d => {
if (!d.ok) throw new Error(d.error || 'could not be read');
VvConfUI.render('vv-cf-all', d.groups || []);
VvConfUI.wire('vv-cf-all', vvCfAllDirty);
vvCfAllDirty();
})
.catch(e => {
// Retryable: the flag goes back, so closing and reopening tries again rather than
// leaving a permanent error where the settings should be.
_vvCfAllLoaded = false;
document.getElementById('vv-cf-all').innerHTML =
'<p class="vv-cf-empty">could not be read: ' + String(e.message || e) + '</p>';
});
}
function vvCfAllDirty() {
const n = VvConfUI.dirtyCount('vv-cf-all');
const btn = document.getElementById('vv-cf-all-save');
const sum = document.getElementById('vv-cf-all-sum');
btn.disabled = (n === 0);
sum.textContent = n ? (n + ' unsaved') : '';
sum.style.color = n ? '#ffb74d' : '#444';
}
// Hides whole sections, never individual fields — a setting means little without the header that
// says what it belongs to. Matches the section name and the key names inside it, because people
// look for both. display:none rather than removal, so a field edited before filtering is still
// dirty and still saved: a filter that silently drops pending edits would be a data-loss bug
// wearing a search box.
function vvCfAllFilter() {
const q = document.getElementById('vv-cf-all-filter').value.trim().toLowerCase();
document.querySelectorAll('#vv-cf-all .vv-cf-group').forEach(g => {
g.classList.toggle('vv-filtered', q !== '' && !g.textContent.toLowerCase().includes(q));
});
}
function vvCfAllSave() {
const changes = VvConfUI.collect('vv-cf-all');
if (!changes.length) return;
const btn = document.getElementById('vv-cf-all-save');
const sum = document.getElementById('vv-cf-all-sum');
btn.disabled = true;
sum.textContent = 'saving…';
sum.style.color = '#444';
// URLSearchParams, not FormData — a multipart POST to this plugin's endpoints hangs with no
// status ever returned.
fetch(VV_CF_ALL_API, { method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: new URLSearchParams({ changes: JSON.stringify(changes) }) })
.then(r => r.text())
.then(t => {
let d;
try { d = JSON.parse(t); }
catch (_) {
throw new Error(t.trim() ? 'unparseable response'
: 'empty response — rejected before the endpoint ran');
}
if (!d.ok) throw new Error(d.error || 'save failed');
// Rebased rather than refetched: the values on screen are now the values on disk, and a
// reload would repaint every control including the one just edited.
VvConfUI.commit('vv-cf-all');
vvCfAllDirty();
sum.textContent = 'saved ' + changes.length + ' change' + (changes.length > 1 ? 's' : '');
sum.style.color = '#4caf50';
})
.catch(e => {
// Left dirty on purpose. A refused write changed nothing, and clearing the marks would
// say it had.
sum.textContent = String(e.message || e);
sum.style.color = '#ef5350';
vvCfAllDirty();
});
}
document.getElementById('vv-cf-all-t').addEventListener('click', e => {
// The filter and the save live in the header; clicking either must not collapse the card.
if (e.target.closest('#vv-cf-all-filter, #vv-cf-all-save')) return;
vvCfAllToggle();
});
document.getElementById('vv-cf-all-filter').addEventListener('input', vvCfAllFilter);
document.getElementById('vv-cf-all-save').addEventListener('click', vvCfAllSave);
</script>