A multipart POST to the plugin API hangs and never completes on this host: no status code, and no server-side trace of any kind. The correlation was exact — every page using URLSearchParams worked, every page using FormData hung, which is why scheduler and partnership appeared fine while docker, rsync, settings, auth, arrs, fallback and monitor did not. URLSearchParams has the same append API and fetch sets the urlencoded content type for it, so each site is a one-token change with the payload logic untouched.
424 lines
20 KiB
PHP
424 lines
20 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, 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] ?? '';
|
|
|
|
?>
|
|
<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-wrap { display:flex;align-items:center;gap:10px;cursor:pointer;user-select:none; }
|
|
.vv-set-tog-track{ width:32px;height:18px;border-radius:9px;background:#222;border:1px solid #333;
|
|
position:relative;transition:background .15s,border-color .15s;flex-shrink:0; }
|
|
.vv-set-tog-track.on { background:#1a3a1a;border-color:#2d5a2d; }
|
|
.vv-set-tog-track::after { content:'';position:absolute;top:2px;left:2px;width:12px;height:12px;
|
|
border-radius:50%;background:#555;transition:left .15s,background .15s; }
|
|
.vv-set-tog-track.on::after { left:16px;background:#4caf50; }
|
|
.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:#888;
|
|
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-set-tog-wrap">
|
|
<div class="vv-set-tog-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-set-tog-wrap">
|
|
<div class="vv-set-tog-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>
|
|
|
|
<!-- 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>
|
|
|
|
</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';
|
|
}
|
|
}
|
|
}
|
|
|
|
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 (!confirm(`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'; });
|
|
}
|
|
|
|
// ── 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>
|