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:
@@ -68,10 +68,16 @@ foreach (VV_SCRIPT_CONF_SECTIONS as $script => $subs) {
|
||||
|
||||
// ── Route 2: pages that show sections by subject ─────────────────────────────────────────────
|
||||
foreach (VV_UI_SECTION_SURFACES as $surface) {
|
||||
$re = '/\b' . preg_quote((string) $surface['match'], '/') . '\b/i';
|
||||
// "*" is the catch-all, matched the same way api/confform.php matches it. preg_quote would
|
||||
// turn it into \* and quietly match nothing, which is how the map went on reporting a third
|
||||
// of the conf as unreachable after the page that reaches it had shipped.
|
||||
$all = ((string) $surface['match'] === '*');
|
||||
$re = $all ? '' : '/\b' . preg_quote((string) $surface['match'], '/') . '\b/i';
|
||||
foreach ($sections as $k => $g) {
|
||||
if (!preg_match($re, (string) $g['subsection'])) continue;
|
||||
$routes[$k][] = $surface['route'] . " → *{$g['subsection']}*";
|
||||
$name = (string) $g['subsection'];
|
||||
if (isset(VV_UI_SECTION_EXCLUDE[$name])) continue;
|
||||
if (!$all && !preg_match($re, $name)) continue;
|
||||
$routes[$k][] = $surface['route'] . " → *{$name}*";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,12 +200,31 @@ foreach ($sections as $k => $g) {
|
||||
}
|
||||
}
|
||||
|
||||
if ($unreachable) {
|
||||
// Two different kinds of "no route", and conflating them was unhelpful. One is a gap; the other
|
||||
// is a decision, and the decision has a reason worth repeating to whoever asks.
|
||||
$excluded = array_filter($unreachable, fn($g) => isset(VV_UI_SECTION_EXCLUDE[$g['subsection']]));
|
||||
$gaps = array_filter($unreachable, fn($g) => !isset(VV_UI_SECTION_EXCLUDE[$g['subsection']]));
|
||||
|
||||
if ($excluded) {
|
||||
$md .= "\n---\n\n## Settings deliberately kept out of the UI\n\n";
|
||||
$md .= "These have no control on purpose. They hold the machine's identity and the roots\n"
|
||||
. "everything else is derived from, and a text box beside a Save button is the wrong\n"
|
||||
. "shape for a value that decides whether the server recognises itself on next boot.\n\n"
|
||||
. "If one is asked about, give the reason and say it is edited in the conf file directly.\n"
|
||||
. "Do not describe a route — there is none, and that is the point.\n\n";
|
||||
foreach ($excluded as $g) {
|
||||
$keys = implode(', ', array_map(fn($f) => '`' . $f['key'] . '`', $g['fields']));
|
||||
$md .= '- **' . $g['subsection'] . '** (`' . $g['file'] . "`) — $keys \n"
|
||||
. ' ' . VV_UI_SECTION_EXCLUDE[$g['subsection']] . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
if ($gaps) {
|
||||
$md .= "\n---\n\n## Settings with no route through the UI\n\n";
|
||||
$md .= "These sections are not rendered by any page, so they can only be changed by editing\n"
|
||||
. "the conf file. If one of these is asked about, say so plainly rather than inventing a\n"
|
||||
. "route — mapping it into the Scheduler is a code change, not a setting.\n\n";
|
||||
foreach ($unreachable as $g) {
|
||||
$md .= "Not a decision, just not built yet: no page renders these, so they can only be changed\n"
|
||||
. "by editing the conf file. If one of these is asked about, say so plainly rather than\n"
|
||||
. "inventing a route.\n\n";
|
||||
foreach ($gaps as $g) {
|
||||
$keys = implode(', ', array_map(fn($f) => '`' . $f['key'] . '`', $g['fields']));
|
||||
$md .= '- **' . $g['subsection'] . '** (`' . $g['file'] . "`) — $keys\n";
|
||||
}
|
||||
|
||||
@@ -105,11 +105,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
//
|
||||
// preg_quote first: the needle arrives from a query string, so it is matched as a literal
|
||||
// with boundaries around it rather than as a pattern a caller could widen to everything.
|
||||
$re = '/\b' . preg_quote($match, '/') . '\b/i';
|
||||
// "*" is every section this host has. Deliberately not the same as an empty needle, which
|
||||
// would match everything by accident rather than on purpose.
|
||||
$all = ($match === '*');
|
||||
$re = $all ? '' : '/\b' . preg_quote($match, '/') . '\b/i';
|
||||
$out = [];
|
||||
foreach (vv_get_conf_files() as $f) {
|
||||
foreach (vv_conf_all_groups($f) as $g) {
|
||||
if (preg_match($re, (string) ($g['subsection'] ?? ''))) $out[] = $g;
|
||||
$name = (string) ($g['subsection'] ?? '');
|
||||
// Excluded everywhere, not only from the catch-all. A section kept out of forms
|
||||
// because a text box is the wrong control for it does not become the right
|
||||
// control because a different page asked.
|
||||
if (isset(VV_UI_SECTION_EXCLUDE[$name])) continue;
|
||||
if ($all || preg_match($re, $name)) $out[] = $g;
|
||||
}
|
||||
}
|
||||
echo json_encode(['ok' => true, 'groups' => $out]);
|
||||
|
||||
@@ -107,6 +107,14 @@ const VV_UI_SECTION_SURFACES = [
|
||||
'tab' => 'AI',
|
||||
'route' => 'AI tab → Settings → Configuration',
|
||||
],
|
||||
// Everything that is not structurally excluded. The catch-all exists so no ordinary setting
|
||||
// is reachable only by editing a file — a settings page whose answer to a third of the conf
|
||||
// is "open it over SSH" is not a settings page.
|
||||
[
|
||||
'match' => '*',
|
||||
'tab' => 'Settings',
|
||||
'route' => 'Settings tab → All settings',
|
||||
],
|
||||
[
|
||||
'match' => 'partnership',
|
||||
'tab' => 'Partnership',
|
||||
@@ -116,6 +124,30 @@ const VV_UI_SECTION_SURFACES = [
|
||||
],
|
||||
];
|
||||
|
||||
// Sections deliberately kept out of every settings surface, and why. Not a security boundary —
|
||||
// vv_conf_edit() still guards the write and the raw editor can still reach them. This is about
|
||||
// what belongs in a form: these hold the machine's identity and the roots everything else is
|
||||
// derived from, and a text box next to a Save button is the wrong shape for a value that decides
|
||||
// whether the server recognises itself on next boot.
|
||||
//
|
||||
// The reason travels with the exclusion because the UI map prints it. "No route" invites someone
|
||||
// to add one; "no route, because changing this moves the entire data tree" does not.
|
||||
const VV_UI_SECTION_EXCLUDE = [
|
||||
'HOST IDENTITIES' =>
|
||||
'HOST1 and HOST2 are what detect_hosts() matches the local hostname against. Editing one '
|
||||
. 'here would change which machine this believes it is.',
|
||||
'SHARED HOST CONFIGURATION' =>
|
||||
'DATA_DIR is the single on-disk root — move it and the whole tree follows, including the '
|
||||
. 'state the running scripts are holding open.',
|
||||
'Cache Roots' =>
|
||||
'The tmpfs half. These must never point at flash, and nothing in a form conveys that.',
|
||||
'State Files' =>
|
||||
'Paths to live state databases, derived from STATE_DIR. Repointing one orphans the state '
|
||||
. 'a watchdog is mid-way through writing.',
|
||||
'GIT / REPO' =>
|
||||
'Where the plugin updates itself from. Wrong here means the next pull is the last one.',
|
||||
];
|
||||
|
||||
// Pages that edit named keys rather than whole sections — a purpose-built control for one
|
||||
// setting, not a form over a conf section. Only the route is declared: the keys themselves are
|
||||
// read out of the page source by Tools/ui_map_build.php, so a control added or removed changes
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user