Give the rest of the conf a place to be changed

A third of the settings had no control anywhere, so the answer to "where do I
change this" was to open a file over SSH.
This commit is contained in:
Gmer4Lfe
2026-08-12 19:41:01 -04:00
parent ec3084db12
commit 8ca52c64b9
5 changed files with 889 additions and 128 deletions
+151
View File
@@ -49,6 +49,10 @@ $_discordHook = $_vars[$_discordKey] ?? '';
$_aiHost = vv_is_ai_host();
$_aiEnabled = strtolower(trim($_vars['AI_ENABLED'] ?? 'false')) === 'true';
// The shared settings renderer, for the All-settings card at the foot of the page.
require_once dirname(__DIR__) . '/include/confui.php';
vv_conf_ui_assets();
?>
<style>
.vv-set-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:14px 16px;margin-bottom:14px; }
@@ -83,6 +87,20 @@ $_aiEnabled = strtolower(trim($_vars['AI_ENABLED'] ?? 'false')) === 'true';
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;">
@@ -220,6 +238,31 @@ $_aiEnabled = strtolower(trim($_vars['AI_ENABLED'] ?? 'false')) === 'true';
<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.
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>
<script>
@@ -486,4 +529,112 @@ 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>