Files
Varaverk/Plugin/unraid/pages/settings.php
T
Gmer4Lfe 0621f66889 Share one AI across the mesh instead of confining it to the owner
Curated state copied to every node is state that can disagree, so the index,
the model and the shared memory stay on the owner and each node reaches them
over the SSH trust onboarding already builds. Chats stay on the node that had
them; memory and bug reports stay the owner's to write.
2026-08-20 19:53:28 -04:00

527 lines
25 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. Drawn on every node. AI_ENABLED is a per-node switch — master.conf is gitignored, so each
// host carries its own — and every node has an assistant behind it, served by the AI owner's
// model over the mesh.
//
// It used to be gated on vv_is_ai_host(), from when AI was genuinely owner-only and the switch
// had nothing behind it anywhere else. That left the only surface that can turn AI on missing
// from exactly the nodes that needed it.
//
// vv_ai_ui_on() is not the test: it folds the switch into the 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';
$_aiOwner = vv_ai_owner_host();
$_aiOwnerNm = trim((string)($_vars[strtoupper($_aiOwner)] ?? $_aiOwner));
// 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; }
</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>
<!-- 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 —
<?= $_aiHost ? 'the AI tab, the assistant dock, and the AI tools on the Tools card'
: 'the assistant dock and the AI tools on the Tools card' ?></div>
</div>
</label>
<!-- Where the model lives. On a mirror this is the only place that says so — the AI tab,
which reports it in full, is owner-only. -->
<div class="vv-set-row" style="margin-bottom:10px;">
<span style="color:#666;font-size:11px;">Model</span>
<span style="font-size:11px;color:#4a7a4a;">
<?= $_aiHost ? 'runs on this node' : 'shared from ' . vvEscHtml($_aiOwnerNm) ?>
</span>
</div>
<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
<?= $_aiHost ? 'the AI tab is not in the tab bar, the assistant dock' : 'the 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.
<br>
<?php if ($_aiHost): ?>
<strong style="color:#a05a2c;">This node owns the model.</strong> The switch is still
per-node, but every other node is served from here — turning it off here takes AI off the
whole mesh, not just this box.
<?php else: ?>
The model, the index and the shared memory live on
<?= vvEscHtml($_aiOwnerNm) ?>, which answers for this node too. This switch is this node's
own: it controls whether the surfaces are drawn here, not whether they work anywhere else.
<?php endif; ?>
</div>
<span id="vv-set-ai-fb" style="font-size:11px;color:#444;"></span>
</div>
<!-- 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>
<?php vv_conf_ui_card('vv-cf-all', '*', 'All settings'); ?>
</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();
</script>