Audit follow-up — after a full-repo sweep, the only remaining functional /docker refs are intentional (the legacy compat shim + the env-overridden legacy-safe backend default). Fix the last user-visible/stale ones: - config-options.js: backup PATH_MODE 'auto' label no longer hardcodes /docker/backups (the path is relocatable) — describes the behaviour instead. - config.js / setup-detector.js / webui_install_image.sh: refresh comments that named /docker to the relocatable system/containers roots. No behaviour change. Active container app scripts already use $containers_dir; the remaining /docker hits across the tree are docker-compose.yml filenames, /var/lib/docker, the docker binary, relative array paths, docs/site, and the unused/ graveyard. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
470 lines
20 KiB
JavaScript
Executable File
470 lines
20 KiB
JavaScript
Executable File
// 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 = `
|
|
<label class="form-label">Mullvad WireGuard</label>
|
|
<div class="mullvad-generate-actions">
|
|
<button type="button" class="btn btn-secondary mullvad-generate-btn">Generate from Mullvad account</button>
|
|
<span class="mullvad-generate-status${configured ? ' is-configured' : ''}">
|
|
<span class="mullvad-generate-tick">${configured ? '✓' : ''}</span>
|
|
<span class="mullvad-generate-status-text">${configured ? 'Configured' : 'Not configured'}</span>
|
|
</span>
|
|
</div>
|
|
<small class="form-help">Generates a WireGuard key against your Mullvad account and fills the credentials below.</small>
|
|
`;
|
|
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) =>
|
|
`<option value="${o.value}" ${o.value === previous ? 'selected' : ''}>${o.label}</option>`
|
|
).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 <select> based on the currently chosen provider.
|
|
// Called from a delegated change listener so we don't touch the renderer.
|
|
static refreshGluetunVpnTypeOptions() {
|
|
const sel = ConfigOptions.findGluetunVpnTypeEl();
|
|
if (!sel) return;
|
|
const previous = sel.value;
|
|
const opts = this.getGluetunVpnTypeOptions();
|
|
const stillValid = opts.some((o) => o.value === previous);
|
|
sel.innerHTML = opts.map((o) =>
|
|
`<option value="${o.value}" ${(stillValid ? o.value === previous : false) || (!stillValid && o.value === opts[0].value) ? 'selected' : ''}>${o.label}</option>`
|
|
).join('');
|
|
this.refreshGluetunCredentialVisibility();
|
|
}
|
|
|
|
static getGluetunVpnTypeOptions() {
|
|
const providers = window.gluetunProviders;
|
|
const selected = (typeof document !== 'undefined' && ConfigOptions.findGluetunProviderEl())
|
|
? ConfigOptions.findGluetunProviderEl().value
|
|
: null;
|
|
if (providers && selected && providers[selected] && Array.isArray(providers[selected].vpnTypes) && providers[selected].vpnTypes.length) {
|
|
return providers[selected].vpnTypes.map((t) => ({
|
|
value: t,
|
|
label: t === 'openvpn' ? 'OpenVPN' : (t === 'wireguard' ? 'WireGuard' : t)
|
|
}));
|
|
}
|
|
return [
|
|
{ value: 'wireguard', label: 'WireGuard' },
|
|
{ value: 'openvpn', label: 'OpenVPN' }
|
|
];
|
|
}
|
|
|
|
// Get select options for specific config keys
|
|
static getSelectOptions(key) {
|
|
//console.log('=== getSelectOptions ENTRY === called with:', key);
|
|
const optionMaps = {
|
|
'CFG_DOCKER_INSTALL_TYPE': [
|
|
{ value: 'rooted', label: 'Rooted' },
|
|
{ value: 'rootless', label: 'Rootless' }
|
|
],
|
|
'CFG_ROOTLESS_NET': [
|
|
{ value: 'pasta', label: 'Pasta (recommended)' },
|
|
{ value: 'slirp4netns', label: 'slirp4netns (fallback)' }
|
|
],
|
|
'CFG_UFW_LOGGING': [
|
|
{ value: 'off', label: 'Off' },
|
|
{ value: 'low', label: 'Low' },
|
|
{ value: 'medium', label: 'Medium' },
|
|
{ value: 'high', label: 'High' },
|
|
{ value: 'full', label: 'Full' }
|
|
],
|
|
'CFG_TEXT_EDITOR': [
|
|
{ value: 'nano', label: 'Nano' },
|
|
{ value: 'vim', label: 'Vim' }
|
|
],
|
|
'CFG_INSTALL_MODE': [
|
|
{ value: 'git', label: 'Git Repository' },
|
|
{ value: 'local', label: 'Local Folder' }
|
|
],
|
|
'CFG_MAIL_SECURE': [
|
|
{ value: 'tls', label: 'TLS' },
|
|
{ value: 'ssl', label: 'SSL' },
|
|
{ value: 'none', label: 'None' }
|
|
],
|
|
'CFG_TRAEFIK_DASHBOARD_ACCESS': [
|
|
{ value: 'local-only', label: 'Local + Domain (recommended)' },
|
|
{ value: 'domain-only', label: 'Domain only (most secure)' },
|
|
{ value: 'public', label: 'Public + Domain (legacy, unauthenticated on :8080)' }
|
|
],
|
|
};
|
|
|
|
if (key === 'CFG_GLUETUN_VPN_SERVICE_PROVIDER') return this.getGluetunProviderOptions();
|
|
if (key === 'CFG_GLUETUN_VPN_TYPE') return this.getGluetunVpnTypeOptions();
|
|
|
|
// Generator-emitted options take precedence — they come from the
|
|
// [value:Label|...] block in the config file's inline comment, so a
|
|
// CFG file change auto-flows through to the dropdown without
|
|
// touching this file. The hardcoded cases below stay as a safety
|
|
// net for keys whose options are computed or theme-specific.
|
|
const generated = window.configData && window.configData.config &&
|
|
window.configData.config[key] &&
|
|
window.configData.config[key].options;
|
|
if (Array.isArray(generated) && generated.length > 0) {
|
|
return generated;
|
|
}
|
|
|
|
// Per-location backup dropdowns (CFG_BACKUP_LOC_<n>_*). Their options are
|
|
// static and identical for every location, but the config generator only
|
|
// scans flat per-category files — it never descends into the nested
|
|
// configs/backup/locations/<n>/ dir — so configData carries no options for
|
|
// these keys. Resolve them here by suffix so every location index works,
|
|
// including ones added after install. (The global CFG_BACKUP_ENGINE/
|
|
// STRATEGY still come from the generator via the check above.)
|
|
const locDropdown = key.match(/^CFG_BACKUP_LOC_[0-9]+_(TYPE|PATH_MODE|ENGINE|SSH_AUTH)$/);
|
|
if (locDropdown) {
|
|
const BACKUP_LOC_OPTIONS = {
|
|
TYPE: [
|
|
{ value: 'local', label: 'Local / mounted path' },
|
|
{ value: 'sftp', label: 'SFTP' },
|
|
{ value: 'rest', label: 'REST' },
|
|
{ value: 's3', label: 'S3' },
|
|
{ value: 'b2', label: 'Backblaze B2' },
|
|
{ value: 'gs', label: 'Google Cloud Storage' },
|
|
{ value: 'azure', label: 'Azure' },
|
|
{ value: 'rclone', label: 'rclone' }
|
|
],
|
|
PATH_MODE: [
|
|
{ value: 'auto', label: 'Automatic (backups root, one subfolder per location)' },
|
|
{ value: 'custom', label: 'Custom path' }
|
|
],
|
|
ENGINE: [
|
|
{ value: 'restic', label: 'Restic' },
|
|
{ value: 'borg', label: 'BorgBackup' },
|
|
{ value: 'kopia', label: 'Kopia' }
|
|
],
|
|
SSH_AUTH: [
|
|
{ value: 'key', label: 'SSH key (~/.ssh/id_rsa)' },
|
|
{ value: 'password', label: 'Password (via sshpass)' }
|
|
]
|
|
};
|
|
return BACKUP_LOC_OPTIONS[locDropdown[1]];
|
|
}
|
|
|
|
const result = optionMaps[key] || [];
|
|
//console.log('Final result for', key, ':', result);
|
|
return result;
|
|
}
|
|
|
|
// Get comprehensive timezone options
|
|
static getTimezoneOptions() {
|
|
return [
|
|
{ value: 'Etc/UTC', label: 'Etc/UTC' },
|
|
{ value: 'America/New_York', label: 'America/New_York' },
|
|
{ value: 'America/Chicago', label: 'America/Chicago' },
|
|
{ value: 'America/Denver', label: 'America/Denver' },
|
|
{ value: 'America/Los_Angeles', label: 'America/Los_Angeles' },
|
|
{ value: 'America/Anchorage', label: 'America/Anchorage' },
|
|
{ value: 'America/Honolulu', label: 'America/Honolulu' },
|
|
{ value: 'America/Toronto', label: 'America/Toronto' },
|
|
{ value: 'America/Vancouver', label: 'America/Vancouver' },
|
|
{ value: 'America/Mexico_City', label: 'America/Mexico_City' },
|
|
{ value: 'America/Sao_Paulo', label: 'America/Sao_Paulo' },
|
|
{ value: 'Europe/London', label: 'Europe/London' },
|
|
{ value: 'Europe/Paris', label: 'Europe/Paris' },
|
|
{ value: 'Europe/Berlin', label: 'Europe/Berlin' },
|
|
{ value: 'Europe/Rome', label: 'Europe/Rome' },
|
|
{ value: 'Europe/Madrid', label: 'Europe/Madrid' },
|
|
{ value: 'Europe/Amsterdam', label: 'Europe/Amsterdam' },
|
|
{ value: 'Europe/Brussels', label: 'Europe/Brussels' },
|
|
{ value: 'Europe/Zurich', label: 'Europe/Zurich' },
|
|
{ value: 'Europe/Vienna', label: 'Europe/Vienna' },
|
|
{ value: 'Europe/Stockholm', label: 'Europe/Stockholm' },
|
|
{ value: 'Europe/Oslo', label: 'Europe/Oslo' },
|
|
{ value: 'Europe/Copenhagen', label: 'Europe/Copenhagen' },
|
|
{ value: 'Europe/Helsinki', label: 'Europe/Helsinki' },
|
|
{ value: 'Europe/Warsaw', label: 'Europe/Warsaw' },
|
|
{ value: 'Europe/Prague', label: 'Europe/Prague' },
|
|
{ value: 'Europe/Budapest', label: 'Europe/Budapest' },
|
|
{ value: 'Europe/Moscow', label: 'Europe/Moscow' },
|
|
{ value: 'Asia/Dubai', label: 'Asia/Dubai' },
|
|
{ value: 'Asia/Kolkata', label: 'Asia/Kolkata' },
|
|
{ value: 'Asia/Shanghai', label: 'Asia/Shanghai' },
|
|
{ value: 'Asia/Hong_Kong', label: 'Asia/Hong_Kong' },
|
|
{ value: 'Asia/Tokyo', label: 'Asia/Tokyo' },
|
|
{ value: 'Asia/Seoul', label: 'Asia/Seoul' },
|
|
{ value: 'Asia/Singapore', label: 'Asia/Singapore' },
|
|
{ value: 'Asia/Bangkok', label: 'Asia/Bangkok' },
|
|
{ value: 'Asia/Jakarta', label: 'Asia/Jakarta' },
|
|
{ value: 'Australia/Sydney', label: 'Australia/Sydney' },
|
|
{ value: 'Australia/Melbourne', label: 'Australia/Melbourne' },
|
|
{ value: 'Australia/Perth', label: 'Australia/Perth' },
|
|
{ value: 'Pacific/Auckland', label: 'Pacific/Auckland' },
|
|
{ value: 'Africa/Cairo', label: 'Africa/Cairo' },
|
|
{ value: 'Africa/Lagos', label: 'Africa/Lagos' },
|
|
{ value: 'Africa/Johannesburg', label: 'Africa/Johannesburg' }
|
|
];
|
|
}
|
|
|
|
// Check if a config key should use dropdown
|
|
static isDropdownKey(key) {
|
|
//console.log('ConfigOptions.isDropdownKey called with:', key);
|
|
const result = key === 'CFG_DOCKER_INSTALL_TYPE' ||
|
|
key === 'CFG_UFW_LOGGING' ||
|
|
key === 'CFG_TEXT_EDITOR' ||
|
|
/^CFG_BACKUP_LOC_[0-9]+_TYPE$/.test(key) ||
|
|
/^CFG_BACKUP_LOC_[0-9]+_ENGINE$/.test(key) ||
|
|
key === 'CFG_BACKUP_ENGINE' ||
|
|
/^CFG_BACKUP_LOC_[0-9]+_SSH_AUTH$/.test(key) ||
|
|
/^CFG_BACKUP_LOC_[0-9]+_PATH_MODE$/.test(key) ||
|
|
key === 'CFG_BACKUP_STRATEGY' ||
|
|
key === 'CFG_INSTALL_MODE' ||
|
|
key === 'CFG_MAIL_SECURE' ||
|
|
key === 'CFG_GLUETUN_VPN_SERVICE_PROVIDER' ||
|
|
key === 'CFG_GLUETUN_VPN_TYPE' ||
|
|
key === 'CFG_TRAEFIK_DASHBOARD_ACCESS';
|
|
//console.log('ConfigOptions.isDropdownKey result:', result);
|
|
return result;
|
|
}
|
|
|
|
// Check if a config key should use timezone dropdown
|
|
static isTimezoneKey(key) {
|
|
return key.includes('TIMEZONE');
|
|
}
|
|
|
|
// Fetch available domains from system configuration
|
|
static async getAvailableDomains() {
|
|
try {
|
|
//console.log('🔍 Starting domain fetch...');
|
|
|
|
// Try to load system config to get domain information
|
|
const response = await fetch('/data/config/generated/configs.json');
|
|
//console.log('📡 Config response status:', response.status);
|
|
|
|
if (!response.ok) {
|
|
console.warn('Could not load system config for domains, returning empty list');
|
|
return [];
|
|
}
|
|
|
|
const configData = await response.json();
|
|
//console.log('📄 Full config data:', configData);
|
|
//console.log('🔧 Config keys available:', Object.keys(configData));
|
|
|
|
const config = configData.config || {};
|
|
//console.log('⚙️ Config object:', config);
|
|
//console.log('🔑 Config keys:', Object.keys(config));
|
|
|
|
const domains = [];
|
|
|
|
// Check CFG_DOMAIN_1 through CFG_DOMAIN_9
|
|
for (let i = 1; i <= 9; i++) {
|
|
const domainKey = `CFG_DOMAIN_${i}`;
|
|
const domainValue = config[domainKey]?.value || config[domainKey] || '';
|
|
//console.log(`🌐 Checking ${domainKey}:`, domainValue);
|
|
|
|
if (domainValue.trim() !== '') {
|
|
domains.push({
|
|
number: i,
|
|
domain: domainValue.trim(),
|
|
key: domainKey
|
|
});
|
|
}
|
|
}
|
|
|
|
//console.log('✅ Found configured domains:', domains);
|
|
return domains;
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error fetching domains:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Get domain options for DOMAIN field
|
|
static async getDomainOptions() {
|
|
//console.log('🎯 Getting domain options...');
|
|
const domains = await this.getAvailableDomains();
|
|
//console.log('📊 Domains returned:', domains);
|
|
|
|
if (domains.length === 0) {
|
|
//console.log('⚠️ No domains found, returning fallback option');
|
|
// No domains configured - return empty option
|
|
return [
|
|
{ value: '1', label: 'No domains configured - Configure domains in Network settings first' }
|
|
];
|
|
}
|
|
|
|
// Create options with just domain names
|
|
const options = domains.map(domain => ({
|
|
value: domain.number.toString(),
|
|
label: domain.domain
|
|
}));
|
|
|
|
//console.log('✅ Generated domain options:', options);
|
|
return options;
|
|
}
|
|
|
|
// Check if a config key should use domain dropdown
|
|
static isDomainKey(key) {
|
|
return key === 'DOMAIN';
|
|
}
|
|
}
|
|
|
|
// Export to global scope
|
|
window.ConfigOptions = ConfigOptions;
|
|
|
|
// Kick off provider snapshot fetch immediately so the dropdown is hot on first paint.
|
|
ConfigOptions.loadGluetunProviders();
|
|
ConfigOptions.loadGluetunProviderIcons();
|
|
|
|
// Delegated listener: when the gluetun provider <select> changes, rebuild the
|
|
// VPN-type <select> from that provider's supported types. Avoids reaching into
|
|
// the renderer to wire onchange per-field.
|
|
document.addEventListener('change', (e) => {
|
|
if (!e.target) return;
|
|
if (e.target.id === 'config-CFG_GLUETUN_VPN_SERVICE_PROVIDER'
|
|
|| e.target.id === 'GLUETUN_VPN_SERVICE_PROVIDER') {
|
|
ConfigOptions.refreshGluetunVpnTypeOptions();
|
|
} else if (e.target.id === 'config-CFG_GLUETUN_VPN_TYPE'
|
|
|| e.target.id === 'GLUETUN_VPN_TYPE') {
|
|
ConfigOptions.refreshGluetunCredentialVisibility();
|
|
} else if (e.target.id === 'config-CFG_GLUETUN_WIREGUARD_PRIVATE_KEY'
|
|
|| e.target.id === 'GLUETUN_WIREGUARD_PRIVATE_KEY'
|
|
|| e.target.id === 'config-CFG_GLUETUN_WIREGUARD_ADDRESSES'
|
|
|| e.target.id === 'GLUETUN_WIREGUARD_ADDRESSES') {
|
|
ConfigOptions.refreshMullvadGenerateStatus();
|
|
}
|
|
});
|
|
|
|
document.addEventListener('input', (e) => {
|
|
if (!e.target) return;
|
|
if (e.target.id === 'config-CFG_GLUETUN_WIREGUARD_PRIVATE_KEY'
|
|
|| e.target.id === 'GLUETUN_WIREGUARD_PRIVATE_KEY'
|
|
|| e.target.id === 'config-CFG_GLUETUN_WIREGUARD_ADDRESSES'
|
|
|| e.target.id === 'GLUETUN_WIREGUARD_ADDRESSES') {
|
|
ConfigOptions.refreshMullvadGenerateStatus();
|
|
}
|
|
});
|