// 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