// Config Options - Centralized configuration options for dropdown fields class ConfigOptions { // Per-app form renders id="GLUETUN_VPN_*"; global form renders id="config-CFG_GLUETUN_VPN_*". static findGluetunProviderEl() { return document.getElementById('config-CFG_GLUETUN_VPN_SERVICE_PROVIDER') || document.getElementById('GLUETUN_VPN_SERVICE_PROVIDER'); } static findGluetunVpnTypeEl() { return document.getElementById('config-CFG_GLUETUN_VPN_TYPE') || document.getElementById('GLUETUN_VPN_TYPE'); } static findGluetunFieldEl(suffix) { return document.getElementById(`config-CFG_GLUETUN_${suffix}`) || document.getElementById(`GLUETUN_${suffix}`); } // Show only the credential fields that match the selected VPN type. static refreshGluetunCredentialVisibility() { const typeEl = this.findGluetunVpnTypeEl(); if (!typeEl) return; const type = (typeEl.value || '').toLowerCase(); const wg = ['WIREGUARD_PRIVATE_KEY', 'WIREGUARD_ADDRESSES']; const ov = ['OPENVPN_USER', 'OPENVPN_PASSWORD']; const setVisible = (suffix, visible) => { const el = this.findGluetunFieldEl(suffix); if (!el) return; const wrapper = el.closest('.form-field') || el.parentElement; if (wrapper) wrapper.style.display = visible ? '' : 'none'; }; wg.forEach((s) => setVisible(s, type === 'wireguard')); ov.forEach((s) => setVisible(s, type === 'openvpn')); this.refreshMullvadGenerateButton(type); } static refreshMullvadGenerateButton(typeValue) { const providerEl = this.findGluetunProviderEl(); const provider = (providerEl?.value || '').toLowerCase(); const type = (typeValue || this.findGluetunVpnTypeEl()?.value || '').toLowerCase(); const shouldShow = provider === 'mullvad' && type === 'wireguard'; document.querySelectorAll('.mullvad-generate-field').forEach(b => b.remove()); if (!shouldShow) return; const typeEl = this.findGluetunVpnTypeEl(); const anchor = typeEl && typeEl.closest('.form-field'); if (!anchor) return; const keyEl = this.findGluetunFieldEl('WIREGUARD_PRIVATE_KEY'); const addrEl = this.findGluetunFieldEl('WIREGUARD_ADDRESSES'); const configured = !!(keyEl?.value && addrEl?.value); const block = document.createElement('div'); block.className = 'form-field mullvad-generate-field'; block.innerHTML = `
${configured ? '✓' : ''} ${configured ? 'Configured' : 'Not configured'}
Generates a WireGuard key against your Mullvad account and fills the credentials below. `; block.querySelector('.mullvad-generate-btn') .addEventListener('click', () => window.appsManager?.openMullvadGenerateModal?.()); anchor.insertAdjacentElement('afterend', block); } static refreshMullvadGenerateStatus() { const status = document.querySelector('.mullvad-generate-field .mullvad-generate-status'); if (!status) return; const keyEl = this.findGluetunFieldEl('WIREGUARD_PRIVATE_KEY'); const addrEl = this.findGluetunFieldEl('WIREGUARD_ADDRESSES'); const configured = !!(keyEl?.value && addrEl?.value); status.classList.toggle('is-configured', configured); status.querySelector('.mullvad-generate-tick').textContent = configured ? '✓' : ''; status.querySelector('.mullvad-generate-status-text').textContent = configured ? 'Configured' : 'Not configured'; } static loadGluetunProviderIcons() { if (this._gluetunIconsPromise) return this._gluetunIconsPromise; this._gluetunIconsPromise = (async () => { try { const res = await fetch('/data/apps/gluetun-provider-icons.json', { cache: 'no-store' }); if (!res.ok) return {}; return await res.json(); } catch { return {}; } })(); this._gluetunIconsPromise.then((m) => { window.gluetunProviderIcons = m || {}; }); return this._gluetunIconsPromise; } static _gluetunIconsPromise = null; // Pulled from gluetun's upstream servers.json by webuiGenerateGluetunProviders. // Generator writes to /data/apps/generated/; static fallback ships at /data/apps/. // We try generated first, fall back to bundled, fall back to a tiny static list. static _gluetunProvidersPromise = null; static loadGluetunProviders() { if (this._gluetunProvidersPromise) return this._gluetunProvidersPromise; this._gluetunProvidersPromise = (async () => { const tryFetch = async (url) => { try { const res = await fetch(url, { cache: 'no-store' }); if (!res.ok) return null; const json = await res.json(); return json && json.providers ? json.providers : null; } catch { return null; } }; const live = await tryFetch('/data/apps/generated/gluetun-providers.json'); if (live) return live; const fallback = await tryFetch('/data/apps/gluetun-providers.json'); if (fallback) return fallback; return null; })(); this._gluetunProvidersPromise.then((p) => { window.gluetunProviders = p; // If the provider field rendered before this finished, repopulate it // (and the VPN-type field, which depends on the selected provider). const sel = ConfigOptions.findGluetunProviderEl(); if (sel && p) { const previous = sel.value; const opts = ConfigOptions.getGluetunProviderOptions(); sel.innerHTML = opts.map((o) => `` ).join(''); ConfigOptions.refreshGluetunVpnTypeOptions(); } ConfigOptions.refreshGluetunCredentialVisibility(); }); return this._gluetunProvidersPromise; } static getGluetunProviderOptions() { const providers = window.gluetunProviders; if (!providers) { this.loadGluetunProviders(); return [ { value: 'mullvad', label: 'Mullvad' }, { value: 'nordvpn', label: 'NordVPN' }, { value: 'protonvpn', label: 'ProtonVPN' }, { value: 'surfshark', label: 'Surfshark' }, { value: 'custom', label: 'Custom (manual config)' } ]; } const titleCase = (s) => s.replace(/\b\w/g, (c) => c.toUpperCase()); return Object.keys(providers).sort().map((slug) => ({ value: slug, label: slug === 'custom' ? 'Custom (manual config)' : titleCase(slug) })); } // Repaint the VPN-type changes, rebuild the // VPN-type