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.
641 lines
31 KiB
PHP
641 lines
31 KiB
PHP
<?php
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
// PURPOSE
|
|
// Settings tab. Varaverk's own configuration — storage mode, global toggles, and the conf
|
|
// fields that are not specific to any one subsystem.
|
|
//
|
|
// DESIGN PRINCIPLES
|
|
// Edits go through the same conf machinery the scheduler page uses (include/confform.php),
|
|
// so a value changed here is written exactly the way a value changed anywhere else is —
|
|
// surgically, preserving comments and ordering.
|
|
//
|
|
// Storage mode is presented as a migration, not a setting. Changing it moves the whole
|
|
// installation and regenerates the cron, which is why it routes through api/storage.php
|
|
// rather than a plain conf write.
|
|
//
|
|
// OPERATIONAL SAFEGUARDS
|
|
// The storage-mode change is confirmed before it runs. It is the most destructive operation
|
|
// reachable from the UI — rsync --delete, rm -rf, and a rewrite of every derived path.
|
|
//
|
|
// Conf writes are surgical. A settings page that regenerated master.conf would destroy the
|
|
// comment blocks that document every threshold in it.
|
|
//
|
|
// Values are escaped on render and on write-back.
|
|
//
|
|
// RENDERS
|
|
// Storage mode selector, global toggles, AI master switch, general conf fields
|
|
//
|
|
// DEPENDS ON
|
|
// api/confform.php conf read/write → include/confform.php
|
|
// api/storage.php storage-mode migration
|
|
$_myHost = vv_detect_host();
|
|
$_vars = vv_conf_vars();
|
|
$_myId = strtoupper($_myHost);
|
|
|
|
// Storage
|
|
$_confKey = $_myId . '_STORAGE_MODE_INTERNAL';
|
|
$_confVal = $_vars[$_confKey] ?? null;
|
|
|
|
// Notifications
|
|
$_notifyUnraid = ($_vars['NOTIFY_UNRAID'] ?? 'true') !== 'false';
|
|
$_enableLogging = ($_vars['ENABLE_LOGGING'] ?? 'false') === 'true';
|
|
$_discordKey = $_myId . '_DISCORD_WEBHOOK';
|
|
$_discordHook = $_vars[$_discordKey] ?? '';
|
|
|
|
// AI. The card is drawn only on the host that can actually run it — elsewhere AI_ENABLED is a
|
|
// switch with nothing behind it, since the model and the index are HOST1's. vv_ai_ui_on() is
|
|
// not the test here: it folds the switch into the host check, and this card is how the switch
|
|
// gets turned back on. Fail-closed on anything but "true", matching the gate it feeds.
|
|
$_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; }
|
|
.vv-set-hdr { font-size:11px;font-weight:700;color:#666;text-transform:uppercase;letter-spacing:.07em;margin-bottom:12px; }
|
|
.vv-set-row { display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin:4px 0; }
|
|
.vv-set-lbl { font-size:11px;color:#444; }
|
|
.vv-set-val { font-size:12px;color:#888;text-align:right;font-family:monospace; }
|
|
.vv-set-badge { display:inline-block;font-size:10px;padding:2px 8px;border-radius:3px;font-weight:700;letter-spacing:.04em; }
|
|
.vv-set-badge.internal { background:#0d1f0d;color:#4caf50;border:1px solid #1a3a1a; }
|
|
.vv-set-badge.flash { background:#1a1200;color:#ffb74d;border:1px solid #3a2800; }
|
|
.vv-set-badge.custom { background:#0a1a2a;color:#4a9eff;border:1px solid #1a3a5a; }
|
|
.vv-set-sep { border:none;border-top:1px solid #1e1e1e;margin:10px 0; }
|
|
.vv-set-btn { background:#1a1a1a;border:1px solid #333;color:#888;font-size:11px;padding:5px 14px;
|
|
border-radius:3px;cursor:pointer;transition:border-color .15s,color .15s; }
|
|
.vv-set-btn:hover { border-color:#555;color:#ccc; }
|
|
.vv-set-btn:disabled { opacity:.4;cursor:default; }
|
|
.vv-set-btn.primary { background:#1a2a1a;border-color:#2d4a2d;color:#4caf50; }
|
|
.vv-set-btn.primary:hover { background:#223a22; }
|
|
.vv-set-btn.warn { background:#1f1500;border-color:#3a2800;color:#ffb74d; }
|
|
.vv-set-btn.warn:hover { background:#2a1e00; }
|
|
.vv-set-out { background:#080808;border:1px solid #1a1a1a;border-radius:3px;padding:10px 12px;
|
|
font-family:monospace;font-size:10px;color:#555;white-space:pre-wrap;word-break:break-all;
|
|
max-height:320px;overflow-y:auto;margin-top:10px;display:none;
|
|
scrollbar-width:none; }
|
|
.vv-set-out::-webkit-scrollbar { display:none; }
|
|
.vv-set-info { font-size:11px;color:#444;line-height:1.6;margin-bottom:10px; }
|
|
.vv-set-warn-box { background:#1a1200;border:1px solid #3a2800;border-radius:4px;
|
|
padding:8px 12px;font-size:11px;color:#ffb74d;margin-top:8px;display:none; }
|
|
.vv-set-tog-lbl { font-size:12px;color:#888; }
|
|
.vv-set-tog-sub { font-size:10px;color:#3a3a3a;margin-top:1px; }
|
|
.vv-set-inp { background:#0d0d0d;border:1px solid #2a2a2a;border-radius:3px;color:#bbb;
|
|
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;">
|
|
|
|
<!-- Storage Location card -->
|
|
<div class="vv-set-card" id="vv-stor-card">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
|
<span class="vv-set-hdr" style="margin-bottom:0;">Storage Location</span>
|
|
<span id="vv-stor-badge" class="vv-set-badge">…</span>
|
|
</div>
|
|
|
|
<div id="vv-stor-info" style="font-size:11px;color:#3a3a3a;">Loading…</div>
|
|
|
|
<hr class="vv-set-sep">
|
|
|
|
<div class="vv-set-info">
|
|
Varaverk stores all scripts, configuration, state files, and the git repository in
|
|
<code style="font-size:10px;color:#4a7a4a;background:#080808;padding:1px 4px;border-radius:2px;">SCRIPTS_DIR</code>.
|
|
<br>
|
|
<strong style="color:#555;">Internal NVMe/SSD</strong> — direct access, zero write-wear concern, git pull/push from boot volume.<br>
|
|
<strong style="color:#555;">USB Flash</strong> — preserves flash lifetime. Requires array to be started. git pull syncs
|
|
<code style="font-size:10px;color:#4a7a4a;background:#080808;padding:1px 4px;border-radius:2px;">Plugin/</code>
|
|
back to <code style="font-size:10px;color:#4a7a4a;background:#080808;padding:1px 4px;border-radius:2px;">/boot/</code>
|
|
after each pull so the webUI always stays current.
|
|
</div>
|
|
|
|
<div id="vv-stor-warn-flash" class="vv-set-warn-box">
|
|
Array must be started for Varaverk to function in flash mode. The plugin tab is always accessible.
|
|
</div>
|
|
<div id="vv-stor-warn-noarray" class="vv-set-warn-box">
|
|
Array is not started — cannot migrate to flash mode right now. Start the array first.
|
|
</div>
|
|
|
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:10px;">
|
|
<button id="vv-stor-migrate-btn" class="vv-set-btn" onclick="vvStorMigrate()" style="display:none;"></button>
|
|
<button class="vv-set-btn" onclick="vvStorLoad()" style="font-size:10px;padding:4px 10px;">↻ Refresh</button>
|
|
<span id="vv-stor-fb" style="font-size:11px;color:#444;"></span>
|
|
</div>
|
|
|
|
<pre class="vv-set-out" id="vv-stor-out"></pre>
|
|
</div>
|
|
|
|
<!-- Notifications card -->
|
|
<div class="vv-set-card">
|
|
<div class="vv-set-hdr">Notifications</div>
|
|
|
|
<div style="display:flex;flex-direction:column;gap:10px;margin-bottom:14px;">
|
|
<label class="vv-cf-toggle">
|
|
<div class="vv-cf-track<?= $_notifyUnraid ? ' on' : '' ?>" id="vv-ntf-unraid"
|
|
onclick="vvNtfToggle(this,'NOTIFY_UNRAID')"></div>
|
|
<div>
|
|
<div class="vv-set-tog-lbl">Unraid native notifications</div>
|
|
<div class="vv-set-tog-sub">Appears in the Unraid bell icon — job completions, warnings, errors</div>
|
|
</div>
|
|
</label>
|
|
<label class="vv-cf-toggle">
|
|
<div class="vv-cf-track<?= $_enableLogging ? ' on' : '' ?>" id="vv-ntf-logging"
|
|
onclick="vvNtfToggle(this,'ENABLE_LOGGING')"></div>
|
|
<div>
|
|
<div class="vv-set-tog-lbl">Verbose logging</div>
|
|
<div class="vv-set-tog-sub">Adds detailed [LOG] lines to script output — useful for debugging scheduled runs</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<hr class="vv-set-sep">
|
|
|
|
<div style="margin-bottom:6px;">
|
|
<div style="font-size:11px;color:#555;margin-bottom:5px;">
|
|
Discord webhook
|
|
<span style="color:#2a2a2a;font-size:10px;margin-left:4px;"><?= htmlspecialchars($_discordKey) ?></span>
|
|
</div>
|
|
<div style="display:flex;gap:6px;align-items:center;">
|
|
<input class="vv-set-inp" id="vv-ntf-discord" type="text"
|
|
placeholder="https://discord.com/api/webhooks/…"
|
|
value="<?= htmlspecialchars($_discordHook) ?>">
|
|
<button class="vv-set-btn primary" onclick="vvNtfSaveWebhook()" id="vv-ntf-save"
|
|
style="white-space:nowrap;flex-shrink:0;">Save</button>
|
|
</div>
|
|
<div style="font-size:10px;color:#2a2a2a;margin-top:4px;">Leave blank to disable Discord notifications for this host</div>
|
|
</div>
|
|
<span id="vv-ntf-fb" style="font-size:11px;"></span>
|
|
</div>
|
|
|
|
<?php if ($_aiHost): ?>
|
|
<!-- AI card -->
|
|
<div class="vv-set-card">
|
|
<div class="vv-set-hdr">AI</div>
|
|
|
|
<label class="vv-cf-toggle" style="margin-bottom:12px;">
|
|
<div class="vv-cf-track<?= $_aiEnabled ? ' on' : '' ?>" id="vv-set-ai-tog"
|
|
onclick="vvSetAiToggle(this)"></div>
|
|
<div>
|
|
<div class="vv-set-tog-lbl">AI features</div>
|
|
<div class="vv-set-tog-sub">Master switch — the AI tab, the Scheduler assistant, and the AI tools on the Tools card</div>
|
|
</div>
|
|
</label>
|
|
|
|
<div class="vv-set-info">
|
|
Varaverk works exactly as well with this off. Every feature that can lean on AI has a
|
|
complete non-AI path, so turning it off removes the AI surfaces from the WebGUI and
|
|
nothing else — no script changes behaviour, no schedule changes.
|
|
<br>
|
|
Off is total: with it off the AI tab is not in the tab bar, the Scheduler's assistant dock
|
|
is not drawn, the AI tools are not listed, and
|
|
<code style="font-size:10px;color:#4a7a4a;background:#080808;padding:1px 4px;border-radius:2px;">api/ai.php</code>
|
|
refuses every request. This card is the way back on.
|
|
</div>
|
|
|
|
<span id="vv-set-ai-fb" style="font-size:11px;color:#444;"></span>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Unraid API Key card -->
|
|
<div class="vv-set-card">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
|
<span class="vv-set-hdr" style="margin-bottom:0;">Unraid API Keys</span>
|
|
<span id="vv-api-badge" class="vv-set-badge">…</span>
|
|
</div>
|
|
|
|
<div id="vv-api-hosts" style="margin-bottom:12px;"></div>
|
|
|
|
<div style="font-size:11px;color:#444;margin-bottom:12px;line-height:1.6;">
|
|
Each host registers its key in the local Unraid registry and pushes it to all partners over SSH.
|
|
Key values are stored in the private <code style="font-size:10px;color:#4a7a4a;background:#080808;padding:1px 4px;border-radius:2px;">host*.conf</code>
|
|
so each host can query every other host's GraphQL API directly — real-time monitoring, no SSH.
|
|
During offboard, partner keys are removed from each registry automatically.
|
|
</div>
|
|
|
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
|
<button class="vv-set-btn" onclick="vvApiCheck()" id="vv-api-check-btn">↻ Check status</button>
|
|
<button class="vv-set-btn primary" onclick="vvApiRenew()" id="vv-api-renew-btn">Renew</button>
|
|
<span id="vv-api-fb" style="font-size:11px;color:#444;"></span>
|
|
</div>
|
|
<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>
|
|
var _vvStorData = null;
|
|
|
|
function vvStorLoad() {
|
|
const info = document.getElementById('vv-stor-info');
|
|
const badge = document.getElementById('vv-stor-badge');
|
|
if (info) info.textContent = 'Loading…';
|
|
|
|
fetch('/plugins/varaverk/api/storage.php?action=status&_=' + Date.now())
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
_vvStorData = d;
|
|
if (!d.ok) { if (info) info.textContent = 'Error loading status'; return; }
|
|
_vvStorRender(d);
|
|
})
|
|
.catch(() => { if (info) info.textContent = 'Request failed'; });
|
|
}
|
|
|
|
function _vvStorRender(d) {
|
|
const badge = document.getElementById('vv-stor-badge');
|
|
const info = document.getElementById('vv-stor-info');
|
|
const btn = document.getElementById('vv-stor-migrate-btn');
|
|
const wFlash = document.getElementById('vv-stor-warn-flash');
|
|
const wArr = document.getElementById('vv-stor-warn-noarray');
|
|
|
|
// Badge
|
|
if (badge) {
|
|
badge.textContent = d.current_mode.toUpperCase();
|
|
badge.className = 'vv-set-badge ' + d.current_mode;
|
|
}
|
|
|
|
// Info grid
|
|
const modeLabel = d.current_mode === 'internal' ? 'Internal NVMe/SSD' : d.current_mode === 'flash' ? 'USB Flash' : 'Custom';
|
|
const detLabel = d.detected === 'internal' ? 'internal NVMe/SSD' : 'USB flash';
|
|
const confBadge = d.conf_val === null ? '<span style="color:#3a3a3a;">not set</span>'
|
|
: d.conf_val === 'true' ? '<span style="color:#4caf50;">true (internal)</span>'
|
|
: '<span style="color:#ffb74d;">false (flash)</span>';
|
|
const matchIcon = (d.current_mode === d.detected || d.current_mode === 'custom') ? '' :
|
|
' <span style="color:#ef5350;">⚠ mismatch with conf</span>';
|
|
|
|
if (info) info.innerHTML = `
|
|
<div style="display:grid;grid-template-columns:auto 1fr;gap:3px 16px;align-items:baseline;">
|
|
<span style="color:#333;">Mode</span> <span style="color:#888;">${modeLabel}${matchIcon}</span>
|
|
<span style="color:#333;">SCRIPTS_DIR</span> <code style="font-size:10px;color:#4a7a4a;">${d.current_dir}</code>
|
|
<span style="color:#333;">Boot device</span> <span style="color:#555;">${d.boot_disk} <span style="color:#2a2a2a;">(${d.transport})</span></span>
|
|
<span style="color:#333;">Auto-detect</span> <span style="color:#555;">${detLabel}</span>
|
|
<span style="color:#333;">${d.conf_key}</span> <span>${confBadge}</span>
|
|
</div>`;
|
|
|
|
// Warnings
|
|
if (wFlash) wFlash.style.display = (d.current_mode !== 'flash' && d.detected === 'usb') ? '' : 'none';
|
|
if (wArr) wArr.style.display = 'none';
|
|
|
|
// Migrate button
|
|
if (btn) {
|
|
if (d.current_mode === 'internal') {
|
|
btn.textContent = 'Migrate to Flash (appdata)';
|
|
btn.className = 'vv-set-btn warn';
|
|
btn.dataset.to = 'flash';
|
|
btn.style.display = '';
|
|
} else if (d.current_mode === 'flash') {
|
|
btn.textContent = 'Migrate to Internal (/boot)';
|
|
btn.className = 'vv-set-btn primary';
|
|
btn.dataset.to = 'internal';
|
|
btn.style.display = '';
|
|
} else {
|
|
btn.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
|
|
async function vvStorMigrate() {
|
|
const btn = document.getElementById('vv-stor-migrate-btn');
|
|
const out = document.getElementById('vv-stor-out');
|
|
const fb = document.getElementById('vv-stor-fb');
|
|
const wArr = document.getElementById('vv-stor-warn-noarray');
|
|
const to = btn?.dataset.to;
|
|
if (!to) return;
|
|
|
|
// Flash guard: array must be started
|
|
if (to === 'flash' && _vvStorData && !_vvStorData.array_started) {
|
|
if (wArr) wArr.style.display = '';
|
|
return;
|
|
}
|
|
|
|
const label = to === 'flash' ? 'Flash (appdata)' : 'Internal (/boot)';
|
|
if (!await vvConfirm(`Migrate Varaverk storage to ${label}?\n\nThis will:\n• Copy all scripts, conf, and git repo to the new location\n• Update varaverk.cfg and master.conf\n• Delete the old location\n\nThe page will need a reload after migration.`)) return;
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = 'Migrating…';
|
|
if (fb) fb.textContent = '';
|
|
if (out) { out.textContent = 'Starting migration…\n'; out.style.display = ''; }
|
|
|
|
const fd = new URLSearchParams();
|
|
fd.append('action', 'migrate');
|
|
fd.append('to', to);
|
|
|
|
fetch('/plugins/varaverk/api/storage.php', { method: 'POST', body: fd })
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
btn.disabled = false;
|
|
if (out) {
|
|
out.textContent = d.output || '(no output)';
|
|
out.scrollTop = out.scrollHeight;
|
|
}
|
|
if (d.ok) {
|
|
btn.textContent = '✓ Done — reload page';
|
|
btn.className = 'vv-set-btn primary';
|
|
btn.onclick = () => location.reload();
|
|
if (fb) { fb.style.color = '#4caf50'; fb.textContent = 'Migration complete — reload to apply'; }
|
|
} else {
|
|
btn.textContent = btn.dataset.to === 'flash' ? 'Migrate to Flash (appdata)' : 'Migrate to Internal (/boot)';
|
|
if (fb) { fb.style.color = '#ef5350'; fb.textContent = 'Migration failed — see output above'; }
|
|
}
|
|
})
|
|
.catch(e => {
|
|
btn.disabled = false;
|
|
btn.textContent = btn.dataset.to === 'flash' ? 'Migrate to Flash (appdata)' : 'Migrate to Internal (/boot)';
|
|
if (fb) { fb.style.color = '#ef5350'; fb.textContent = 'Request failed: ' + e; }
|
|
});
|
|
}
|
|
|
|
vvStorLoad();
|
|
|
|
// ── Notifications ─────────────────────────────────────────────────────────────
|
|
function vvNtfToggle(track, key) {
|
|
const on = !track.classList.contains('on');
|
|
track.classList.toggle('on', on);
|
|
const file = (key === 'NOTIFY_UNRAID' || key === 'ENABLE_LOGGING') ? 'master.conf'
|
|
: '<?= htmlspecialchars($_myHost) ?>.conf';
|
|
const fd = new URLSearchParams();
|
|
fd.append('id', 'settings');
|
|
fd.append('changes', JSON.stringify([{ file, key, value: on ? 'true' : 'false', type: 'scalar' }]));
|
|
fetch('/plugins/varaverk/api/confform.php', { method: 'POST', body: fd })
|
|
.then(r => r.json())
|
|
.then(d => { if (!d.ok) track.classList.toggle('on', !on); })
|
|
.catch(() => track.classList.toggle('on', !on));
|
|
}
|
|
|
|
function vvNtfSaveWebhook() {
|
|
const inp = document.getElementById('vv-ntf-discord');
|
|
const fb = document.getElementById('vv-ntf-fb');
|
|
const btn = document.getElementById('vv-ntf-save');
|
|
btn.disabled = true; btn.textContent = 'Saving…'; fb.textContent = '';
|
|
const fd = new URLSearchParams();
|
|
fd.append('id', 'settings');
|
|
fd.append('changes', JSON.stringify([{
|
|
file: '<?= htmlspecialchars($_myHost) ?>.conf',
|
|
key: '<?= htmlspecialchars($_discordKey) ?>',
|
|
value: inp.value.trim(),
|
|
type: 'scalar',
|
|
}]));
|
|
fetch('/plugins/varaverk/api/confform.php', { method: 'POST', body: fd })
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
btn.disabled = false; btn.textContent = 'Save';
|
|
fb.style.color = d.ok ? '#4caf50' : '#ef5350';
|
|
fb.textContent = d.ok ? 'Saved ✓' : (d.error || 'Failed');
|
|
if (d.ok) setTimeout(() => { fb.textContent = ''; }, 3000);
|
|
})
|
|
.catch(() => { btn.disabled = false; btn.textContent = 'Save'; fb.style.color='#ef5350'; fb.textContent='Request failed'; });
|
|
}
|
|
|
|
// ── AI ────────────────────────────────────────────────────────────────────────
|
|
// Reloads on success, unlike the notification toggles, because this one changes the page around
|
|
// it: the tab bar and the assistant dock are decided server-side at render, so without a reload
|
|
// the switch would read "off" with the AI tab still sitting next to it. The switch is moved
|
|
// first and reverted if the write fails, matching the toggles above.
|
|
function vvSetAiToggle(track) {
|
|
const on = !track.classList.contains('on');
|
|
const fb = document.getElementById('vv-set-ai-fb');
|
|
track.classList.toggle('on', on);
|
|
fb.style.color = '#444';
|
|
fb.textContent = 'Saving…';
|
|
|
|
const fd = new URLSearchParams();
|
|
fd.append('id', 'settings');
|
|
fd.append('changes', JSON.stringify([
|
|
{ file: 'master.conf', key: 'AI_ENABLED', value: on ? 'true' : 'false', type: 'scalar' }
|
|
]));
|
|
fetch('/plugins/varaverk/api/confform.php', { method: 'POST', body: fd })
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
if (!d.ok) {
|
|
track.classList.toggle('on', !on);
|
|
fb.style.color = '#ef5350';
|
|
fb.textContent = d.error || 'Failed';
|
|
return;
|
|
}
|
|
fb.style.color = '#4caf50';
|
|
fb.textContent = (on ? 'Enabled' : 'Disabled') + ' — reloading…';
|
|
setTimeout(() => location.reload(), 700);
|
|
})
|
|
.catch(() => {
|
|
track.classList.toggle('on', !on);
|
|
fb.style.color = '#ef5350';
|
|
fb.textContent = 'Request failed';
|
|
});
|
|
}
|
|
|
|
// ── Unraid API Keys ───────────────────────────────────────────────────────────
|
|
function vvApiCheck() {
|
|
const btn = document.getElementById('vv-api-check-btn');
|
|
const badge = document.getElementById('vv-api-badge');
|
|
const hosts = document.getElementById('vv-api-hosts');
|
|
btn.disabled = true; btn.textContent = 'Checking…';
|
|
fetch('/plugins/varaverk/api/storage.php?action=api_status&_=' + Date.now())
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
btn.disabled = false; btn.textContent = '↻ Check status';
|
|
if (!d.ok) return;
|
|
const allOk = d.hosts.every(h => h.api_ok);
|
|
const anyMissing = d.hosts.some(h => !h.key_present);
|
|
badge.textContent = allOk ? 'all ok' : anyMissing ? 'keys missing' : 'partial';
|
|
badge.className = 'vv-set-badge ' + (allOk ? 'internal' : 'flash');
|
|
|
|
if (hosts) hosts.innerHTML = d.hosts.map(h => {
|
|
const tag = h.is_local
|
|
? `<span style="color:#2a3a2a;font-size:9px;margin-left:4px;">own</span>`
|
|
: `<span style="color:#2a2a3a;font-size:9px;margin-left:4px;">${d.my_id}'s key on ${h.host_id}</span>`;
|
|
const keyBit = h.key_present
|
|
? `<code style="font-size:9px;color:#4a6a4a;">${h.key_preview}</code>`
|
|
: `<span style="color:#8b2a2a;font-size:10px;">not set — click Renew</span>`;
|
|
const dot = h.api_ok ? '#4caf50' : (h.key_present ? '#ffb74d' : '#444');
|
|
return `<div style="display:flex;align-items:center;gap:8px;padding:4px 0;border-bottom:1px solid #111;">
|
|
<span style="width:6px;height:6px;border-radius:50%;background:${dot};flex-shrink:0;display:inline-block;"></span>
|
|
<span style="font-size:11px;color:#666;flex-shrink:0;min-width:50px;">${h.host_id}</span>
|
|
<code style="font-size:9px;color:#2a3a2a;flex-shrink:0;">${h.is_local ? d.key_name : h.key_var}</code>
|
|
${tag}
|
|
<span style="flex:1;text-align:right;">${keyBit}</span>
|
|
</div>`;
|
|
}).join('');
|
|
})
|
|
.catch(() => { btn.disabled = false; btn.textContent = '↻ Check status'; });
|
|
}
|
|
|
|
function vvApiRenew() {
|
|
const btn = document.getElementById('vv-api-renew-btn');
|
|
const out = document.getElementById('vv-api-out');
|
|
const fb = document.getElementById('vv-api-fb');
|
|
const badge = document.getElementById('vv-api-badge');
|
|
btn.disabled = true; btn.textContent = 'Renewing…';
|
|
fb.textContent = ''; out.textContent = ''; out.style.display = '';
|
|
|
|
const fd = new URLSearchParams();
|
|
fd.append('action', 'setup_apikeys');
|
|
fetch('/plugins/varaverk/api/storage.php', { method: 'POST', body: fd })
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
btn.disabled = false; btn.textContent = 'Renew';
|
|
out.textContent = d.output || '(no output)';
|
|
out.scrollTop = out.scrollHeight;
|
|
fb.style.color = d.ok ? '#4caf50' : '#ef5350';
|
|
fb.textContent = d.ok ? '✓ Done' : 'Failed — see output';
|
|
if (d.ok) { badge.textContent = ''; vvApiCheck(); }
|
|
})
|
|
.catch(() => {
|
|
btn.disabled = false; btn.textContent = 'Renew';
|
|
fb.style.color = '#ef5350'; fb.textContent = 'Request failed';
|
|
});
|
|
}
|
|
|
|
// 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>
|