mirror of
https://github.com/itdoginfo/podkop.git
synced 2025-12-09 04:56:51 +03:00
chore: change podkop config luci builder
This commit is contained in:
@@ -24,10 +24,30 @@ export const GlobalStyles = `
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cbi-podkop-main-_status > div {
|
#cbi-podkop-dashboard-_mount_node > div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#cbi-podkop-dashboard > h3 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cbi-podkop-settings > h3 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cbi-podkop-section > h3:nth-child(1) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbi-section-remove {
|
||||||
|
margin-bottom: -32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbi-value {
|
||||||
|
margin-bottom: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Dashboard styles */
|
/* Dashboard styles */
|
||||||
|
|
||||||
.pdk_dashboard-page {
|
.pdk_dashboard-page {
|
||||||
|
|||||||
@@ -1,362 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
'require form';
|
|
||||||
'require baseclass';
|
|
||||||
'require tools.widgets as widgets';
|
|
||||||
'require view.podkop.main as main';
|
|
||||||
|
|
||||||
function createAdditionalSection(mainSection) {
|
|
||||||
let o = mainSection.tab('additional', _('Additional Settings'));
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'additional',
|
|
||||||
form.Flag,
|
|
||||||
'yacd',
|
|
||||||
_('Yacd enable'),
|
|
||||||
`<a href="${main.getClashUIUrl()}" target="_blank">${main.getClashUIUrl()}</a>`,
|
|
||||||
);
|
|
||||||
o.default = '0';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = 'main';
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'additional',
|
|
||||||
form.Flag,
|
|
||||||
'exclude_ntp',
|
|
||||||
_('Exclude NTP'),
|
|
||||||
_('Allows you to exclude NTP protocol traffic from the tunnel'),
|
|
||||||
);
|
|
||||||
o.default = '0';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = 'main';
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'additional',
|
|
||||||
form.Flag,
|
|
||||||
'quic_disable',
|
|
||||||
_('QUIC disable'),
|
|
||||||
_('For issues with the video stream'),
|
|
||||||
);
|
|
||||||
o.default = '0';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = 'main';
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'additional',
|
|
||||||
form.ListValue,
|
|
||||||
'update_interval',
|
|
||||||
_('List Update Frequency'),
|
|
||||||
_('Select how often the lists will be updated'),
|
|
||||||
);
|
|
||||||
Object.entries(main.UPDATE_INTERVAL_OPTIONS).forEach(([key, label]) => {
|
|
||||||
o.value(key, _(label));
|
|
||||||
});
|
|
||||||
o.default = '1d';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = 'main';
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'additional',
|
|
||||||
form.ListValue,
|
|
||||||
'dns_type',
|
|
||||||
_('DNS Protocol Type'),
|
|
||||||
_('Select DNS protocol to use'),
|
|
||||||
);
|
|
||||||
o.value('doh', _('DNS over HTTPS (DoH)'));
|
|
||||||
o.value('dot', _('DNS over TLS (DoT)'));
|
|
||||||
o.value('udp', _('UDP (Unprotected DNS)'));
|
|
||||||
o.default = 'udp';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = 'main';
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'additional',
|
|
||||||
form.Value,
|
|
||||||
'dns_server',
|
|
||||||
_('DNS Server'),
|
|
||||||
_('Select or enter DNS server address'),
|
|
||||||
);
|
|
||||||
Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => {
|
|
||||||
o.value(key, _(label));
|
|
||||||
});
|
|
||||||
o.default = '8.8.8.8';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = 'main';
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
const validation = main.validateDNS(value);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return validation.message;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'additional',
|
|
||||||
form.Value,
|
|
||||||
'bootstrap_dns_server',
|
|
||||||
_('Bootstrap DNS server'),
|
|
||||||
_(
|
|
||||||
'The DNS server used to look up the IP address of an upstream DNS server',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Object.entries(main.BOOTSTRAP_DNS_SERVER_OPTIONS).forEach(([key, label]) => {
|
|
||||||
o.value(key, _(label));
|
|
||||||
});
|
|
||||||
o.default = '77.88.8.8';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = 'main';
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
const validation = main.validateDNS(value);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return validation.message;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'additional',
|
|
||||||
form.Value,
|
|
||||||
'dns_rewrite_ttl',
|
|
||||||
_('DNS Rewrite TTL'),
|
|
||||||
_('Time in seconds for DNS record caching (default: 60)'),
|
|
||||||
);
|
|
||||||
o.default = '60';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = 'main';
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
if (!value) {
|
|
||||||
return _('TTL value cannot be empty');
|
|
||||||
}
|
|
||||||
|
|
||||||
const ttl = parseInt(value);
|
|
||||||
if (isNaN(ttl) || ttl < 0) {
|
|
||||||
return _('TTL must be a positive number');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'additional',
|
|
||||||
form.ListValue,
|
|
||||||
'config_path',
|
|
||||||
_('Config File Path'),
|
|
||||||
_(
|
|
||||||
'Select path for sing-box config file. Change this ONLY if you know what you are doing',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
o.value('/etc/sing-box/config.json', 'Flash (/etc/sing-box/config.json)');
|
|
||||||
o.value('/tmp/sing-box/config.json', 'RAM (/tmp/sing-box/config.json)');
|
|
||||||
o.default = '/etc/sing-box/config.json';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = 'main';
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'additional',
|
|
||||||
form.Value,
|
|
||||||
'cache_path',
|
|
||||||
_('Cache File Path'),
|
|
||||||
_(
|
|
||||||
'Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
o.value('/tmp/sing-box/cache.db', 'RAM (/tmp/sing-box/cache.db)');
|
|
||||||
o.value(
|
|
||||||
'/usr/share/sing-box/cache.db',
|
|
||||||
'Flash (/usr/share/sing-box/cache.db)',
|
|
||||||
);
|
|
||||||
o.default = '/tmp/sing-box/cache.db';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = 'main';
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
if (!value) {
|
|
||||||
return _('Cache file path cannot be empty');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!value.startsWith('/')) {
|
|
||||||
return _('Path must be absolute (start with /)');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!value.endsWith('cache.db')) {
|
|
||||||
return _('Path must end with cache.db');
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = value.split('/').filter(Boolean);
|
|
||||||
if (parts.length < 2) {
|
|
||||||
return _('Path must contain at least one directory (like /tmp/cache.db)');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'additional',
|
|
||||||
widgets.DeviceSelect,
|
|
||||||
'iface',
|
|
||||||
_('Source Network Interface'),
|
|
||||||
_('Select the network interface from which the traffic will originate'),
|
|
||||||
);
|
|
||||||
o.ucisection = 'main';
|
|
||||||
o.default = 'br-lan';
|
|
||||||
o.noaliases = true;
|
|
||||||
o.nobridges = false;
|
|
||||||
o.noinactive = false;
|
|
||||||
o.multiple = true;
|
|
||||||
o.filter = function (section_id, value) {
|
|
||||||
// Block specific interface names from being selectable
|
|
||||||
const blocked = ['wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan'];
|
|
||||||
if (blocked.includes(value)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to find the device object by its name
|
|
||||||
const device = this.devices.find((dev) => dev.getName() === value);
|
|
||||||
|
|
||||||
// If no device is found, allow the value
|
|
||||||
if (!device) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the type of the device
|
|
||||||
const type = device.getType();
|
|
||||||
|
|
||||||
// Consider any Wi-Fi / wireless / wlan device as invalid
|
|
||||||
const isWireless =
|
|
||||||
type === 'wifi' || type === 'wireless' || type.includes('wlan');
|
|
||||||
|
|
||||||
// Allow only non-wireless devices
|
|
||||||
return !isWireless;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'additional',
|
|
||||||
form.Flag,
|
|
||||||
'mon_restart_ifaces',
|
|
||||||
_('Interface monitoring'),
|
|
||||||
_('Interface monitoring for bad WAN'),
|
|
||||||
);
|
|
||||||
o.default = '0';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = 'main';
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'additional',
|
|
||||||
widgets.NetworkSelect,
|
|
||||||
'restart_ifaces',
|
|
||||||
_('Interface for monitoring'),
|
|
||||||
_('Select the WAN interfaces to be monitored'),
|
|
||||||
);
|
|
||||||
o.ucisection = 'main';
|
|
||||||
o.depends('mon_restart_ifaces', '1');
|
|
||||||
o.multiple = true;
|
|
||||||
o.filter = function (section_id, value) {
|
|
||||||
// Reject if the value is in the blocked list ['lan', 'loopback']
|
|
||||||
if (['lan', 'loopback'].includes(value)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject if the value starts with '@' (means it's an alias/reference)
|
|
||||||
if (value.startsWith('@')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise allow it
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'additional',
|
|
||||||
form.Value,
|
|
||||||
'procd_reload_delay',
|
|
||||||
_('Interface Monitoring Delay'),
|
|
||||||
_('Delay in milliseconds before reloading podkop after interface UP'),
|
|
||||||
);
|
|
||||||
o.ucisection = 'main';
|
|
||||||
o.depends('mon_restart_ifaces', '1');
|
|
||||||
o.default = '2000';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
if (!value) {
|
|
||||||
return _('Delay value cannot be empty');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'additional',
|
|
||||||
form.Flag,
|
|
||||||
'dont_touch_dhcp',
|
|
||||||
_('Dont touch my DHCP!'),
|
|
||||||
_('Podkop will not change the DHCP config'),
|
|
||||||
);
|
|
||||||
o.default = '0';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = 'main';
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'additional',
|
|
||||||
form.Flag,
|
|
||||||
'detour',
|
|
||||||
_('Proxy download of lists'),
|
|
||||||
_('Downloading all lists via main Proxy/VPN'),
|
|
||||||
);
|
|
||||||
o.default = '0';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = 'main';
|
|
||||||
|
|
||||||
// Extra IPs and exclusions (main section)
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'basic',
|
|
||||||
form.Flag,
|
|
||||||
'exclude_from_ip_enabled',
|
|
||||||
_('IP for exclusion'),
|
|
||||||
_('Specify local IP addresses that will never use the configured route'),
|
|
||||||
);
|
|
||||||
o.default = '0';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = 'main';
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'basic',
|
|
||||||
form.DynamicList,
|
|
||||||
'exclude_traffic_ip',
|
|
||||||
_('Local IPs'),
|
|
||||||
_('Enter valid IPv4 addresses'),
|
|
||||||
);
|
|
||||||
o.placeholder = 'IP';
|
|
||||||
o.depends('exclude_from_ip_enabled', '1');
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = 'main';
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
// Optional
|
|
||||||
if (!value || value.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = main.validateIPV4(value);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return validation.message;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = mainSection.taboption(
|
|
||||||
'basic',
|
|
||||||
form.Flag,
|
|
||||||
'socks5',
|
|
||||||
_('Mixed enable'),
|
|
||||||
_('Browser port: 2080'),
|
|
||||||
);
|
|
||||||
o.default = '0';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = 'main';
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseclass.extend({
|
|
||||||
createAdditionalSection,
|
|
||||||
});
|
|
||||||
@@ -1,779 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
'require baseclass';
|
|
||||||
'require form';
|
|
||||||
'require ui';
|
|
||||||
'require network';
|
|
||||||
'require view.podkop.main as main';
|
|
||||||
'require tools.widgets as widgets';
|
|
||||||
|
|
||||||
function createConfigSection(section) {
|
|
||||||
const s = section;
|
|
||||||
|
|
||||||
let o = s.tab('basic', _('Basic Settings'));
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.ListValue,
|
|
||||||
'mode',
|
|
||||||
_('Connection Type'),
|
|
||||||
_('Select between VPN and Proxy connection methods for traffic routing'),
|
|
||||||
);
|
|
||||||
o.value('proxy', 'Proxy');
|
|
||||||
o.value('vpn', 'VPN');
|
|
||||||
o.value('block', 'Block');
|
|
||||||
o.ucisection = s.section;
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.ListValue,
|
|
||||||
'proxy_config_type',
|
|
||||||
_('Configuration Type'),
|
|
||||||
_('Select how to configure the proxy'),
|
|
||||||
);
|
|
||||||
o.value('url', _('Connection URL'));
|
|
||||||
o.value('outbound', _('Outbound Config'));
|
|
||||||
o.value('urltest', _('URLTest'));
|
|
||||||
o.default = 'url';
|
|
||||||
o.depends('mode', 'proxy');
|
|
||||||
o.ucisection = s.section;
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.TextValue,
|
|
||||||
'proxy_string',
|
|
||||||
_('Proxy Configuration URL'),
|
|
||||||
'',
|
|
||||||
);
|
|
||||||
o.depends('proxy_config_type', 'url');
|
|
||||||
o.rows = 5;
|
|
||||||
// Enable soft wrapping for multi-line proxy URLs (e.g., for URLTest proxy links)
|
|
||||||
o.wrap = 'soft';
|
|
||||||
// Render as a textarea to allow multiple proxy URLs/configs
|
|
||||||
o.textarea = true;
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
o.sectionDescriptions = new Map();
|
|
||||||
o.placeholder =
|
|
||||||
'vless://uuid@server:port?type=tcp&security=tls#main\n// backup ss://method:pass@server:port\n// backup2 vless://uuid@server:port?type=grpc&security=reality#alt\n// backup3 trojan://04agAQapcl@127.0.0.1:33641?type=tcp&security=none#trojan-tcp-none';
|
|
||||||
|
|
||||||
o.renderWidget = function (section_id, option_index, cfgvalue) {
|
|
||||||
const original = form.TextValue.prototype.renderWidget.apply(this, [
|
|
||||||
section_id,
|
|
||||||
option_index,
|
|
||||||
cfgvalue,
|
|
||||||
]);
|
|
||||||
const container = E('div', {});
|
|
||||||
container.appendChild(original);
|
|
||||||
|
|
||||||
if (cfgvalue) {
|
|
||||||
try {
|
|
||||||
const activeConfig = cfgvalue
|
|
||||||
.split('\n')
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.find((line) => line && !line.startsWith('//'));
|
|
||||||
|
|
||||||
if (activeConfig) {
|
|
||||||
if (activeConfig.includes('#')) {
|
|
||||||
const label = activeConfig.split('#').pop();
|
|
||||||
if (label && label.trim()) {
|
|
||||||
const decodedLabel = decodeURIComponent(label);
|
|
||||||
const descDiv = E(
|
|
||||||
'div',
|
|
||||||
{ class: 'cbi-value-description' },
|
|
||||||
_('Current config: ') + decodedLabel,
|
|
||||||
);
|
|
||||||
container.appendChild(descDiv);
|
|
||||||
} else {
|
|
||||||
const descDiv = E(
|
|
||||||
'div',
|
|
||||||
{ class: 'cbi-value-description' },
|
|
||||||
_('Config without description'),
|
|
||||||
);
|
|
||||||
container.appendChild(descDiv);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const descDiv = E(
|
|
||||||
'div',
|
|
||||||
{ class: 'cbi-value-description' },
|
|
||||||
_('Config without description'),
|
|
||||||
);
|
|
||||||
container.appendChild(descDiv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing config label:', e);
|
|
||||||
const descDiv = E(
|
|
||||||
'div',
|
|
||||||
{ class: 'cbi-value-description' },
|
|
||||||
_('Config without description'),
|
|
||||||
);
|
|
||||||
container.appendChild(descDiv);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const defaultDesc = E(
|
|
||||||
'div',
|
|
||||||
{ class: 'cbi-value-description' },
|
|
||||||
_(
|
|
||||||
'Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
container.appendChild(defaultDesc);
|
|
||||||
}
|
|
||||||
|
|
||||||
return container;
|
|
||||||
};
|
|
||||||
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
// Optional
|
|
||||||
if (!value || value.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const activeConfigs = main.splitProxyString(value);
|
|
||||||
|
|
||||||
if (!activeConfigs.length) {
|
|
||||||
return _(
|
|
||||||
'No active configuration found. One configuration is required.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeConfigs.length > 1) {
|
|
||||||
return _(
|
|
||||||
'Multiply active configurations found. Please leave one configuration.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = main.validateProxyUrl(activeConfigs[0]);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return validation.message;
|
|
||||||
} catch (e) {
|
|
||||||
return `${_('Invalid URL format:')} ${e?.message}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.TextValue,
|
|
||||||
'outbound_json',
|
|
||||||
_('Outbound Configuration'),
|
|
||||||
_('Enter complete outbound configuration in JSON format'),
|
|
||||||
);
|
|
||||||
o.depends('proxy_config_type', 'outbound');
|
|
||||||
o.rows = 10;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
// Optional
|
|
||||||
if (!value || value.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = main.validateOutboundJson(value);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return validation.message;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.DynamicList,
|
|
||||||
'urltest_proxy_links',
|
|
||||||
_('URLTest Proxy Links'),
|
|
||||||
);
|
|
||||||
o.depends('proxy_config_type', 'urltest');
|
|
||||||
o.placeholder = 'vless://, ss://, trojan:// links';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
// Optional
|
|
||||||
if (!value || value.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = main.validateProxyUrl(value);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return validation.message;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.Flag,
|
|
||||||
'ss_uot',
|
|
||||||
_('Shadowsocks UDP over TCP'),
|
|
||||||
_('Apply for SS2022'),
|
|
||||||
);
|
|
||||||
o.default = '0';
|
|
||||||
o.depends('mode', 'proxy');
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
widgets.DeviceSelect,
|
|
||||||
'interface',
|
|
||||||
_('Network Interface'),
|
|
||||||
_('Select network interface for VPN connection'),
|
|
||||||
);
|
|
||||||
o.depends('mode', 'vpn');
|
|
||||||
o.ucisection = s.section;
|
|
||||||
o.noaliases = true;
|
|
||||||
o.nobridges = false;
|
|
||||||
o.noinactive = false;
|
|
||||||
o.filter = function (section_id, value) {
|
|
||||||
// Blocked interface names that should never be selectable
|
|
||||||
const blockedInterfaces = [
|
|
||||||
'br-lan',
|
|
||||||
'eth0',
|
|
||||||
'eth1',
|
|
||||||
'wan',
|
|
||||||
'phy0-ap0',
|
|
||||||
'phy1-ap0',
|
|
||||||
'pppoe-wan',
|
|
||||||
'lan',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Reject immediately if the value matches any blocked interface
|
|
||||||
if (blockedInterfaces.includes(value)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to find the device object with the given name
|
|
||||||
const device = this.devices.find((dev) => dev.getName() === value);
|
|
||||||
|
|
||||||
// If no device is found, allow the value
|
|
||||||
if (!device) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the device type (e.g., "wifi", "ethernet", etc.)
|
|
||||||
const type = device.getType();
|
|
||||||
|
|
||||||
// Reject wireless-related devices
|
|
||||||
const isWireless =
|
|
||||||
type === 'wifi' || type === 'wireless' || type.includes('wlan');
|
|
||||||
|
|
||||||
return !isWireless;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.Flag,
|
|
||||||
'domain_resolver_enabled',
|
|
||||||
_('Domain Resolver'),
|
|
||||||
_('Enable built-in DNS resolver for domains handled by this section'),
|
|
||||||
);
|
|
||||||
o.default = '0';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.depends('mode', 'vpn');
|
|
||||||
o.ucisection = s.section;
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.ListValue,
|
|
||||||
'domain_resolver_dns_type',
|
|
||||||
_('DNS Protocol Type'),
|
|
||||||
_('Select the DNS protocol type for the domain resolver'),
|
|
||||||
);
|
|
||||||
o.value('doh', _('DNS over HTTPS (DoH)'));
|
|
||||||
o.value('dot', _('DNS over TLS (DoT)'));
|
|
||||||
o.value('udp', _('UDP (Unprotected DNS)'));
|
|
||||||
o.default = 'udp';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.depends('domain_resolver_enabled', '1');
|
|
||||||
o.ucisection = s.section;
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.Value,
|
|
||||||
'domain_resolver_dns_server',
|
|
||||||
_('DNS Server'),
|
|
||||||
_('Select or enter DNS server address'),
|
|
||||||
);
|
|
||||||
Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => {
|
|
||||||
o.value(key, _(label));
|
|
||||||
});
|
|
||||||
o.default = '8.8.8.8';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.depends('domain_resolver_enabled', '1');
|
|
||||||
o.ucisection = s.section;
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
const validation = main.validateDNS(value);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return validation.message;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.Flag,
|
|
||||||
'community_lists_enabled',
|
|
||||||
_('Community Lists'),
|
|
||||||
);
|
|
||||||
o.default = '0';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.DynamicList,
|
|
||||||
'community_lists',
|
|
||||||
_('Service List'),
|
|
||||||
_('Select predefined service for routing') +
|
|
||||||
' <a href="https://github.com/itdoginfo/allow-domains" target="_blank">github.com/itdoginfo/allow-domains</a>',
|
|
||||||
);
|
|
||||||
o.placeholder = 'Service list';
|
|
||||||
Object.entries(main.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => {
|
|
||||||
o.value(key, _(label));
|
|
||||||
});
|
|
||||||
o.depends('community_lists_enabled', '1');
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
|
|
||||||
let lastValues = [];
|
|
||||||
let isProcessing = false;
|
|
||||||
|
|
||||||
o.onchange = function (ev, section_id, value) {
|
|
||||||
if (isProcessing) return;
|
|
||||||
isProcessing = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const values = Array.isArray(value) ? value : [value];
|
|
||||||
let newValues = [...values];
|
|
||||||
let notifications = [];
|
|
||||||
|
|
||||||
const selectedRegionalOptions = main.REGIONAL_OPTIONS.filter((opt) =>
|
|
||||||
newValues.includes(opt),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedRegionalOptions.length > 1) {
|
|
||||||
const lastSelected =
|
|
||||||
selectedRegionalOptions[selectedRegionalOptions.length - 1];
|
|
||||||
const removedRegions = selectedRegionalOptions.slice(0, -1);
|
|
||||||
newValues = newValues.filter(
|
|
||||||
(v) => v === lastSelected || !main.REGIONAL_OPTIONS.includes(v),
|
|
||||||
);
|
|
||||||
notifications.push(
|
|
||||||
E('p', { class: 'alert-message warning' }, [
|
|
||||||
E('strong', {}, _('Regional options cannot be used together')),
|
|
||||||
E('br'),
|
|
||||||
_(
|
|
||||||
'Warning: %s cannot be used together with %s. Previous selections have been removed.',
|
|
||||||
).format(removedRegions.join(', '), lastSelected),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newValues.includes('russia_inside')) {
|
|
||||||
const removedServices = newValues.filter(
|
|
||||||
(v) => !main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v),
|
|
||||||
);
|
|
||||||
if (removedServices.length > 0) {
|
|
||||||
newValues = newValues.filter((v) =>
|
|
||||||
main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v),
|
|
||||||
);
|
|
||||||
notifications.push(
|
|
||||||
E('p', { class: 'alert-message warning' }, [
|
|
||||||
E('strong', {}, _('Russia inside restrictions')),
|
|
||||||
E('br'),
|
|
||||||
_(
|
|
||||||
'Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.',
|
|
||||||
).format(
|
|
||||||
main.ALLOWED_WITH_RUSSIA_INSIDE.map(
|
|
||||||
(key) => main.DOMAIN_LIST_OPTIONS[key],
|
|
||||||
)
|
|
||||||
.filter((label) => label !== 'Russia inside')
|
|
||||||
.join(', '),
|
|
||||||
removedServices.join(', '),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (JSON.stringify(newValues.sort()) !== JSON.stringify(values.sort())) {
|
|
||||||
this.getUIElement(section_id).setValue(newValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
notifications.forEach((notification) =>
|
|
||||||
ui.addNotification(null, notification),
|
|
||||||
);
|
|
||||||
lastValues = newValues;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error in onchange handler:', e);
|
|
||||||
} finally {
|
|
||||||
isProcessing = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.ListValue,
|
|
||||||
'user_domain_list_type',
|
|
||||||
_('User Domain List Type'),
|
|
||||||
_('Select how to add your custom domains'),
|
|
||||||
);
|
|
||||||
o.value('disabled', _('Disabled'));
|
|
||||||
o.value('dynamic', _('Dynamic List'));
|
|
||||||
o.value('text', _('Text List'));
|
|
||||||
o.default = 'disabled';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.DynamicList,
|
|
||||||
'user_domains',
|
|
||||||
_('User Domains'),
|
|
||||||
_(
|
|
||||||
'Enter domain names without protocols (example: sub.example.com or example.com)',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
o.placeholder = 'Domains list';
|
|
||||||
o.depends('user_domain_list_type', 'dynamic');
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
// Optional
|
|
||||||
if (!value || value.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = main.validateDomain(value, true);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return validation.message;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.TextValue,
|
|
||||||
'user_domains_text',
|
|
||||||
_('User Domains List'),
|
|
||||||
_(
|
|
||||||
'Enter domain names separated by comma, space or newline. You can add comments after //',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
o.placeholder =
|
|
||||||
'example.com, sub.example.com\n// Social networks\ndomain.com test.com // personal domains';
|
|
||||||
o.depends('user_domain_list_type', 'text');
|
|
||||||
o.rows = 8;
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
// Optional
|
|
||||||
if (!value || value.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const domains = main.parseValueList(value);
|
|
||||||
|
|
||||||
if (!domains.length) {
|
|
||||||
return _(
|
|
||||||
'At least one valid domain must be specified. Comments-only content is not allowed.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { valid, results } = main.bulkValidate(domains, row => main.validateDomain(row, true));
|
|
||||||
|
|
||||||
if (!valid) {
|
|
||||||
const errors = results
|
|
||||||
.filter((validation) => !validation.valid) // Leave only failed validations
|
|
||||||
.map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors
|
|
||||||
|
|
||||||
return [_('Validation errors:'), ...errors].join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.Flag,
|
|
||||||
'local_domain_lists_enabled',
|
|
||||||
_('Local Domain Lists'),
|
|
||||||
_('Use the list from the router filesystem'),
|
|
||||||
);
|
|
||||||
o.default = '0';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.DynamicList,
|
|
||||||
'local_domain_lists',
|
|
||||||
_('Local Domain List Paths'),
|
|
||||||
_('Enter the list file path'),
|
|
||||||
);
|
|
||||||
o.placeholder = '/path/file.lst';
|
|
||||||
o.depends('local_domain_lists_enabled', '1');
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
// Optional
|
|
||||||
if (!value || value.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = main.validatePath(value);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return validation.message;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.Flag,
|
|
||||||
'remote_domain_lists_enabled',
|
|
||||||
_('Remote Domain Lists'),
|
|
||||||
_('Download and use domain lists from remote URLs'),
|
|
||||||
);
|
|
||||||
o.default = '0';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.DynamicList,
|
|
||||||
'remote_domain_lists',
|
|
||||||
_('Remote Domain URLs'),
|
|
||||||
_('Enter full URLs starting with http:// or https://'),
|
|
||||||
);
|
|
||||||
o.placeholder = 'URL';
|
|
||||||
o.depends('remote_domain_lists_enabled', '1');
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
// Optional
|
|
||||||
if (!value || value.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = main.validateUrl(value);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return validation.message;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.Flag,
|
|
||||||
'local_subnet_lists_enabled',
|
|
||||||
_('Local Subnet Lists'),
|
|
||||||
_('Use the list from the router filesystem'),
|
|
||||||
);
|
|
||||||
o.default = '0';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.DynamicList,
|
|
||||||
'local_subnet_lists',
|
|
||||||
_('Local Subnet List Paths'),
|
|
||||||
_('Enter the list file path'),
|
|
||||||
);
|
|
||||||
o.placeholder = '/path/file.lst';
|
|
||||||
o.depends('local_subnet_lists_enabled', '1');
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
// Optional
|
|
||||||
if (!value || value.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = main.validatePath(value);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return validation.message;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.ListValue,
|
|
||||||
'user_subnet_list_type',
|
|
||||||
_('User Subnet List Type'),
|
|
||||||
_('Select how to add your custom subnets'),
|
|
||||||
);
|
|
||||||
o.value('disabled', _('Disabled'));
|
|
||||||
o.value('dynamic', _('Dynamic List'));
|
|
||||||
o.value('text', _('Text List (comma/space/newline separated)'));
|
|
||||||
o.default = 'disabled';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.DynamicList,
|
|
||||||
'user_subnets',
|
|
||||||
_('User Subnets'),
|
|
||||||
_(
|
|
||||||
'Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
o.placeholder = 'IP or subnet';
|
|
||||||
o.depends('user_subnet_list_type', 'dynamic');
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
// Optional
|
|
||||||
if (!value || value.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = main.validateSubnet(value);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return validation.message;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.TextValue,
|
|
||||||
'user_subnets_text',
|
|
||||||
_('User Subnets List'),
|
|
||||||
_(
|
|
||||||
'Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
o.placeholder =
|
|
||||||
'103.21.244.0/22\n// Google DNS\n8.8.8.8\n1.1.1.1/32, 9.9.9.9 // Cloudflare and Quad9';
|
|
||||||
o.depends('user_subnet_list_type', 'text');
|
|
||||||
o.rows = 10;
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
// Optional
|
|
||||||
if (!value || value.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const subnets = main.parseValueList(value);
|
|
||||||
|
|
||||||
if (!subnets.length) {
|
|
||||||
return _(
|
|
||||||
'At least one valid subnet or IP must be specified. Comments-only content is not allowed.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { valid, results } = main.bulkValidate(subnets, main.validateSubnet);
|
|
||||||
|
|
||||||
if (!valid) {
|
|
||||||
const errors = results
|
|
||||||
.filter((validation) => !validation.valid) // Leave only failed validations
|
|
||||||
.map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors
|
|
||||||
|
|
||||||
return [_('Validation errors:'), ...errors].join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.Flag,
|
|
||||||
'remote_subnet_lists_enabled',
|
|
||||||
_('Remote Subnet Lists'),
|
|
||||||
_('Download and use subnet lists from remote URLs'),
|
|
||||||
);
|
|
||||||
o.default = '0';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.DynamicList,
|
|
||||||
'remote_subnet_lists',
|
|
||||||
_('Remote Subnet URLs'),
|
|
||||||
_('Enter full URLs starting with http:// or https://'),
|
|
||||||
);
|
|
||||||
o.placeholder = 'URL';
|
|
||||||
o.depends('remote_subnet_lists_enabled', '1');
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
// Optional
|
|
||||||
if (!value || value.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = main.validateUrl(value);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return validation.message;
|
|
||||||
};
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.Flag,
|
|
||||||
'all_traffic_from_ip_enabled',
|
|
||||||
_('IP for full redirection'),
|
|
||||||
_(
|
|
||||||
'Specify local IP addresses whose traffic will always use the configured route',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
o.default = '0';
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
|
|
||||||
o = s.taboption(
|
|
||||||
'basic',
|
|
||||||
form.DynamicList,
|
|
||||||
'all_traffic_ip',
|
|
||||||
_('Local IPs'),
|
|
||||||
_('Enter valid IPv4 addresses'),
|
|
||||||
);
|
|
||||||
o.placeholder = 'IP';
|
|
||||||
o.depends('all_traffic_from_ip_enabled', '1');
|
|
||||||
o.rmempty = false;
|
|
||||||
o.ucisection = s.section;
|
|
||||||
o.validate = function (section_id, value) {
|
|
||||||
// Optional
|
|
||||||
if (!value || value.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = main.validateSubnet(value);
|
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return validation.message;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseclass.extend({
|
|
||||||
createConfigSection,
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
'use strict';
|
||||||
|
'require baseclass';
|
||||||
|
'require form';
|
||||||
|
'require ui';
|
||||||
|
'require uci';
|
||||||
|
'require fs';
|
||||||
|
'require view.podkop.main as main';
|
||||||
|
|
||||||
|
function createDashboardContent(section) {
|
||||||
|
const o = section.option(form.DummyValue, '_mount_node');
|
||||||
|
o.rawhtml = true;
|
||||||
|
o.cfgvalue = () => {
|
||||||
|
main.initDashboardController();
|
||||||
|
return main.renderDashboard();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntryPoint = {
|
||||||
|
createDashboardContent,
|
||||||
|
};
|
||||||
|
|
||||||
|
return baseclass.extend(EntryPoint);
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
'require baseclass';
|
|
||||||
'require form';
|
|
||||||
'require ui';
|
|
||||||
'require uci';
|
|
||||||
'require fs';
|
|
||||||
'require view.podkop.utils as utils';
|
|
||||||
'require view.podkop.main as main';
|
|
||||||
|
|
||||||
function createDashboardSection(mainSection) {
|
|
||||||
let o = mainSection.tab('dashboard', _('Dashboard'));
|
|
||||||
|
|
||||||
o = mainSection.taboption('dashboard', form.DummyValue, '_status');
|
|
||||||
o.rawhtml = true;
|
|
||||||
o.cfgvalue = () => {
|
|
||||||
main.initDashboardController();
|
|
||||||
|
|
||||||
return main.renderDashboard();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const EntryPoint = {
|
|
||||||
createDashboardSection,
|
|
||||||
};
|
|
||||||
|
|
||||||
return baseclass.extend(EntryPoint);
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -247,10 +247,30 @@ var GlobalStyles = `
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cbi-podkop-main-_status > div {
|
#cbi-podkop-dashboard-_mount_node > div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#cbi-podkop-dashboard > h3 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cbi-podkop-settings > h3 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cbi-podkop-section > h3:nth-child(1) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbi-section-remove {
|
||||||
|
margin-bottom: -32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbi-value {
|
||||||
|
margin-bottom: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Dashboard styles */
|
/* Dashboard styles */
|
||||||
|
|
||||||
.pdk_dashboard-page {
|
.pdk_dashboard-page {
|
||||||
|
|||||||
@@ -1,82 +1,68 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
'require view';
|
'require view';
|
||||||
'require form';
|
'require form';
|
||||||
|
'require baseclass';
|
||||||
'require network';
|
'require network';
|
||||||
'require view.podkop.configSection as configSection';
|
|
||||||
'require view.podkop.diagnosticTab as diagnosticTab';
|
|
||||||
'require view.podkop.additionalTab as additionalTab';
|
|
||||||
'require view.podkop.dashboardTab as dashboardTab';
|
|
||||||
'require view.podkop.utils as utils';
|
|
||||||
'require view.podkop.main as main';
|
'require view.podkop.main as main';
|
||||||
|
|
||||||
const EntryNode = {
|
// Settings content
|
||||||
|
'require view.podkop.settings as settings';
|
||||||
|
|
||||||
|
// Sections content
|
||||||
|
'require view.podkop.section as section';
|
||||||
|
|
||||||
|
// Dashboard content
|
||||||
|
'require view.podkop.dashboard as dashboard';
|
||||||
|
|
||||||
|
|
||||||
|
const EntryPoint = {
|
||||||
async render() {
|
async render() {
|
||||||
main.injectGlobalStyles();
|
main.injectGlobalStyles();
|
||||||
|
|
||||||
const podkopFormMap = new form.Map('podkop', '', null, ['main', 'extra']);
|
const podkopMap = new form.Map('podkop', _('Podkop Settings'), _('Configuration for Podkop service'));
|
||||||
|
// Enable tab views
|
||||||
|
podkopMap.tabbed = true;
|
||||||
|
|
||||||
// Main Section
|
|
||||||
const mainSection = podkopFormMap.section(form.TypedSection, 'main');
|
|
||||||
mainSection.anonymous = true;
|
|
||||||
|
|
||||||
configSection.createConfigSection(mainSection);
|
// Settings tab
|
||||||
|
const settingsSection = podkopMap.section(form.TypedSection, 'settings', _('Settings'));
|
||||||
|
settingsSection.anonymous = true;
|
||||||
|
settingsSection.addremove = false;
|
||||||
|
// Make it named [ config settings 'settings' ]
|
||||||
|
settingsSection.cfgsections = function () { return ['settings']; };
|
||||||
|
|
||||||
// Additional Settings Tab (main section)
|
// Render settings content
|
||||||
additionalTab.createAdditionalSection(mainSection);
|
settings.createSettingsContent(settingsSection);
|
||||||
|
|
||||||
// Diagnostics Tab (main section)
|
|
||||||
diagnosticTab.createDiagnosticsSection(mainSection);
|
|
||||||
const podkopFormMapPromise = podkopFormMap.render().then((node) => {
|
|
||||||
// Set up diagnostics event handlers
|
|
||||||
diagnosticTab.setupDiagnosticsEventHandlers(node);
|
|
||||||
|
|
||||||
// Start critical error polling for all tabs
|
// Sections tab
|
||||||
utils.startErrorPolling();
|
const sectionsSection = podkopMap.section(form.TypedSection, 'section', _('Sections'));
|
||||||
|
sectionsSection.anonymous = false;
|
||||||
|
sectionsSection.addremove = true;
|
||||||
|
sectionsSection.template = 'cbi/simpleform';
|
||||||
|
|
||||||
// Add event listener to keep error polling active when switching tabs
|
// Render section content
|
||||||
const tabs = node.querySelectorAll('.cbi-tabmenu');
|
section.createSectionContent(sectionsSection);
|
||||||
if (tabs.length > 0) {
|
|
||||||
tabs[0].addEventListener('click', function (e) {
|
|
||||||
const tab = e.target.closest('.cbi-tab');
|
|
||||||
if (tab) {
|
|
||||||
// Ensure error polling continues when switching tabs
|
|
||||||
utils.startErrorPolling();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add visibility change handler to manage error polling
|
|
||||||
document.addEventListener('visibilitychange', function () {
|
|
||||||
if (document.hidden) {
|
|
||||||
utils.stopErrorPolling();
|
|
||||||
} else {
|
|
||||||
utils.startErrorPolling();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return node;
|
// Dashboard tab
|
||||||
});
|
const dashboardSection = podkopMap.section(form.TypedSection, 'dashboard', _('Dashboard'));
|
||||||
|
dashboardSection.anonymous = true;
|
||||||
|
dashboardSection.addremove = false;
|
||||||
|
// dashboardSection.title = '';
|
||||||
|
dashboardSection.cfgsections = function () { return ['dashboard']; };
|
||||||
|
|
||||||
|
// Render dashboard content
|
||||||
|
dashboard.createDashboardContent(dashboardSection);
|
||||||
|
|
||||||
// Extra Section
|
|
||||||
const extraSection = podkopFormMap.section(
|
|
||||||
form.TypedSection,
|
|
||||||
'extra',
|
|
||||||
_('Extra configurations'),
|
|
||||||
);
|
|
||||||
extraSection.anonymous = false;
|
|
||||||
extraSection.addremove = true;
|
|
||||||
extraSection.addbtntitle = _('Add Section');
|
|
||||||
extraSection.multiple = true;
|
|
||||||
configSection.createConfigSection(extraSection);
|
|
||||||
|
|
||||||
// Initial dashboard render
|
|
||||||
dashboardTab.createDashboardSection(mainSection);
|
|
||||||
|
|
||||||
// Inject core service
|
// Inject core service
|
||||||
main.coreService();
|
main.coreService();
|
||||||
|
|
||||||
return podkopFormMapPromise;
|
return podkopMap.render();
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return view.extend(EntryNode);
|
|
||||||
|
return view.extend(EntryPoint);
|
||||||
|
|||||||
@@ -0,0 +1,586 @@
|
|||||||
|
'use strict';
|
||||||
|
'require form';
|
||||||
|
'require baseclass';
|
||||||
|
'require tools.widgets as widgets';
|
||||||
|
'require view.podkop.main as main';
|
||||||
|
|
||||||
|
function createSectionContent(section) {
|
||||||
|
let o = section.option(
|
||||||
|
form.ListValue,
|
||||||
|
'mode',
|
||||||
|
_('Connection Type'),
|
||||||
|
_('Select between VPN and Proxy connection methods for traffic routing'),
|
||||||
|
);
|
||||||
|
o.value('proxy', 'Proxy');
|
||||||
|
o.value('vpn', 'VPN');
|
||||||
|
o.value('block', 'Block');
|
||||||
|
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.ListValue,
|
||||||
|
'proxy_config_type',
|
||||||
|
_('Configuration Type'),
|
||||||
|
_('Select how to configure the proxy'),
|
||||||
|
);
|
||||||
|
o.value('url', _('Connection URL'));
|
||||||
|
o.value('outbound', _('Outbound Config'));
|
||||||
|
o.value('urltest', _('URLTest'));
|
||||||
|
o.default = 'url';
|
||||||
|
o.depends('mode', 'proxy');
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.TextValue,
|
||||||
|
'proxy_string',
|
||||||
|
_('Proxy Configuration URL'),
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
o.depends('proxy_config_type', 'url');
|
||||||
|
o.rows = 5;
|
||||||
|
// Enable soft wrapping for multi-line proxy URLs (e.g., for URLTest proxy links)
|
||||||
|
o.wrap = 'soft';
|
||||||
|
// Render as a textarea to allow multiple proxy URLs/configs
|
||||||
|
o.textarea = true;
|
||||||
|
o.rmempty = false;
|
||||||
|
o.sectionDescriptions = new Map();
|
||||||
|
o.placeholder =
|
||||||
|
'vless://uuid@server:port?type=tcp&security=tls#main\n// backup ss://method:pass@server:port\n// backup2 vless://uuid@server:port?type=grpc&security=reality#alt\n// backup3 trojan://04agAQapcl@127.0.0.1:33641?type=tcp&security=none#trojan-tcp-none';
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
// Optional
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const activeConfigs = main.splitProxyString(value);
|
||||||
|
|
||||||
|
if (!activeConfigs.length) {
|
||||||
|
return _(
|
||||||
|
'No active configuration found. One configuration is required.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeConfigs.length > 1) {
|
||||||
|
return _(
|
||||||
|
'Multiply active configurations found. Please leave one configuration.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = main.validateProxyUrl(activeConfigs[0]);
|
||||||
|
|
||||||
|
if (validation.valid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.message;
|
||||||
|
} catch (e) {
|
||||||
|
return `${_('Invalid URL format:')} ${e?.message}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.TextValue,
|
||||||
|
'outbound_json',
|
||||||
|
_('Outbound Configuration'),
|
||||||
|
_('Enter complete outbound configuration in JSON format'),
|
||||||
|
);
|
||||||
|
o.depends('proxy_config_type', 'outbound');
|
||||||
|
o.rows = 10;
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
// Optional
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = main.validateOutboundJson(value);
|
||||||
|
|
||||||
|
if (validation.valid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.DynamicList,
|
||||||
|
'urltest_proxy_links',
|
||||||
|
_('URLTest Proxy Links'),
|
||||||
|
);
|
||||||
|
o.depends('proxy_config_type', 'urltest');
|
||||||
|
o.placeholder = 'vless://, ss://, trojan:// links';
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
// Optional
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = main.validateProxyUrl(value);
|
||||||
|
|
||||||
|
if (validation.valid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
'ss_uot',
|
||||||
|
_('Shadowsocks UDP over TCP'),
|
||||||
|
_('Apply for SS2022'),
|
||||||
|
);
|
||||||
|
o.default = '0';
|
||||||
|
o.depends('mode', 'proxy');
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
widgets.DeviceSelect,
|
||||||
|
'interface',
|
||||||
|
_('Network Interface'),
|
||||||
|
_('Select network interface for VPN connection'),
|
||||||
|
);
|
||||||
|
o.depends('mode', 'vpn');
|
||||||
|
o.noaliases = true;
|
||||||
|
o.nobridges = false;
|
||||||
|
o.noinactive = false;
|
||||||
|
o.filter = function (section_id, value) {
|
||||||
|
// Blocked interface names that should never be selectable
|
||||||
|
const blockedInterfaces = [
|
||||||
|
'br-lan',
|
||||||
|
'eth0',
|
||||||
|
'eth1',
|
||||||
|
'wan',
|
||||||
|
'phy0-ap0',
|
||||||
|
'phy1-ap0',
|
||||||
|
'pppoe-wan',
|
||||||
|
'lan',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Reject immediately if the value matches any blocked interface
|
||||||
|
if (blockedInterfaces.includes(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find the device object with the given name
|
||||||
|
const device = this.devices.find((dev) => dev.getName() === value);
|
||||||
|
|
||||||
|
// If no device is found, allow the value
|
||||||
|
if (!device) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the device type (e.g., "wifi", "ethernet", etc.)
|
||||||
|
const type = device.getType();
|
||||||
|
|
||||||
|
// Reject wireless-related devices
|
||||||
|
const isWireless =
|
||||||
|
type === 'wifi' || type === 'wireless' || type.includes('wlan');
|
||||||
|
|
||||||
|
return !isWireless;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
'domain_resolver_enabled',
|
||||||
|
_('Domain Resolver'),
|
||||||
|
_('Enable built-in DNS resolver for domains handled by this section'),
|
||||||
|
);
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
o.depends('mode', 'vpn');
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.ListValue,
|
||||||
|
'domain_resolver_dns_type',
|
||||||
|
_('DNS Protocol Type'),
|
||||||
|
_('Select the DNS protocol type for the domain resolver'),
|
||||||
|
);
|
||||||
|
o.value('doh', _('DNS over HTTPS (DoH)'));
|
||||||
|
o.value('dot', _('DNS over TLS (DoT)'));
|
||||||
|
o.value('udp', _('UDP (Unprotected DNS)'));
|
||||||
|
o.default = 'udp';
|
||||||
|
o.rmempty = false;
|
||||||
|
o.depends('domain_resolver_enabled', '1');
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Value,
|
||||||
|
'domain_resolver_dns_server',
|
||||||
|
_('DNS Server'),
|
||||||
|
_('Select or enter DNS server address'),
|
||||||
|
);
|
||||||
|
Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => {
|
||||||
|
o.value(key, _(label));
|
||||||
|
});
|
||||||
|
o.default = '8.8.8.8';
|
||||||
|
o.rmempty = false;
|
||||||
|
o.depends('domain_resolver_enabled', '1');
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
const validation = main.validateDNS(value);
|
||||||
|
|
||||||
|
if (validation.valid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
'community_lists_enabled',
|
||||||
|
_('Community Lists'),
|
||||||
|
);
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.DynamicList,
|
||||||
|
'community_lists',
|
||||||
|
_('Service List'),
|
||||||
|
_('Select predefined service for routing') +
|
||||||
|
' <a href="https://github.com/itdoginfo/allow-domains" target="_blank">github.com/itdoginfo/allow-domains</a>',
|
||||||
|
);
|
||||||
|
o.placeholder = 'Service list';
|
||||||
|
Object.entries(main.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => {
|
||||||
|
o.value(key, _(label));
|
||||||
|
});
|
||||||
|
o.depends('community_lists_enabled', '1');
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.ListValue,
|
||||||
|
'user_domain_list_type',
|
||||||
|
_('User Domain List Type'),
|
||||||
|
_('Select how to add your custom domains'),
|
||||||
|
);
|
||||||
|
o.value('disabled', _('Disabled'));
|
||||||
|
o.value('dynamic', _('Dynamic List'));
|
||||||
|
o.value('text', _('Text List'));
|
||||||
|
o.default = 'disabled';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.DynamicList,
|
||||||
|
'user_domains',
|
||||||
|
_('User Domains'),
|
||||||
|
_(
|
||||||
|
'Enter domain names without protocols (example: sub.example.com or example.com)',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
o.placeholder = 'Domains list';
|
||||||
|
o.depends('user_domain_list_type', 'dynamic');
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
// Optional
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = main.validateDomain(value, true);
|
||||||
|
|
||||||
|
if (validation.valid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.TextValue,
|
||||||
|
'user_domains_text',
|
||||||
|
_('User Domains List'),
|
||||||
|
_(
|
||||||
|
'Enter domain names separated by comma, space or newline. You can add comments after //',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
o.placeholder =
|
||||||
|
'example.com, sub.example.com\n// Social networks\ndomain.com test.com // personal domains';
|
||||||
|
o.depends('user_domain_list_type', 'text');
|
||||||
|
o.rows = 8;
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
// Optional
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domains = main.parseValueList(value);
|
||||||
|
|
||||||
|
if (!domains.length) {
|
||||||
|
return _(
|
||||||
|
'At least one valid domain must be specified. Comments-only content is not allowed.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { valid, results } = main.bulkValidate(domains, row => main.validateDomain(row, true));
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
const errors = results
|
||||||
|
.filter((validation) => !validation.valid) // Leave only failed validations
|
||||||
|
.map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors
|
||||||
|
|
||||||
|
return [_('Validation errors:'), ...errors].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
'local_domain_lists_enabled',
|
||||||
|
_('Local Domain Lists'),
|
||||||
|
_('Use the list from the router filesystem'),
|
||||||
|
);
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.DynamicList,
|
||||||
|
'local_domain_lists',
|
||||||
|
_('Local Domain List Paths'),
|
||||||
|
_('Enter the list file path'),
|
||||||
|
);
|
||||||
|
o.placeholder = '/path/file.lst';
|
||||||
|
o.depends('local_domain_lists_enabled', '1');
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
// Optional
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = main.validatePath(value);
|
||||||
|
|
||||||
|
if (validation.valid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
'remote_domain_lists_enabled',
|
||||||
|
_('Remote Domain Lists'),
|
||||||
|
_('Download and use domain lists from remote URLs'),
|
||||||
|
);
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.DynamicList,
|
||||||
|
'remote_domain_lists',
|
||||||
|
_('Remote Domain URLs'),
|
||||||
|
_('Enter full URLs starting with http:// or https://'),
|
||||||
|
);
|
||||||
|
o.placeholder = 'URL';
|
||||||
|
o.depends('remote_domain_lists_enabled', '1');
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
// Optional
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = main.validateUrl(value);
|
||||||
|
|
||||||
|
if (validation.valid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
'local_subnet_lists_enabled',
|
||||||
|
_('Local Subnet Lists'),
|
||||||
|
_('Use the list from the router filesystem'),
|
||||||
|
);
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.DynamicList,
|
||||||
|
'local_subnet_lists',
|
||||||
|
_('Local Subnet List Paths'),
|
||||||
|
_('Enter the list file path'),
|
||||||
|
);
|
||||||
|
o.placeholder = '/path/file.lst';
|
||||||
|
o.depends('local_subnet_lists_enabled', '1');
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
// Optional
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = main.validatePath(value);
|
||||||
|
|
||||||
|
if (validation.valid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.ListValue,
|
||||||
|
'user_subnet_list_type',
|
||||||
|
_('User Subnet List Type'),
|
||||||
|
_('Select how to add your custom subnets'),
|
||||||
|
);
|
||||||
|
o.value('disabled', _('Disabled'));
|
||||||
|
o.value('dynamic', _('Dynamic List'));
|
||||||
|
o.value('text', _('Text List (comma/space/newline separated)'));
|
||||||
|
o.default = 'disabled';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.DynamicList,
|
||||||
|
'user_subnets',
|
||||||
|
_('User Subnets'),
|
||||||
|
_(
|
||||||
|
'Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
o.placeholder = 'IP or subnet';
|
||||||
|
o.depends('user_subnet_list_type', 'dynamic');
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
// Optional
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = main.validateSubnet(value);
|
||||||
|
|
||||||
|
if (validation.valid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.TextValue,
|
||||||
|
'user_subnets_text',
|
||||||
|
_('User Subnets List'),
|
||||||
|
_(
|
||||||
|
'Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
o.placeholder =
|
||||||
|
'103.21.244.0/22\n// Google DNS\n8.8.8.8\n1.1.1.1/32, 9.9.9.9 // Cloudflare and Quad9';
|
||||||
|
o.depends('user_subnet_list_type', 'text');
|
||||||
|
o.rows = 10;
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
// Optional
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subnets = main.parseValueList(value);
|
||||||
|
|
||||||
|
if (!subnets.length) {
|
||||||
|
return _(
|
||||||
|
'At least one valid subnet or IP must be specified. Comments-only content is not allowed.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { valid, results } = main.bulkValidate(subnets, main.validateSubnet);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
const errors = results
|
||||||
|
.filter((validation) => !validation.valid) // Leave only failed validations
|
||||||
|
.map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors
|
||||||
|
|
||||||
|
return [_('Validation errors:'), ...errors].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
'remote_subnet_lists_enabled',
|
||||||
|
_('Remote Subnet Lists'),
|
||||||
|
_('Download and use subnet lists from remote URLs'),
|
||||||
|
);
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.DynamicList,
|
||||||
|
'remote_subnet_lists',
|
||||||
|
_('Remote Subnet URLs'),
|
||||||
|
_('Enter full URLs starting with http:// or https://'),
|
||||||
|
);
|
||||||
|
o.placeholder = 'URL';
|
||||||
|
o.depends('remote_subnet_lists_enabled', '1');
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
// Optional
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = main.validateUrl(value);
|
||||||
|
|
||||||
|
if (validation.valid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
'all_traffic_from_ip_enabled',
|
||||||
|
_('IP for full redirection'),
|
||||||
|
_(
|
||||||
|
'Specify local IP addresses whose traffic will always use the configured route',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.DynamicList,
|
||||||
|
'all_traffic_ip',
|
||||||
|
_('Local IPs'),
|
||||||
|
_('Enter valid IPv4 addresses'),
|
||||||
|
);
|
||||||
|
o.placeholder = 'IP';
|
||||||
|
o.depends('all_traffic_from_ip_enabled', '1');
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
// Optional
|
||||||
|
if (!value || value.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = main.validateSubnet(value);
|
||||||
|
|
||||||
|
if (validation.valid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
'socks5',
|
||||||
|
_('Mixed enable'),
|
||||||
|
_('Browser port: 2080'),
|
||||||
|
);
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntryPoint = {
|
||||||
|
createSectionContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseclass.extend(EntryPoint);
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
'use strict';
|
||||||
|
'require form';
|
||||||
|
'require baseclass';
|
||||||
|
'require tools.widgets as widgets';
|
||||||
|
'require view.podkop.main as main';
|
||||||
|
|
||||||
|
function createSettingsContent(section) {
|
||||||
|
let o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
'yacd',
|
||||||
|
_('Yacd enable'),
|
||||||
|
`<a href="${main.getBaseUrl()}:9090/ui" target="_blank">${main.getBaseUrl()}:9090/ui</a>`,
|
||||||
|
);
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
'exclude_ntp',
|
||||||
|
_('Exclude NTP'),
|
||||||
|
_('Allows you to exclude NTP protocol traffic from the tunnel'),
|
||||||
|
);
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
'quic_disable',
|
||||||
|
_('QUIC disable'),
|
||||||
|
_('For issues with the video stream'),
|
||||||
|
);
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.ListValue,
|
||||||
|
'update_interval',
|
||||||
|
_('List Update Frequency'),
|
||||||
|
_('Select how often the lists will be updated'),
|
||||||
|
);
|
||||||
|
Object.entries(main.UPDATE_INTERVAL_OPTIONS).forEach(([key, label]) => {
|
||||||
|
o.value(key, _(label));
|
||||||
|
});
|
||||||
|
o.default = '1d';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.ListValue,
|
||||||
|
'dns_type',
|
||||||
|
_('DNS Protocol Type'),
|
||||||
|
_('Select DNS protocol to use'),
|
||||||
|
);
|
||||||
|
o.value('doh', _('DNS over HTTPS (DoH)'));
|
||||||
|
o.value('dot', _('DNS over TLS (DoT)'));
|
||||||
|
o.value('udp', _('UDP (Unprotected DNS)'));
|
||||||
|
o.default = 'udp';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Value,
|
||||||
|
'dns_server',
|
||||||
|
_('DNS Server'),
|
||||||
|
_('Select or enter DNS server address'),
|
||||||
|
);
|
||||||
|
Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => {
|
||||||
|
o.value(key, _(label));
|
||||||
|
});
|
||||||
|
o.default = '8.8.8.8';
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
const validation = main.validateDNS(value);
|
||||||
|
|
||||||
|
if (validation.valid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Value,
|
||||||
|
'bootstrap_dns_server',
|
||||||
|
_('Bootstrap DNS server'),
|
||||||
|
_(
|
||||||
|
'The DNS server used to look up the IP address of an upstream DNS server',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Object.entries(main.BOOTSTRAP_DNS_SERVER_OPTIONS).forEach(([key, label]) => {
|
||||||
|
o.value(key, _(label));
|
||||||
|
});
|
||||||
|
o.default = '77.88.8.8';
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
const validation = main.validateDNS(value);
|
||||||
|
|
||||||
|
if (validation.valid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Value,
|
||||||
|
'dns_rewrite_ttl',
|
||||||
|
_('DNS Rewrite TTL'),
|
||||||
|
_('Time in seconds for DNS record caching (default: 60)'),
|
||||||
|
);
|
||||||
|
o.default = '60';
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
if (!value) {
|
||||||
|
return _('TTL value cannot be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ttl = parseInt(value);
|
||||||
|
if (isNaN(ttl) || ttl < 0) {
|
||||||
|
return _('TTL must be a positive number');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.ListValue,
|
||||||
|
'config_path',
|
||||||
|
_('Config File Path'),
|
||||||
|
_(
|
||||||
|
'Select path for sing-box config file. Change this ONLY if you know what you are doing',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
o.value('/etc/sing-box/config.json', 'Flash (/etc/sing-box/config.json)');
|
||||||
|
o.value('/tmp/sing-box/config.json', 'RAM (/tmp/sing-box/config.json)');
|
||||||
|
o.default = '/etc/sing-box/config.json';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Value,
|
||||||
|
'cache_path',
|
||||||
|
_('Cache File Path'),
|
||||||
|
_(
|
||||||
|
'Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
o.value('/tmp/sing-box/cache.db', 'RAM (/tmp/sing-box/cache.db)');
|
||||||
|
o.value(
|
||||||
|
'/usr/share/sing-box/cache.db',
|
||||||
|
'Flash (/usr/share/sing-box/cache.db)',
|
||||||
|
);
|
||||||
|
o.default = '/tmp/sing-box/cache.db';
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
if (!value) {
|
||||||
|
return _('Cache file path cannot be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value.startsWith('/')) {
|
||||||
|
return _('Path must be absolute (start with /)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value.endsWith('cache.db')) {
|
||||||
|
return _('Path must end with cache.db');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = value.split('/').filter(Boolean);
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return _('Path must contain at least one directory (like /tmp/cache.db)');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
widgets.DeviceSelect,
|
||||||
|
'iface',
|
||||||
|
_('Source Network Interface'),
|
||||||
|
_('Select the network interface from which the traffic will originate'),
|
||||||
|
);
|
||||||
|
o.default = 'br-lan';
|
||||||
|
o.noaliases = true;
|
||||||
|
o.nobridges = false;
|
||||||
|
o.noinactive = false;
|
||||||
|
o.multiple = true;
|
||||||
|
o.filter = function (section_id, value) {
|
||||||
|
// Block specific interface names from being selectable
|
||||||
|
const blocked = ['wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan'];
|
||||||
|
if (blocked.includes(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find the device object by its name
|
||||||
|
const device = this.devices.find((dev) => dev.getName() === value);
|
||||||
|
|
||||||
|
// If no device is found, allow the value
|
||||||
|
if (!device) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the type of the device
|
||||||
|
const type = device.getType();
|
||||||
|
|
||||||
|
// Consider any Wi-Fi / wireless / wlan device as invalid
|
||||||
|
const isWireless =
|
||||||
|
type === 'wifi' || type === 'wireless' || type.includes('wlan');
|
||||||
|
|
||||||
|
// Allow only non-wireless devices
|
||||||
|
return !isWireless;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
'mon_restart_ifaces',
|
||||||
|
_('Interface monitoring'),
|
||||||
|
_('Interface monitoring for bad WAN'),
|
||||||
|
);
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
widgets.NetworkSelect,
|
||||||
|
'restart_ifaces',
|
||||||
|
_('Interface for monitoring'),
|
||||||
|
_('Select the WAN interfaces to be monitored'),
|
||||||
|
);
|
||||||
|
o.depends('mon_restart_ifaces', '1');
|
||||||
|
o.multiple = true;
|
||||||
|
o.filter = function (section_id, value) {
|
||||||
|
// Reject if the value is in the blocked list ['lan', 'loopback']
|
||||||
|
if (['lan', 'loopback'].includes(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject if the value starts with '@' (means it's an alias/reference)
|
||||||
|
if (value.startsWith('@')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise allow it
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Value,
|
||||||
|
'procd_reload_delay',
|
||||||
|
_('Interface Monitoring Delay'),
|
||||||
|
_('Delay in milliseconds before reloading podkop after interface UP'),
|
||||||
|
);
|
||||||
|
o.depends('mon_restart_ifaces', '1');
|
||||||
|
o.default = '2000';
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = function (section_id, value) {
|
||||||
|
if (!value) {
|
||||||
|
return _('Delay value cannot be empty');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
'dont_touch_dhcp',
|
||||||
|
_('Dont touch my DHCP!'),
|
||||||
|
_('Podkop will not change the DHCP config'),
|
||||||
|
);
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = section.option(
|
||||||
|
form.Flag,
|
||||||
|
'detour',
|
||||||
|
_('Proxy download of lists'),
|
||||||
|
_('Downloading all lists via main Proxy/VPN'),
|
||||||
|
);
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntryPoint = {
|
||||||
|
createSettingsContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseclass.extend(EntryPoint);
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
'require baseclass';
|
|
||||||
'require ui';
|
|
||||||
'require fs';
|
|
||||||
'require view.podkop.main as main';
|
|
||||||
|
|
||||||
// Flag to track if this is the first error check
|
|
||||||
let isInitialCheck = true;
|
|
||||||
|
|
||||||
// Set to track which errors we've already seen
|
|
||||||
const lastErrorsSet = new Set();
|
|
||||||
|
|
||||||
// Timer for periodic error polling
|
|
||||||
let errorPollTimer = null;
|
|
||||||
|
|
||||||
// Helper function to fetch errors from the podkop command
|
|
||||||
async function getPodkopErrors() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
safeExec('/usr/bin/podkop', ['check_logs'], 'P0_PRIORITY', (result) => {
|
|
||||||
if (!result || !result.stdout) return resolve([]);
|
|
||||||
|
|
||||||
const logs = result.stdout.split('\n');
|
|
||||||
const errors = logs.filter((log) => log.includes('[critical]'));
|
|
||||||
|
|
||||||
resolve(errors);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show error notification to the user
|
|
||||||
function showErrorNotification(error, isMultiple = false) {
|
|
||||||
const notificationContent = E('div', { class: 'alert-message error' }, [
|
|
||||||
E('pre', { class: 'error-log' }, error),
|
|
||||||
]);
|
|
||||||
|
|
||||||
ui.addNotification(null, notificationContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function for command execution with prioritization
|
|
||||||
function safeExec(
|
|
||||||
command,
|
|
||||||
args,
|
|
||||||
priority,
|
|
||||||
callback,
|
|
||||||
timeout = main.COMMAND_TIMEOUT,
|
|
||||||
) {
|
|
||||||
// Default to highest priority execution if priority is not provided or invalid
|
|
||||||
let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY;
|
|
||||||
|
|
||||||
// If priority is a string, try to get the corresponding delay value
|
|
||||||
if (
|
|
||||||
typeof priority === 'string' &&
|
|
||||||
main.COMMAND_SCHEDULING[priority] !== undefined
|
|
||||||
) {
|
|
||||||
schedulingDelay = main.COMMAND_SCHEDULING[priority];
|
|
||||||
}
|
|
||||||
|
|
||||||
const executeCommand = async () => {
|
|
||||||
try {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
||||||
|
|
||||||
const result = await Promise.race([
|
|
||||||
fs.exec(command, args),
|
|
||||||
new Promise((_, reject) => {
|
|
||||||
controller.signal.addEventListener('abort', () => {
|
|
||||||
reject(new Error('Command execution timed out'));
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (callback && typeof callback === 'function') {
|
|
||||||
callback(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(
|
|
||||||
`Command execution failed or timed out: ${command} ${args.join(' ')}`,
|
|
||||||
);
|
|
||||||
const errorResult = { stdout: '', stderr: error.message, error: error };
|
|
||||||
|
|
||||||
if (callback && typeof callback === 'function') {
|
|
||||||
callback(errorResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
return errorResult;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (callback && typeof callback === 'function') {
|
|
||||||
setTimeout(executeCommand, schedulingDelay);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
return executeCommand();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for critical errors and show notifications
|
|
||||||
async function checkForCriticalErrors() {
|
|
||||||
try {
|
|
||||||
const errors = await getPodkopErrors();
|
|
||||||
|
|
||||||
if (errors && errors.length > 0) {
|
|
||||||
// Filter out errors we've already seen
|
|
||||||
const newErrors = errors.filter((error) => !lastErrorsSet.has(error));
|
|
||||||
|
|
||||||
if (newErrors.length > 0) {
|
|
||||||
// On initial check, just store errors without showing notifications
|
|
||||||
if (!isInitialCheck) {
|
|
||||||
// Show each new error as a notification
|
|
||||||
newErrors.forEach((error) => {
|
|
||||||
showErrorNotification(error, newErrors.length > 1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new errors to our set of seen errors
|
|
||||||
newErrors.forEach((error) => lastErrorsSet.add(error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// After first check, mark as no longer initial
|
|
||||||
isInitialCheck = false;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking for critical messages:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start polling for errors at regular intervals
|
|
||||||
function startErrorPolling() {
|
|
||||||
if (errorPollTimer) {
|
|
||||||
clearInterval(errorPollTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset initial check flag to make sure we show errors
|
|
||||||
isInitialCheck = false;
|
|
||||||
|
|
||||||
// Immediately check for errors on start
|
|
||||||
checkForCriticalErrors();
|
|
||||||
|
|
||||||
// Then set up periodic checks
|
|
||||||
errorPollTimer = setInterval(
|
|
||||||
checkForCriticalErrors,
|
|
||||||
main.ERROR_POLL_INTERVAL,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop polling for errors
|
|
||||||
function stopErrorPolling() {
|
|
||||||
if (errorPollTimer) {
|
|
||||||
clearInterval(errorPollTimer);
|
|
||||||
errorPollTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseclass.extend({
|
|
||||||
startErrorPolling,
|
|
||||||
stopErrorPolling,
|
|
||||||
checkForCriticalErrors,
|
|
||||||
safeExec,
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user