diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js
index 501de00..2686fb0 100644
--- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js
+++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js
@@ -5,235 +5,358 @@
'require view.podkop.main as main';
function createAdditionalSection(mainSection) {
- let o = mainSection.tab('additional', _('Additional Settings'));
+ let o = mainSection.tab('additional', _('Additional Settings'));
- o = mainSection.taboption('additional', form.Flag, 'yacd', _('Yacd enable'), `${main.getBaseUrl()}:9090/ui`);
- o.default = '0';
- o.rmempty = false;
- o.ucisection = 'main';
+ o = mainSection.taboption(
+ 'additional',
+ form.Flag,
+ 'yacd',
+ _('Yacd enable'),
+ `${main.getBaseUrl()}:9090/ui`,
+ );
+ 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,
+ '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.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,
+ '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.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);
+ 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;
- }
+ if (validation.valid) {
+ return true;
+ }
- return _(validation.message);
- };
+ 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);
+ 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;
- }
+ if (validation.valid) {
+ return true;
+ }
- return _(validation.message);
- };
+ 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');
- }
+ 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');
- }
+ const ttl = parseInt(value);
+ if (isNaN(ttl) || ttl < 0) {
+ return _('TTL must be a positive number');
+ }
- return true;
- };
+ 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.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');
- }
+ 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.startsWith('/')) {
+ return _('Path must be absolute (start with /)');
+ }
- if (!value.endsWith('cache.db')) {
- return _('Path must end with cache.db');
- }
+ 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)');
- }
+ const parts = value.split('/').filter(Boolean);
+ if (parts.length < 2) {
+ return _('Path must contain at least one directory (like /tmp/cache.db)');
+ }
- return true;
- };
+ 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;
- }
+ 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);
+ // 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;
- }
+ // If no device is found, allow the value
+ if (!device) {
+ return true;
+ }
- // Check the type of the device
- const type = device.getType();
+ // 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');
+ // 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;
- };
+ // 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',
+ 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;
- }
+ 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;
- }
+ // Reject if the value starts with '@' (means it's an alias/reference)
+ if (value.startsWith('@')) {
+ return false;
+ }
- // Otherwise allow it
- return true;
- };
+ // 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.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,
+ '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';
+ 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';
+ // 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
- }
+ 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);
+ const validation = main.validateIPV4(value);
- if (validation.valid) {
- return true;
- }
+ if (validation.valid) {
+ return true;
+ }
- return _(validation.message)
- };
+ return _(validation.message);
+ };
- o = mainSection.taboption('basic', form.Flag, 'socks5', _('Mixed enable'), _('Browser port: 2080'));
- o.default = '0';
- o.rmempty = false;
- o.ucisection = 'main';
+ 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
+ createAdditionalSection,
});
diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js
index 0c215c6..08d8fae 100644
--- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js
+++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js
@@ -6,541 +6,772 @@
'require view.podkop.main as main';
'require tools.widgets as widgets';
-
function createConfigSection(section) {
- const s = section;
+ const s = section;
- let o = s.tab('basic', _('Basic Settings'));
+ 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,
+ '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.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;
- o.wrap = 'soft';
- 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 = s.taboption(
+ 'basic',
+ form.TextValue,
+ 'proxy_string',
+ _('Proxy Configuration URL'),
+ '',
+ );
+ o.depends('proxy_config_type', 'url');
+ o.rows = 5;
+ o.wrap = 'soft';
+ 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);
+ 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 (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);
+ 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 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 activeConfig = value.split('\n')
- .map(line => line.trim())
- .find(line => line && !line.startsWith('//'));
-
- if (!activeConfig) {
- return _('No active configuration found. At least one non-commented line is required.');
- }
-
- const validation = main.validateProxyUrl(activeConfig);
-
- 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.textarea = true;
- o.rows = 3;
- o.wrap = 'soft';
- 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') + ' github.com/itdoginfo/allow-domains');
- 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);
-
- 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.'
+ } 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);
+ }
- const { valid, results } = main.bulkValidate(domains, main.validateDomain);
+ return container;
+ };
- if (!valid) {
- const errors = results
- .filter(validation => !validation.valid) // Leave only failed validations
- .map((validation) => _(`${validation.value}: ${validation.message}`)) // Collect validation errors
+ o.validate = function (section_id, value) {
+ // Optional
+ if (!value || value.length === 0) {
+ return true;
+ }
- return [_('Validation errors:'), ...errors].join('\n');
- }
+ try {
+ const activeConfig = value
+ .split('\n')
+ .map((line) => line.trim())
+ .find((line) => line && !line.startsWith('//'));
+ if (!activeConfig) {
+ return _(
+ 'No active configuration found. At least one non-commented line is required.',
+ );
+ }
+
+ const validation = main.validateProxyUrl(activeConfig);
+
+ if (validation.valid) {
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;
+ return _(validation.message);
+ } catch (e) {
+ return `${_('Invalid URL format:')} ${e?.message}`;
+ }
+ };
- 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
+ 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.textarea = true;
+ o.rows = 3;
+ o.wrap = 'soft';
+ 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') +
+ ' github.com/itdoginfo/allow-domains',
+ );
+ 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(', '),
+ ),
+ ]),
+ );
}
+ }
- const validation = main.validatePath(value);
+ if (JSON.stringify(newValues.sort()) !== JSON.stringify(values.sort())) {
+ this.getUIElement(section_id).setValue(newValues);
+ }
- if (validation.valid) {
- return true;
- }
+ notifications.forEach((notification) =>
+ ui.addNotification(null, notification),
+ );
+ lastValues = newValues;
+ } catch (e) {
+ console.error('Error in onchange handler:', e);
+ } finally {
+ isProcessing = false;
+ }
+ };
- return _(validation.message)
- };
+ 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.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,
+ '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;
+ }
- 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.validateDomain(value);
- const validation = main.validateUrl(value);
+ if (validation.valid) {
+ return true;
+ }
- if (validation.valid) {
- return true;
- }
+ return _(validation.message);
+ };
- 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;
+ }
- 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;
+ const domains = main.parseValueList(value);
- 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
- }
+ if (!domains.length) {
+ return _(
+ 'At least one valid domain must be specified. Comments-only content is not allowed.',
+ );
+ }
- const validation = main.validatePath(value);
+ const { valid, results } = main.bulkValidate(domains, main.validateDomain);
- if (validation.valid) {
- return 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.message)
- };
+ return [_('Validation errors:'), ...errors].join('\n');
+ }
- 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;
+ return true;
+ };
- 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
- }
+ 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;
- const validation = main.validateSubnet(value);
+ 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;
+ }
- if (validation.valid) {
- return true;
- }
+ const validation = main.validatePath(value);
- return _(validation.message)
- };
+ if (validation.valid) {
+ return true;
+ }
- 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
- }
+ return _(validation.message);
+ };
- const subnets = main.parseValueList(value);
+ 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;
- if (!subnets.length) {
- return _(
- 'At least one valid subnet or IP must be specified. Comments-only content is not allowed.'
- );
- }
+ 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 { valid, results } = main.bulkValidate(subnets, main.validateSubnet);
+ const validation = main.validateUrl(value);
- if (!valid) {
- const errors = results
- .filter(validation => !validation.valid) // Leave only failed validations
- .map((validation) => _(`${validation.value}: ${validation.message}`)) // Collect validation errors
+ if (validation.valid) {
+ return true;
+ }
- return [_('Validation errors:'), ...errors].join('\n');
- }
+ return _(validation.message);
+ };
- return true;
- };
+ 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.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,
+ '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;
+ }
- 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.validatePath(value);
- const validation = main.validateUrl(value);
+ if (validation.valid) {
+ return true;
+ }
- if (validation.valid) {
- return true;
- }
+ return _(validation.message);
+ };
- 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.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,
+ '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;
+ }
- 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);
- const validation = main.validateIPV4(value);
+ if (validation.valid) {
+ return true;
+ }
- if (validation.valid) {
- return true;
- }
+ return _(validation.message);
+ };
- 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.validateIPV4(value);
+
+ if (validation.valid) {
+ return true;
+ }
+
+ return _(validation.message);
+ };
}
return baseclass.extend({
- createConfigSection
+ createConfigSection,
});
diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticTab.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticTab.js
index 1768b43..5b5b2cf 100644
--- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticTab.js
+++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticTab.js
@@ -12,553 +12,770 @@ const fetchCache = {};
// Helper function to fetch with cache
async function cachedFetch(url, options = {}) {
- const cacheKey = url;
- const currentTime = Date.now();
+ const cacheKey = url;
+ const currentTime = Date.now();
- // If we have a valid cached response, return it
- if (fetchCache[cacheKey] && currentTime - fetchCache[cacheKey].timestamp < main.CACHE_TIMEOUT) {
- console.log(`Using cached response for ${url}`);
- return Promise.resolve(fetchCache[cacheKey].response.clone());
- }
+ // If we have a valid cached response, return it
+ if (
+ fetchCache[cacheKey] &&
+ currentTime - fetchCache[cacheKey].timestamp < main.CACHE_TIMEOUT
+ ) {
+ console.log(`Using cached response for ${url}`);
+ return Promise.resolve(fetchCache[cacheKey].response.clone());
+ }
- // Otherwise, make a new request
- try {
- const response = await fetch(url, options);
+ // Otherwise, make a new request
+ try {
+ const response = await fetch(url, options);
- // Cache the response
- fetchCache[cacheKey] = {
- response: response.clone(),
- timestamp: currentTime
- };
+ // Cache the response
+ fetchCache[cacheKey] = {
+ response: response.clone(),
+ timestamp: currentTime,
+ };
- return response;
- } catch (error) {
- throw error;
- }
+ return response;
+ } catch (error) {
+ throw error;
+ }
}
// Helper functions for command execution with prioritization - Using from utils.js now
-function safeExec(command, args, priority, callback, timeout = main.COMMAND_TIMEOUT) {
- return utils.safeExec(command, args, priority, callback, timeout);
+function safeExec(
+ command,
+ args,
+ priority,
+ callback,
+ timeout = main.COMMAND_TIMEOUT,
+) {
+ return utils.safeExec(command, args, priority, callback, timeout);
}
// Helper functions for handling checks
function runCheck(checkFunction, priority, callback) {
- // Default to highest priority execution if priority is not provided or invalid
- let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY;
+ // 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];
+ // 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 executeCheck = async () => {
+ try {
+ const result = await checkFunction();
+ if (callback && typeof callback === 'function') {
+ callback(result);
+ }
+ return result;
+ } catch (error) {
+ if (callback && typeof callback === 'function') {
+ callback({ error });
+ }
+ return { error };
}
+ };
- const executeCheck = async () => {
- try {
- const result = await checkFunction();
- if (callback && typeof callback === 'function') {
- callback(result);
- }
- return result;
- } catch (error) {
- if (callback && typeof callback === 'function') {
- callback({ error });
- }
- return { error };
- }
- };
-
- if (callback && typeof callback === 'function') {
- setTimeout(executeCheck, schedulingDelay);
- return;
- } else {
- return executeCheck();
- }
+ if (callback && typeof callback === 'function') {
+ setTimeout(executeCheck, schedulingDelay);
+ return;
+ } else {
+ return executeCheck();
+ }
}
function runAsyncTask(taskFunction, priority) {
- // Default to highest priority execution if priority is not provided or invalid
- let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY;
+ // 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];
+ // 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];
+ }
+
+ setTimeout(async () => {
+ try {
+ await taskFunction();
+ } catch (error) {
+ console.error('Async task error:', error);
}
-
- setTimeout(async () => {
- try {
- await taskFunction();
- } catch (error) {
- console.error('Async task error:', error);
- }
- }, schedulingDelay);
+ }, schedulingDelay);
}
// Helper Functions for UI and formatting
function createStatus(state, message, color) {
- return {
- state,
- message: _(message),
- color: main.STATUS_COLORS[color]
- };
+ return {
+ state,
+ message: _(message),
+ color: main.STATUS_COLORS[color],
+ };
}
function formatDiagnosticOutput(output) {
- if (typeof output !== 'string') return '';
- return output.trim()
- .replace(/\x1b\[[0-9;]*m/g, '')
- .replace(/\r\n/g, '\n')
- .replace(/\r/g, '\n');
+ if (typeof output !== 'string') return '';
+ return output
+ .trim()
+ .replace(/\x1b\[[0-9;]*m/g, '')
+ .replace(/\r\n/g, '\n')
+ .replace(/\r/g, '\n');
}
function copyToClipboard(text, button) {
- const textarea = document.createElement('textarea');
- textarea.value = text;
- document.body.appendChild(textarea);
- textarea.select();
- try {
- document.execCommand('copy');
- const originalText = button.textContent;
- button.textContent = _('Copied!');
- setTimeout(() => button.textContent = originalText, main.BUTTON_FEEDBACK_TIMEOUT);
- } catch (err) {
- ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message));
- }
- document.body.removeChild(textarea);
+ const textarea = document.createElement('textarea');
+ textarea.value = text;
+ document.body.appendChild(textarea);
+ textarea.select();
+ try {
+ document.execCommand('copy');
+ const originalText = button.textContent;
+ button.textContent = _('Copied!');
+ setTimeout(
+ () => (button.textContent = originalText),
+ main.BUTTON_FEEDBACK_TIMEOUT,
+ );
+ } catch (err) {
+ ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message));
+ }
+ document.body.removeChild(textarea);
}
// IP masking function
function maskIP(ip) {
- if (!ip) return '';
- const parts = ip.split('.');
- if (parts.length !== 4) return ip;
- return ['XX', 'XX', 'XX', parts[3]].join('.');
+ if (!ip) return '';
+ const parts = ip.split('.');
+ if (parts.length !== 4) return ip;
+ return ['XX', 'XX', 'XX', parts[3]].join('.');
}
// Status Check Functions
async function checkFakeIP() {
+ try {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), main.FETCH_TIMEOUT);
+
try {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), main.FETCH_TIMEOUT);
+ const response = await cachedFetch(
+ `https://${main.FAKEIP_CHECK_DOMAIN}/check`,
+ { signal: controller.signal },
+ );
+ const data = await response.json();
+ clearTimeout(timeoutId);
- try {
- const response = await cachedFetch(`https://${main.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal });
- const data = await response.json();
- clearTimeout(timeoutId);
-
- if (data.fakeip === true) {
- return createStatus('working', 'working', 'SUCCESS');
- } else {
- return createStatus('not_working', 'not working', 'ERROR');
- }
- } catch (fetchError) {
- clearTimeout(timeoutId);
- const message = fetchError.name === 'AbortError' ? 'timeout' : 'check error';
- return createStatus('error', message, 'WARNING');
- }
- } catch (error) {
- return createStatus('error', 'check error', 'WARNING');
+ if (data.fakeip === true) {
+ return createStatus('working', 'working', 'SUCCESS');
+ } else {
+ return createStatus('not_working', 'not working', 'ERROR');
+ }
+ } catch (fetchError) {
+ clearTimeout(timeoutId);
+ const message =
+ fetchError.name === 'AbortError' ? 'timeout' : 'check error';
+ return createStatus('error', message, 'WARNING');
}
+ } catch (error) {
+ return createStatus('error', 'check error', 'WARNING');
+ }
}
async function checkFakeIPCLI() {
- try {
- return new Promise((resolve) => {
- safeExec('nslookup', ['-timeout=2', main.FAKEIP_CHECK_DOMAIN, '127.0.0.42'], 'P0_PRIORITY', result => {
- if (result.stdout && result.stdout.includes('198.18')) {
- resolve(createStatus('working', 'working on router', 'SUCCESS'));
- } else {
- resolve(createStatus('not_working', 'not working on router', 'ERROR'));
- }
- });
- });
- } catch (error) {
- return createStatus('error', 'CLI check error', 'WARNING');
- }
+ try {
+ return new Promise((resolve) => {
+ safeExec(
+ 'nslookup',
+ ['-timeout=2', main.FAKEIP_CHECK_DOMAIN, '127.0.0.42'],
+ 'P0_PRIORITY',
+ (result) => {
+ if (result.stdout && result.stdout.includes('198.18')) {
+ resolve(createStatus('working', 'working on router', 'SUCCESS'));
+ } else {
+ resolve(
+ createStatus('not_working', 'not working on router', 'ERROR'),
+ );
+ }
+ },
+ );
+ });
+ } catch (error) {
+ return createStatus('error', 'CLI check error', 'WARNING');
+ }
}
function checkDNSAvailability() {
- return new Promise(async (resolve) => {
- try {
- safeExec('/usr/bin/podkop', ['check_dns_available'], 'P0_PRIORITY', dnsStatusResult => {
- if (!dnsStatusResult || !dnsStatusResult.stdout) {
- return resolve({
- remote: createStatus('error', 'DNS check timeout', 'WARNING'),
- local: createStatus('error', 'DNS check timeout', 'WARNING')
- });
- }
-
- try {
- const dnsStatus = JSON.parse(dnsStatusResult.stdout);
-
- const remoteStatus = dnsStatus.is_available ?
- createStatus('available', `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) available`, 'SUCCESS') :
- createStatus('unavailable', `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) unavailable`, 'ERROR');
-
- const localStatus = dnsStatus.local_dns_working ?
- createStatus('available', 'Router DNS working', 'SUCCESS') :
- createStatus('unavailable', 'Router DNS not working', 'ERROR');
-
- return resolve({
- remote: remoteStatus,
- local: localStatus
- });
- } catch (parseError) {
- return resolve({
- remote: createStatus('error', 'DNS check parse error', 'WARNING'),
- local: createStatus('error', 'DNS check parse error', 'WARNING')
- });
- }
- });
- } catch (error) {
+ return new Promise(async (resolve) => {
+ try {
+ safeExec(
+ '/usr/bin/podkop',
+ ['check_dns_available'],
+ 'P0_PRIORITY',
+ (dnsStatusResult) => {
+ if (!dnsStatusResult || !dnsStatusResult.stdout) {
return resolve({
- remote: createStatus('error', 'DNS check error', 'WARNING'),
- local: createStatus('error', 'DNS check error', 'WARNING')
+ remote: createStatus('error', 'DNS check timeout', 'WARNING'),
+ local: createStatus('error', 'DNS check timeout', 'WARNING'),
});
- }
- });
+ }
+
+ try {
+ const dnsStatus = JSON.parse(dnsStatusResult.stdout);
+
+ const remoteStatus = dnsStatus.is_available
+ ? createStatus(
+ 'available',
+ `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) available`,
+ 'SUCCESS',
+ )
+ : createStatus(
+ 'unavailable',
+ `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) unavailable`,
+ 'ERROR',
+ );
+
+ const localStatus = dnsStatus.local_dns_working
+ ? createStatus('available', 'Router DNS working', 'SUCCESS')
+ : createStatus('unavailable', 'Router DNS not working', 'ERROR');
+
+ return resolve({
+ remote: remoteStatus,
+ local: localStatus,
+ });
+ } catch (parseError) {
+ return resolve({
+ remote: createStatus('error', 'DNS check parse error', 'WARNING'),
+ local: createStatus('error', 'DNS check parse error', 'WARNING'),
+ });
+ }
+ },
+ );
+ } catch (error) {
+ return resolve({
+ remote: createStatus('error', 'DNS check error', 'WARNING'),
+ local: createStatus('error', 'DNS check error', 'WARNING'),
+ });
+ }
+ });
}
async function checkBypass() {
+ try {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), main.FETCH_TIMEOUT);
+
try {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), main.FETCH_TIMEOUT);
+ const response1 = await cachedFetch(
+ `https://${main.FAKEIP_CHECK_DOMAIN}/check`,
+ { signal: controller.signal },
+ );
+ const data1 = await response1.json();
- try {
- const response1 = await cachedFetch(`https://${main.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal });
- const data1 = await response1.json();
+ const response2 = await cachedFetch(
+ `https://${main.IP_CHECK_DOMAIN}/check`,
+ { signal: controller.signal },
+ );
+ const data2 = await response2.json();
- const response2 = await cachedFetch(`https://${main.IP_CHECK_DOMAIN}/check`, { signal: controller.signal });
- const data2 = await response2.json();
+ clearTimeout(timeoutId);
- clearTimeout(timeoutId);
-
- if (data1.IP && data2.IP) {
- if (data1.IP !== data2.IP) {
- return createStatus('working', 'working', 'SUCCESS');
- } else {
- return createStatus('not_working', 'same IP for both domains', 'ERROR');
- }
- } else {
- return createStatus('error', 'check error (no IP)', 'WARNING');
- }
- } catch (fetchError) {
- clearTimeout(timeoutId);
- const message = fetchError.name === 'AbortError' ? 'timeout' : 'check error';
- return createStatus('error', message, 'WARNING');
+ if (data1.IP && data2.IP) {
+ if (data1.IP !== data2.IP) {
+ return createStatus('working', 'working', 'SUCCESS');
+ } else {
+ return createStatus(
+ 'not_working',
+ 'same IP for both domains',
+ 'ERROR',
+ );
}
- } catch (error) {
- return createStatus('error', 'check error', 'WARNING');
+ } else {
+ return createStatus('error', 'check error (no IP)', 'WARNING');
+ }
+ } catch (fetchError) {
+ clearTimeout(timeoutId);
+ const message =
+ fetchError.name === 'AbortError' ? 'timeout' : 'check error';
+ return createStatus('error', message, 'WARNING');
}
+ } catch (error) {
+ return createStatus('error', 'check error', 'WARNING');
+ }
}
function showConfigModal(command, title) {
- // Create and show modal immediately with loading state
- const modalContent = E('div', { 'class': 'panel-body' }, [
- E('div', {
- 'class': 'panel-body',
- style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; ' +
- 'font-family: monospace; white-space: pre-wrap; word-wrap: break-word; ' +
- 'line-height: 1.5; font-size: 14px;'
- }, [
- E('pre', {
- 'id': 'modal-content-pre',
- style: 'margin: 0;'
- }, _('Loading...'))
- ]),
- E('div', {
- 'class': 'right',
- style: 'margin-top: 1em;'
- }, [
- E('button', {
- 'class': 'btn',
- 'id': 'copy-button',
- 'click': ev => copyToClipboard('```txt\n' + document.getElementById('modal-content-pre').innerText + '\n```', ev.target)
- }, _('Copy to Clipboard')),
- E('button', {
- 'class': 'btn',
- 'click': ui.hideModal
- }, _('Close'))
- ])
- ]);
+ // Create and show modal immediately with loading state
+ const modalContent = E('div', { class: 'panel-body' }, [
+ E(
+ 'div',
+ {
+ class: 'panel-body',
+ style:
+ 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; ' +
+ 'font-family: monospace; white-space: pre-wrap; word-wrap: break-word; ' +
+ 'line-height: 1.5; font-size: 14px;',
+ },
+ [
+ E(
+ 'pre',
+ {
+ id: 'modal-content-pre',
+ style: 'margin: 0;',
+ },
+ _('Loading...'),
+ ),
+ ],
+ ),
+ E(
+ 'div',
+ {
+ class: 'right',
+ style: 'margin-top: 1em;',
+ },
+ [
+ E(
+ 'button',
+ {
+ class: 'btn',
+ id: 'copy-button',
+ click: (ev) =>
+ copyToClipboard(
+ '```txt\n' +
+ document.getElementById('modal-content-pre').innerText +
+ '\n```',
+ ev.target,
+ ),
+ },
+ _('Copy to Clipboard'),
+ ),
+ E(
+ 'button',
+ {
+ class: 'btn',
+ click: ui.hideModal,
+ },
+ _('Close'),
+ ),
+ ],
+ ),
+ ]);
- ui.showModal(_(title), modalContent);
+ ui.showModal(_(title), modalContent);
- // Function to update modal content
- const updateModalContent = (content) => {
- const pre = document.getElementById('modal-content-pre');
- if (pre) {
- pre.textContent = content;
- }
- };
-
- try {
- let formattedOutput = '';
-
- if (command === 'global_check') {
- safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', res => {
- formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
-
- try {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), main.FETCH_TIMEOUT);
-
- cachedFetch(`https://${main.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal })
- .then(response => response.json())
- .then(data => {
- clearTimeout(timeoutId);
-
- if (data.fakeip === true) {
- formattedOutput += '\n✅ ' + _('FakeIP is working in browser!') + '\n';
- } else {
- formattedOutput += '\n❌ ' + _('FakeIP is not working in browser') + '\n';
- formattedOutput += _('Check DNS server on current device (PC, phone)') + '\n';
- formattedOutput += _('Its must be router!') + '\n';
- }
-
- // Bypass check
- cachedFetch(`https://${main.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal })
- .then(bypassResponse => bypassResponse.json())
- .then(bypassData => {
- cachedFetch(`https://${main.IP_CHECK_DOMAIN}/check`, { signal: controller.signal })
- .then(bypassResponse2 => bypassResponse2.json())
- .then(bypassData2 => {
- formattedOutput += '━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
-
- if (bypassData.IP && bypassData2.IP && bypassData.IP !== bypassData2.IP) {
- formattedOutput += '✅ ' + _('Proxy working correctly') + '\n';
- formattedOutput += _('Direct IP: ') + maskIP(bypassData.IP) + '\n';
- formattedOutput += _('Proxy IP: ') + maskIP(bypassData2.IP) + '\n';
- } else if (bypassData.IP === bypassData2.IP) {
- formattedOutput += '❌ ' + _('Proxy is not working - same IP for both domains') + '\n';
- formattedOutput += _('IP: ') + maskIP(bypassData.IP) + '\n';
- } else {
- formattedOutput += '❌ ' + _('Proxy check failed') + '\n';
- }
-
- updateModalContent(formattedOutput);
- })
- .catch(error => {
- formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n';
- updateModalContent(formattedOutput);
- });
- })
- .catch(error => {
- formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n';
- updateModalContent(formattedOutput);
- });
- })
- .catch(error => {
- formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n';
- updateModalContent(formattedOutput);
- });
- } catch (error) {
- formattedOutput += '\n❌ ' + _('Check failed: ') + error.message + '\n';
- updateModalContent(formattedOutput);
- }
- });
- } else {
- safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', res => {
- formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
- updateModalContent(formattedOutput);
- });
- }
- } catch (error) {
- updateModalContent(_('Error: ') + error.message);
+ // Function to update modal content
+ const updateModalContent = (content) => {
+ const pre = document.getElementById('modal-content-pre');
+ if (pre) {
+ pre.textContent = content;
}
+ };
+
+ try {
+ let formattedOutput = '';
+
+ if (command === 'global_check') {
+ safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', (res) => {
+ formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
+
+ try {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(
+ () => controller.abort(),
+ main.FETCH_TIMEOUT,
+ );
+
+ cachedFetch(`https://${main.FAKEIP_CHECK_DOMAIN}/check`, {
+ signal: controller.signal,
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ clearTimeout(timeoutId);
+
+ if (data.fakeip === true) {
+ formattedOutput +=
+ '\n✅ ' + _('FakeIP is working in browser!') + '\n';
+ } else {
+ formattedOutput +=
+ '\n❌ ' + _('FakeIP is not working in browser') + '\n';
+ formattedOutput +=
+ _('Check DNS server on current device (PC, phone)') + '\n';
+ formattedOutput += _('Its must be router!') + '\n';
+ }
+
+ // Bypass check
+ cachedFetch(`https://${main.FAKEIP_CHECK_DOMAIN}/check`, {
+ signal: controller.signal,
+ })
+ .then((bypassResponse) => bypassResponse.json())
+ .then((bypassData) => {
+ cachedFetch(`https://${main.IP_CHECK_DOMAIN}/check`, {
+ signal: controller.signal,
+ })
+ .then((bypassResponse2) => bypassResponse2.json())
+ .then((bypassData2) => {
+ formattedOutput += '━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
+
+ if (
+ bypassData.IP &&
+ bypassData2.IP &&
+ bypassData.IP !== bypassData2.IP
+ ) {
+ formattedOutput +=
+ '✅ ' + _('Proxy working correctly') + '\n';
+ formattedOutput +=
+ _('Direct IP: ') + maskIP(bypassData.IP) + '\n';
+ formattedOutput +=
+ _('Proxy IP: ') + maskIP(bypassData2.IP) + '\n';
+ } else if (bypassData.IP === bypassData2.IP) {
+ formattedOutput +=
+ '❌ ' +
+ _('Proxy is not working - same IP for both domains') +
+ '\n';
+ formattedOutput +=
+ _('IP: ') + maskIP(bypassData.IP) + '\n';
+ } else {
+ formattedOutput +=
+ '❌ ' + _('Proxy check failed') + '\n';
+ }
+
+ updateModalContent(formattedOutput);
+ })
+ .catch((error) => {
+ formattedOutput +=
+ '\n❌ ' +
+ _('Check failed: ') +
+ (error.name === 'AbortError'
+ ? _('timeout')
+ : error.message) +
+ '\n';
+ updateModalContent(formattedOutput);
+ });
+ })
+ .catch((error) => {
+ formattedOutput +=
+ '\n❌ ' +
+ _('Check failed: ') +
+ (error.name === 'AbortError'
+ ? _('timeout')
+ : error.message) +
+ '\n';
+ updateModalContent(formattedOutput);
+ });
+ })
+ .catch((error) => {
+ formattedOutput +=
+ '\n❌ ' +
+ _('Check failed: ') +
+ (error.name === 'AbortError' ? _('timeout') : error.message) +
+ '\n';
+ updateModalContent(formattedOutput);
+ });
+ } catch (error) {
+ formattedOutput +=
+ '\n❌ ' + _('Check failed: ') + error.message + '\n';
+ updateModalContent(formattedOutput);
+ }
+ });
+ } else {
+ safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', (res) => {
+ formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
+ updateModalContent(formattedOutput);
+ });
+ }
+ } catch (error) {
+ updateModalContent(_('Error: ') + error.message);
+ }
}
// Button Factory
const ButtonFactory = {
- createButton: function (config) {
- return E('button', {
- 'class': `btn ${config.additionalClass || ''}`.trim(),
- 'click': config.onClick,
- 'style': config.style || ''
- }, _(config.label));
- },
+ createButton: function (config) {
+ return E(
+ 'button',
+ {
+ class: `btn ${config.additionalClass || ''}`.trim(),
+ click: config.onClick,
+ style: config.style || '',
+ },
+ _(config.label),
+ );
+ },
- createActionButton: function (config) {
- return this.createButton({
- label: config.label,
- additionalClass: `cbi-button-${config.type || ''}`,
- onClick: () => safeExec('/usr/bin/podkop', [config.action], 'P0_PRIORITY')
- .then(() => config.reload && location.reload()),
- style: config.style
- });
- },
+ createActionButton: function (config) {
+ return this.createButton({
+ label: config.label,
+ additionalClass: `cbi-button-${config.type || ''}`,
+ onClick: () =>
+ safeExec('/usr/bin/podkop', [config.action], 'P0_PRIORITY').then(
+ () => config.reload && location.reload(),
+ ),
+ style: config.style,
+ });
+ },
- createInitActionButton: function (config) {
- return this.createButton({
- label: config.label,
- additionalClass: `cbi-button-${config.type || ''}`,
- onClick: () => safeExec('/etc/init.d/podkop', [config.action], 'P0_PRIORITY')
- .then(() => config.reload && location.reload()),
- style: config.style
- });
- },
+ createInitActionButton: function (config) {
+ return this.createButton({
+ label: config.label,
+ additionalClass: `cbi-button-${config.type || ''}`,
+ onClick: () =>
+ safeExec('/etc/init.d/podkop', [config.action], 'P0_PRIORITY').then(
+ () => config.reload && location.reload(),
+ ),
+ style: config.style,
+ });
+ },
- createModalButton: function (config) {
- return this.createButton({
- label: config.label,
- onClick: () => showConfigModal(config.command, config.title),
- additionalClass: `cbi-button-${config.type || ''}`,
- style: config.style
- });
- }
+ createModalButton: function (config) {
+ return this.createButton({
+ label: config.label,
+ onClick: () => showConfigModal(config.command, config.title),
+ additionalClass: `cbi-button-${config.type || ''}`,
+ style: config.style,
+ });
+ },
};
// Create a loading placeholder for status text
function createLoadingStatusText() {
- return E('span', { 'class': 'loading-indicator' }, _('Loading...'));
+ return E('span', { class: 'loading-indicator' }, _('Loading...'));
}
// Create the status section with buttons loaded immediately but status indicators loading asynchronously
let createStatusSection = async function () {
- // Get initial podkop status
- let initialPodkopStatus = { enabled: false };
- try {
- const result = await fs.exec('/usr/bin/podkop', ['get_status']);
- if (result && result.stdout) {
- const status = JSON.parse(result.stdout);
- initialPodkopStatus.enabled = status.enabled === 1;
- }
- } catch (e) {
- console.error('Error getting initial podkop status:', e);
+ // Get initial podkop status
+ let initialPodkopStatus = { enabled: false };
+ try {
+ const result = await fs.exec('/usr/bin/podkop', ['get_status']);
+ if (result && result.stdout) {
+ const status = JSON.parse(result.stdout);
+ initialPodkopStatus.enabled = status.enabled === 1;
}
+ } catch (e) {
+ console.error('Error getting initial podkop status:', e);
+ }
- return E('div', { 'class': 'cbi-section' }, [
- E('div', { 'class': 'table', style: 'display: flex; gap: 20px;' }, [
- // Podkop Status Panel
- E('div', { 'id': 'podkop-status-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [
- E('div', { 'class': 'panel-heading' }, [
- E('strong', {}, _('Podkop Status')),
- E('br'),
- E('span', { 'id': 'podkop-status-text' }, createLoadingStatusText())
- ]),
- E('div', { 'class': 'panel-body', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [
- ButtonFactory.createActionButton({
- label: 'Restart Podkop',
- type: 'apply',
- action: 'restart',
- reload: true
- }),
- ButtonFactory.createActionButton({
- label: 'Stop Podkop',
- type: 'apply',
- action: 'stop',
- reload: true
- }),
- // Autostart button - create with initial state
- ButtonFactory.createInitActionButton({
- label: initialPodkopStatus.enabled ? 'Disable Autostart' : 'Enable Autostart',
- type: initialPodkopStatus.enabled ? 'remove' : 'apply',
- action: initialPodkopStatus.enabled ? 'disable' : 'enable',
- reload: true
- }),
- ButtonFactory.createModalButton({
- label: _('Global check'),
- command: 'global_check',
- title: _('Click here for all the info')
- }),
- ButtonFactory.createModalButton({
- label: 'View Logs',
- command: 'check_logs',
- title: 'Podkop Logs'
- }),
- ButtonFactory.createModalButton({
- label: _('Update Lists'),
- command: 'list_update',
- title: _('Lists Update Results')
- })
- ])
- ]),
+ return E('div', { class: 'cbi-section' }, [
+ E('div', { class: 'table', style: 'display: flex; gap: 20px;' }, [
+ // Podkop Status Panel
+ E(
+ 'div',
+ {
+ id: 'podkop-status-panel',
+ class: 'panel',
+ style: 'flex: 1; padding: 15px;',
+ },
+ [
+ E('div', { class: 'panel-heading' }, [
+ E('strong', {}, _('Podkop Status')),
+ E('br'),
+ E('span', { id: 'podkop-status-text' }, createLoadingStatusText()),
+ ]),
+ E(
+ 'div',
+ {
+ class: 'panel-body',
+ style: 'display: flex; flex-direction: column; gap: 8px;',
+ },
+ [
+ ButtonFactory.createActionButton({
+ label: 'Restart Podkop',
+ type: 'apply',
+ action: 'restart',
+ reload: true,
+ }),
+ ButtonFactory.createActionButton({
+ label: 'Stop Podkop',
+ type: 'apply',
+ action: 'stop',
+ reload: true,
+ }),
+ // Autostart button - create with initial state
+ ButtonFactory.createInitActionButton({
+ label: initialPodkopStatus.enabled
+ ? 'Disable Autostart'
+ : 'Enable Autostart',
+ type: initialPodkopStatus.enabled ? 'remove' : 'apply',
+ action: initialPodkopStatus.enabled ? 'disable' : 'enable',
+ reload: true,
+ }),
+ ButtonFactory.createModalButton({
+ label: _('Global check'),
+ command: 'global_check',
+ title: _('Click here for all the info'),
+ }),
+ ButtonFactory.createModalButton({
+ label: 'View Logs',
+ command: 'check_logs',
+ title: 'Podkop Logs',
+ }),
+ ButtonFactory.createModalButton({
+ label: _('Update Lists'),
+ command: 'list_update',
+ title: _('Lists Update Results'),
+ }),
+ ],
+ ),
+ ],
+ ),
- // Sing-box Status Panel
- E('div', { 'id': 'singbox-status-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [
- E('div', { 'class': 'panel-heading' }, [
- E('strong', {}, _('Sing-box Status')),
- E('br'),
- E('span', { 'id': 'singbox-status-text' }, createLoadingStatusText())
- ]),
- E('div', { 'class': 'panel-body', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [
- ButtonFactory.createModalButton({
- label: 'Show Config',
- command: 'show_sing_box_config',
- title: 'Sing-box Configuration'
- }),
- ButtonFactory.createModalButton({
- label: 'View Logs',
- command: 'check_sing_box_logs',
- title: 'Sing-box Logs'
- }),
- ButtonFactory.createModalButton({
- label: 'Check Connections',
- command: 'check_sing_box_connections',
- title: 'Active Connections'
- }),
- ButtonFactory.createModalButton({
- label: _('Check NFT Rules'),
- command: 'check_nft',
- title: _('NFT Rules')
- }),
- ButtonFactory.createModalButton({
- label: _('Check DNSMasq'),
- command: 'check_dnsmasq',
- title: _('DNSMasq Configuration')
- })
- ])
- ]),
+ // Sing-box Status Panel
+ E(
+ 'div',
+ {
+ id: 'singbox-status-panel',
+ class: 'panel',
+ style: 'flex: 1; padding: 15px;',
+ },
+ [
+ E('div', { class: 'panel-heading' }, [
+ E('strong', {}, _('Sing-box Status')),
+ E('br'),
+ E('span', { id: 'singbox-status-text' }, createLoadingStatusText()),
+ ]),
+ E(
+ 'div',
+ {
+ class: 'panel-body',
+ style: 'display: flex; flex-direction: column; gap: 8px;',
+ },
+ [
+ ButtonFactory.createModalButton({
+ label: 'Show Config',
+ command: 'show_sing_box_config',
+ title: 'Sing-box Configuration',
+ }),
+ ButtonFactory.createModalButton({
+ label: 'View Logs',
+ command: 'check_sing_box_logs',
+ title: 'Sing-box Logs',
+ }),
+ ButtonFactory.createModalButton({
+ label: 'Check Connections',
+ command: 'check_sing_box_connections',
+ title: 'Active Connections',
+ }),
+ ButtonFactory.createModalButton({
+ label: _('Check NFT Rules'),
+ command: 'check_nft',
+ title: _('NFT Rules'),
+ }),
+ ButtonFactory.createModalButton({
+ label: _('Check DNSMasq'),
+ command: 'check_dnsmasq',
+ title: _('DNSMasq Configuration'),
+ }),
+ ],
+ ),
+ ],
+ ),
- // FakeIP Status Panel
- E('div', { 'id': 'fakeip-status-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [
- E('div', { 'class': 'panel-heading' }, [
- E('strong', {}, _('FakeIP Status'))
+ // FakeIP Status Panel
+ E(
+ 'div',
+ {
+ id: 'fakeip-status-panel',
+ class: 'panel',
+ style: 'flex: 1; padding: 15px;',
+ },
+ [
+ E('div', { class: 'panel-heading' }, [
+ E('strong', {}, _('FakeIP Status')),
+ ]),
+ E(
+ 'div',
+ {
+ class: 'panel-body',
+ style: 'display: flex; flex-direction: column; gap: 8px;',
+ },
+ [
+ E('div', { style: 'margin-bottom: 5px;' }, [
+ E('div', {}, [
+ E(
+ 'span',
+ { id: 'fakeip-browser-status' },
+ createLoadingStatusText(),
+ ),
]),
- E('div', { 'class': 'panel-body', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [
- E('div', { style: 'margin-bottom: 5px;' }, [
- E('div', {}, [
- E('span', { 'id': 'fakeip-browser-status' }, createLoadingStatusText())
- ]),
- E('div', {}, [
- E('span', { 'id': 'fakeip-router-status' }, createLoadingStatusText())
- ])
- ]),
- E('div', { style: 'margin-bottom: 5px;' }, [
- E('div', {}, [
- E('strong', {}, _('DNS Status')),
- E('br'),
- E('span', { 'id': 'dns-remote-status' }, createLoadingStatusText()),
- E('br'),
- E('span', { 'id': 'dns-local-status' }, createLoadingStatusText())
- ])
- ]),
- E('div', { style: 'margin-bottom: 5px;' }, [
- E('div', {}, [
- E('strong', { 'id': 'config-name-text' }, _('Main config')),
- E('br'),
- E('span', { 'id': 'bypass-status' }, createLoadingStatusText())
- ])
- ])
- ])
- ]),
+ E('div', {}, [
+ E(
+ 'span',
+ { id: 'fakeip-router-status' },
+ createLoadingStatusText(),
+ ),
+ ]),
+ ]),
+ E('div', { style: 'margin-bottom: 5px;' }, [
+ E('div', {}, [
+ E('strong', {}, _('DNS Status')),
+ E('br'),
+ E(
+ 'span',
+ { id: 'dns-remote-status' },
+ createLoadingStatusText(),
+ ),
+ E('br'),
+ E(
+ 'span',
+ { id: 'dns-local-status' },
+ createLoadingStatusText(),
+ ),
+ ]),
+ ]),
+ E('div', { style: 'margin-bottom: 5px;' }, [
+ E('div', {}, [
+ E('strong', { id: 'config-name-text' }, _('Main config')),
+ E('br'),
+ E('span', { id: 'bypass-status' }, createLoadingStatusText()),
+ ]),
+ ]),
+ ],
+ ),
+ ],
+ ),
- // Version Information Panel
- E('div', { 'id': 'version-info-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [
- E('div', { 'class': 'panel-heading' }, [
- E('strong', {}, _('Version Information'))
- ]),
- E('div', { 'class': 'panel-body' }, [
- E('div', { 'style': 'margin-top: 10px; font-family: monospace; white-space: pre-wrap;' }, [
- E('strong', {}, _('Podkop: ')), E('span', { 'id': 'podkop-version' }, _('Loading...')), '\n',
- E('strong', {}, _('LuCI App: ')), E('span', { 'id': 'luci-version' }, _('Loading...')), '\n',
- E('strong', {}, _('Sing-box: ')), E('span', { 'id': 'singbox-version' }, _('Loading...')), '\n',
- E('strong', {}, _('OpenWrt Version: ')), E('span', { 'id': 'openwrt-version' }, _('Loading...')), '\n',
- E('strong', {}, _('Device Model: ')), E('span', { 'id': 'device-model' }, _('Loading...'))
- ])
- ])
- ])
- ])
- ]);
+ // Version Information Panel
+ E(
+ 'div',
+ {
+ id: 'version-info-panel',
+ class: 'panel',
+ style: 'flex: 1; padding: 15px;',
+ },
+ [
+ E('div', { class: 'panel-heading' }, [
+ E('strong', {}, _('Version Information')),
+ ]),
+ E('div', { class: 'panel-body' }, [
+ E(
+ 'div',
+ {
+ style:
+ 'margin-top: 10px; font-family: monospace; white-space: pre-wrap;',
+ },
+ [
+ E('strong', {}, _('Podkop: ')),
+ E('span', { id: 'podkop-version' }, _('Loading...')),
+ '\n',
+ E('strong', {}, _('LuCI App: ')),
+ E('span', { id: 'luci-version' }, _('Loading...')),
+ '\n',
+ E('strong', {}, _('Sing-box: ')),
+ E('span', { id: 'singbox-version' }, _('Loading...')),
+ '\n',
+ E('strong', {}, _('OpenWrt Version: ')),
+ E('span', { id: 'openwrt-version' }, _('Loading...')),
+ '\n',
+ E('strong', {}, _('Device Model: ')),
+ E('span', { id: 'device-model' }, _('Loading...')),
+ ],
+ ),
+ ]),
+ ],
+ ),
+ ]),
+ ]);
};
// Global variables for tracking state
@@ -567,294 +784,444 @@ let isInitialCheck = true;
showConfigModal.busy = false;
function startDiagnosticsUpdates() {
- if (diagnosticsUpdateTimer) {
- clearInterval(diagnosticsUpdateTimer);
- }
+ if (diagnosticsUpdateTimer) {
+ clearInterval(diagnosticsUpdateTimer);
+ }
- // Immediately update when started
- updateDiagnostics();
+ // Immediately update when started
+ updateDiagnostics();
- // Then set up periodic updates
- diagnosticsUpdateTimer = setInterval(updateDiagnostics, main.DIAGNOSTICS_UPDATE_INTERVAL);
+ // Then set up periodic updates
+ diagnosticsUpdateTimer = setInterval(
+ updateDiagnostics,
+ main.DIAGNOSTICS_UPDATE_INTERVAL,
+ );
}
function stopDiagnosticsUpdates() {
- if (diagnosticsUpdateTimer) {
- clearInterval(diagnosticsUpdateTimer);
- diagnosticsUpdateTimer = null;
- }
+ if (diagnosticsUpdateTimer) {
+ clearInterval(diagnosticsUpdateTimer);
+ diagnosticsUpdateTimer = null;
+ }
}
// Update individual text element with new content
function updateTextElement(elementId, content) {
- const element = document.getElementById(elementId);
- if (element) {
- element.innerHTML = '';
- element.appendChild(content);
- }
+ const element = document.getElementById(elementId);
+ if (element) {
+ element.innerHTML = '';
+ element.appendChild(content);
+ }
}
async function updateDiagnostics() {
- // Podkop Status check
- safeExec('/usr/bin/podkop', ['get_status'], 'P0_PRIORITY', result => {
- try {
- const parsedPodkopStatus = JSON.parse(result.stdout || '{"enabled":0,"status":"error"}');
+ // Podkop Status check
+ safeExec('/usr/bin/podkop', ['get_status'], 'P0_PRIORITY', (result) => {
+ try {
+ const parsedPodkopStatus = JSON.parse(
+ result.stdout || '{"enabled":0,"status":"error"}',
+ );
- // Update Podkop status text
- updateTextElement('podkop-status-text',
- E('span', {
- 'style': `color: ${parsedPodkopStatus.enabled ? main.STATUS_COLORS.SUCCESS : main.STATUS_COLORS.ERROR}`
- }, [
- parsedPodkopStatus.enabled ? '✔ Autostart enabled' : '✘ Autostart disabled'
- ])
- );
+ // Update Podkop status text
+ updateTextElement(
+ 'podkop-status-text',
+ E(
+ 'span',
+ {
+ style: `color: ${parsedPodkopStatus.enabled ? main.STATUS_COLORS.SUCCESS : main.STATUS_COLORS.ERROR}`,
+ },
+ [
+ parsedPodkopStatus.enabled
+ ? '✔ Autostart enabled'
+ : '✘ Autostart disabled',
+ ],
+ ),
+ );
- // Update autostart button
- const autostartButton = parsedPodkopStatus.enabled ?
- ButtonFactory.createInitActionButton({
- label: 'Disable Autostart',
- type: 'remove',
- action: 'disable',
- reload: true
- }) :
- ButtonFactory.createInitActionButton({
- label: 'Enable Autostart',
- type: 'apply',
- action: 'enable',
- reload: true
- });
+ // Update autostart button
+ const autostartButton = parsedPodkopStatus.enabled
+ ? ButtonFactory.createInitActionButton({
+ label: 'Disable Autostart',
+ type: 'remove',
+ action: 'disable',
+ reload: true,
+ })
+ : ButtonFactory.createInitActionButton({
+ label: 'Enable Autostart',
+ type: 'apply',
+ action: 'enable',
+ reload: true,
+ });
- // Find the autostart button and replace it
- const panel = document.getElementById('podkop-status-panel');
- if (panel) {
- const buttons = panel.querySelectorAll('.cbi-button');
- if (buttons.length >= 3) {
- buttons[2].parentNode.replaceChild(autostartButton, buttons[2]);
- }
+ // Find the autostart button and replace it
+ const panel = document.getElementById('podkop-status-panel');
+ if (panel) {
+ const buttons = panel.querySelectorAll('.cbi-button');
+ if (buttons.length >= 3) {
+ buttons[2].parentNode.replaceChild(autostartButton, buttons[2]);
+ }
+ }
+ } catch (error) {
+ updateTextElement(
+ 'podkop-status-text',
+ E('span', { style: `color: ${main.STATUS_COLORS.ERROR}` }, '✘ Error'),
+ );
+ }
+ });
+
+ // Sing-box Status check
+ safeExec(
+ '/usr/bin/podkop',
+ ['get_sing_box_status'],
+ 'P0_PRIORITY',
+ (result) => {
+ try {
+ const parsedSingboxStatus = JSON.parse(
+ result.stdout || '{"running":0,"enabled":0,"status":"error"}',
+ );
+
+ // Update Sing-box status text
+ updateTextElement(
+ 'singbox-status-text',
+ E(
+ 'span',
+ {
+ style: `color: ${
+ parsedSingboxStatus.running && !parsedSingboxStatus.enabled
+ ? main.STATUS_COLORS.SUCCESS
+ : main.STATUS_COLORS.ERROR
+ }`,
+ },
+ [
+ parsedSingboxStatus.running && !parsedSingboxStatus.enabled
+ ? '✔ running'
+ : '✘ ' + parsedSingboxStatus.status,
+ ],
+ ),
+ );
+ } catch (error) {
+ updateTextElement(
+ 'singbox-status-text',
+ E('span', { style: `color: ${main.STATUS_COLORS.ERROR}` }, '✘ Error'),
+ );
+ }
+ },
+ );
+
+ // Version Information checks
+ safeExec('/usr/bin/podkop', ['show_version'], 'P2_PRIORITY', (result) => {
+ updateTextElement(
+ 'podkop-version',
+ document.createTextNode(
+ result.stdout ? result.stdout.trim() : _('Unknown'),
+ ),
+ );
+ });
+
+ safeExec(
+ '/usr/bin/podkop',
+ ['show_luci_version'],
+ 'P2_PRIORITY',
+ (result) => {
+ updateTextElement(
+ 'luci-version',
+ document.createTextNode(
+ result.stdout ? result.stdout.trim() : _('Unknown'),
+ ),
+ );
+ },
+ );
+
+ safeExec(
+ '/usr/bin/podkop',
+ ['show_sing_box_version'],
+ 'P2_PRIORITY',
+ (result) => {
+ updateTextElement(
+ 'singbox-version',
+ document.createTextNode(
+ result.stdout ? result.stdout.trim() : _('Unknown'),
+ ),
+ );
+ },
+ );
+
+ safeExec('/usr/bin/podkop', ['show_system_info'], 'P2_PRIORITY', (result) => {
+ if (result.stdout) {
+ updateTextElement(
+ 'openwrt-version',
+ document.createTextNode(result.stdout.split('\n')[1].trim()),
+ );
+ updateTextElement(
+ 'device-model',
+ document.createTextNode(result.stdout.split('\n')[4].trim()),
+ );
+ } else {
+ updateTextElement(
+ 'openwrt-version',
+ document.createTextNode(_('Unknown')),
+ );
+ updateTextElement('device-model', document.createTextNode(_('Unknown')));
+ }
+ });
+
+ // FakeIP and DNS status checks
+ runCheck(checkFakeIP, 'P3_PRIORITY', (result) => {
+ updateTextElement(
+ 'fakeip-browser-status',
+ E(
+ 'span',
+ {
+ style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}`,
+ },
+ [
+ result.error
+ ? '! '
+ : result.state === 'working'
+ ? '✔ '
+ : result.state === 'not_working'
+ ? '✘ '
+ : '! ',
+ result.error
+ ? 'check error'
+ : result.state === 'working'
+ ? _('works in browser')
+ : _('does not work in browser'),
+ ],
+ ),
+ );
+ });
+
+ runCheck(checkFakeIPCLI, 'P8_PRIORITY', (result) => {
+ updateTextElement(
+ 'fakeip-router-status',
+ E(
+ 'span',
+ {
+ style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}`,
+ },
+ [
+ result.error
+ ? '! '
+ : result.state === 'working'
+ ? '✔ '
+ : result.state === 'not_working'
+ ? '✘ '
+ : '! ',
+ result.error
+ ? 'check error'
+ : result.state === 'working'
+ ? _('works on router')
+ : _('does not work on router'),
+ ],
+ ),
+ );
+ });
+
+ runCheck(checkDNSAvailability, 'P4_PRIORITY', (result) => {
+ if (result.error) {
+ updateTextElement(
+ 'dns-remote-status',
+ E(
+ 'span',
+ { style: `color: ${main.STATUS_COLORS.WARNING}` },
+ '! DNS check error',
+ ),
+ );
+ updateTextElement(
+ 'dns-local-status',
+ E(
+ 'span',
+ { style: `color: ${main.STATUS_COLORS.WARNING}` },
+ '! DNS check error',
+ ),
+ );
+ } else {
+ updateTextElement(
+ 'dns-remote-status',
+ E('span', { style: `color: ${result.remote.color}` }, [
+ result.remote.state === 'available'
+ ? '✔ '
+ : result.remote.state === 'unavailable'
+ ? '✘ '
+ : '! ',
+ result.remote.message,
+ ]),
+ );
+
+ updateTextElement(
+ 'dns-local-status',
+ E('span', { style: `color: ${result.local.color}` }, [
+ result.local.state === 'available'
+ ? '✔ '
+ : result.local.state === 'unavailable'
+ ? '✘ '
+ : '! ',
+ result.local.message,
+ ]),
+ );
+ }
+ });
+
+ runCheck(
+ checkBypass,
+ 'P1_PRIORITY',
+ (result) => {
+ updateTextElement(
+ 'bypass-status',
+ E(
+ 'span',
+ {
+ style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}`,
+ },
+ [
+ result.error
+ ? '! '
+ : result.state === 'working'
+ ? '✔ '
+ : result.state === 'not_working'
+ ? '✘ '
+ : '! ',
+ result.error ? 'check error' : result.message,
+ ],
+ ),
+ );
+ },
+ 'P1_PRIORITY',
+ );
+
+ // Config name
+ runAsyncTask(async () => {
+ try {
+ let configName = _('Main config');
+ const data = await uci.load('podkop');
+ const proxyString = uci.get('podkop', 'main', 'proxy_string');
+
+ if (proxyString) {
+ const activeConfig = proxyString
+ .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()) {
+ configName = _('Config: ') + decodeURIComponent(label);
}
- } catch (error) {
- updateTextElement('podkop-status-text',
- E('span', { 'style': `color: ${main.STATUS_COLORS.ERROR}` }, '✘ Error')
- );
+ }
}
- });
+ }
- // Sing-box Status check
- safeExec('/usr/bin/podkop', ['get_sing_box_status'], 'P0_PRIORITY', result => {
- try {
- const parsedSingboxStatus = JSON.parse(result.stdout || '{"running":0,"enabled":0,"status":"error"}');
-
- // Update Sing-box status text
- updateTextElement('singbox-status-text',
- E('span', {
- 'style': `color: ${parsedSingboxStatus.running && !parsedSingboxStatus.enabled ?
- main.STATUS_COLORS.SUCCESS : main.STATUS_COLORS.ERROR}`
- }, [
- parsedSingboxStatus.running && !parsedSingboxStatus.enabled ?
- '✔ running' : '✘ ' + parsedSingboxStatus.status
- ])
- );
- } catch (error) {
- updateTextElement('singbox-status-text',
- E('span', { 'style': `color: ${main.STATUS_COLORS.ERROR}` }, '✘ Error')
- );
- }
- });
-
- // Version Information checks
- safeExec('/usr/bin/podkop', ['show_version'], 'P2_PRIORITY', result => {
- updateTextElement('podkop-version',
- document.createTextNode(result.stdout ? result.stdout.trim() : _('Unknown'))
- );
- });
-
- safeExec('/usr/bin/podkop', ['show_luci_version'], 'P2_PRIORITY', result => {
- updateTextElement('luci-version',
- document.createTextNode(result.stdout ? result.stdout.trim() : _('Unknown'))
- );
- });
-
- safeExec('/usr/bin/podkop', ['show_sing_box_version'], 'P2_PRIORITY', result => {
- updateTextElement('singbox-version',
- document.createTextNode(result.stdout ? result.stdout.trim() : _('Unknown'))
- );
- });
-
- safeExec('/usr/bin/podkop', ['show_system_info'], 'P2_PRIORITY', result => {
- if (result.stdout) {
- updateTextElement('openwrt-version',
- document.createTextNode(result.stdout.split('\n')[1].trim())
- );
- updateTextElement('device-model',
- document.createTextNode(result.stdout.split('\n')[4].trim())
- );
- } else {
- updateTextElement('openwrt-version', document.createTextNode(_('Unknown')));
- updateTextElement('device-model', document.createTextNode(_('Unknown')));
- }
- });
-
- // FakeIP and DNS status checks
- runCheck(checkFakeIP, 'P3_PRIORITY', result => {
- updateTextElement('fakeip-browser-status',
- E('span', { style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}` }, [
- result.error ? '! ' : result.state === 'working' ? '✔ ' : result.state === 'not_working' ? '✘ ' : '! ',
- result.error ? 'check error' : result.state === 'working' ? _('works in browser') : _('does not work in browser')
- ])
- );
- });
-
- runCheck(checkFakeIPCLI, 'P8_PRIORITY', result => {
- updateTextElement('fakeip-router-status',
- E('span', { style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}` }, [
- result.error ? '! ' : result.state === 'working' ? '✔ ' : result.state === 'not_working' ? '✘ ' : '! ',
- result.error ? 'check error' : result.state === 'working' ? _('works on router') : _('does not work on router')
- ])
- );
- });
-
- runCheck(checkDNSAvailability, 'P4_PRIORITY', result => {
- if (result.error) {
- updateTextElement('dns-remote-status',
- E('span', { style: `color: ${main.STATUS_COLORS.WARNING}` }, '! DNS check error')
- );
- updateTextElement('dns-local-status',
- E('span', { style: `color: ${main.STATUS_COLORS.WARNING}` }, '! DNS check error')
- );
- } else {
- updateTextElement('dns-remote-status',
- E('span', { style: `color: ${result.remote.color}` }, [
- result.remote.state === 'available' ? '✔ ' : result.remote.state === 'unavailable' ? '✘ ' : '! ',
- result.remote.message
- ])
- );
-
- updateTextElement('dns-local-status',
- E('span', { style: `color: ${result.local.color}` }, [
- result.local.state === 'available' ? '✔ ' : result.local.state === 'unavailable' ? '✘ ' : '! ',
- result.local.message
- ])
- );
- }
- });
-
- runCheck(checkBypass, 'P1_PRIORITY', result => {
- updateTextElement('bypass-status',
- E('span', { style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}` }, [
- result.error ? '! ' : result.state === 'working' ? '✔ ' : result.state === 'not_working' ? '✘ ' : '! ',
- result.error ? 'check error' : result.message
- ])
- );
- }, 'P1_PRIORITY');
-
- // Config name
- runAsyncTask(async () => {
- try {
- let configName = _('Main config');
- const data = await uci.load('podkop');
- const proxyString = uci.get('podkop', 'main', 'proxy_string');
-
- if (proxyString) {
- const activeConfig = proxyString.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()) {
- configName = _('Config: ') + decodeURIComponent(label);
- }
- }
- }
- }
-
- updateTextElement('config-name-text', document.createTextNode(configName));
- } catch (e) {
- console.error('Error getting config name from UCI:', e);
- }
- }, 'P1_PRIORITY');
+ updateTextElement(
+ 'config-name-text',
+ document.createTextNode(configName),
+ );
+ } catch (e) {
+ console.error('Error getting config name from UCI:', e);
+ }
+ }, 'P1_PRIORITY');
}
function createDiagnosticsSection(mainSection) {
- let o = mainSection.tab('diagnostics', _('Diagnostics'));
+ let o = mainSection.tab('diagnostics', _('Diagnostics'));
- o = mainSection.taboption('diagnostics', form.DummyValue, '_status');
- o.rawhtml = true;
- o.cfgvalue = () => E('div', {
- id: 'diagnostics-status',
- 'data-loading': 'true'
+ o = mainSection.taboption('diagnostics', form.DummyValue, '_status');
+ o.rawhtml = true;
+ o.cfgvalue = () =>
+ E('div', {
+ id: 'diagnostics-status',
+ 'data-loading': 'true',
});
}
function setupDiagnosticsEventHandlers(node) {
- const titleDiv = E('h2', { 'class': 'cbi-map-title' }, _('Podkop'));
- node.insertBefore(titleDiv, node.firstChild);
+ const titleDiv = E('h2', { class: 'cbi-map-title' }, _('Podkop'));
+ node.insertBefore(titleDiv, node.firstChild);
- // Function to initialize diagnostics
- function initDiagnostics(container) {
- if (container && container.hasAttribute('data-loading')) {
- container.innerHTML = '';
- showConfigModal.busy = false;
- createStatusSection().then(section => {
- container.appendChild(section);
- startDiagnosticsUpdates();
- // Start error polling when diagnostics tab is active
- utils.startErrorPolling();
- });
- }
+ // Function to initialize diagnostics
+ function initDiagnostics(container) {
+ if (container && container.hasAttribute('data-loading')) {
+ container.innerHTML = '';
+ showConfigModal.busy = false;
+ createStatusSection().then((section) => {
+ container.appendChild(section);
+ startDiagnosticsUpdates();
+ // Start error polling when diagnostics tab is active
+ utils.startErrorPolling();
+ });
+ }
+ }
+
+ document.addEventListener('visibilitychange', function () {
+ const diagnosticsContainer = document.getElementById('diagnostics-status');
+ const diagnosticsTab = document.querySelector(
+ '.cbi-tab[data-tab="diagnostics"]',
+ );
+
+ if (
+ document.hidden ||
+ !diagnosticsTab ||
+ !diagnosticsTab.classList.contains('cbi-tab-active')
+ ) {
+ stopDiagnosticsUpdates();
+ // Don't stop error polling here - it's managed in podkop.js for all tabs
+ } else if (
+ diagnosticsContainer &&
+ diagnosticsContainer.hasAttribute('data-loading')
+ ) {
+ startDiagnosticsUpdates();
+ // Ensure error polling is running when diagnostics tab is active
+ utils.startErrorPolling();
+ }
+ });
+
+ setTimeout(() => {
+ const diagnosticsContainer = document.getElementById('diagnostics-status');
+ const diagnosticsTab = document.querySelector(
+ '.cbi-tab[data-tab="diagnostics"]',
+ );
+ const otherTabs = document.querySelectorAll(
+ '.cbi-tab:not([data-tab="diagnostics"])',
+ );
+
+ // Check for direct page load case
+ const noActiveTabsExist = !Array.from(otherTabs).some((tab) =>
+ tab.classList.contains('cbi-tab-active'),
+ );
+
+ if (
+ diagnosticsContainer &&
+ diagnosticsTab &&
+ (diagnosticsTab.classList.contains('cbi-tab-active') || noActiveTabsExist)
+ ) {
+ initDiagnostics(diagnosticsContainer);
}
- document.addEventListener('visibilitychange', function () {
- const diagnosticsContainer = document.getElementById('diagnostics-status');
- const diagnosticsTab = document.querySelector('.cbi-tab[data-tab="diagnostics"]');
-
- if (document.hidden || !diagnosticsTab || !diagnosticsTab.classList.contains('cbi-tab-active')) {
+ const tabs = node.querySelectorAll('.cbi-tabmenu');
+ if (tabs.length > 0) {
+ tabs[0].addEventListener('click', function (e) {
+ const tab = e.target.closest('.cbi-tab');
+ if (tab) {
+ const tabName = tab.getAttribute('data-tab');
+ if (tabName === 'diagnostics') {
+ const container = document.getElementById('diagnostics-status');
+ container.setAttribute('data-loading', 'true');
+ initDiagnostics(container);
+ } else {
stopDiagnosticsUpdates();
- // Don't stop error polling here - it's managed in podkop.js for all tabs
- } else if (diagnosticsContainer && diagnosticsContainer.hasAttribute('data-loading')) {
- startDiagnosticsUpdates();
- // Ensure error polling is running when diagnostics tab is active
- utils.startErrorPolling();
+ // Don't stop error polling - it should continue on all tabs
+ }
}
- });
+ });
+ }
+ }, main.DIAGNOSTICS_INITIAL_DELAY);
- setTimeout(() => {
- const diagnosticsContainer = document.getElementById('diagnostics-status');
- const diagnosticsTab = document.querySelector('.cbi-tab[data-tab="diagnostics"]');
- const otherTabs = document.querySelectorAll('.cbi-tab:not([data-tab="diagnostics"])');
-
- // Check for direct page load case
- const noActiveTabsExist = !Array.from(otherTabs).some(tab => tab.classList.contains('cbi-tab-active'));
-
- if (diagnosticsContainer && diagnosticsTab && (diagnosticsTab.classList.contains('cbi-tab-active') || noActiveTabsExist)) {
- initDiagnostics(diagnosticsContainer);
- }
-
- const tabs = node.querySelectorAll('.cbi-tabmenu');
- if (tabs.length > 0) {
- tabs[0].addEventListener('click', function (e) {
- const tab = e.target.closest('.cbi-tab');
- if (tab) {
- const tabName = tab.getAttribute('data-tab');
- if (tabName === 'diagnostics') {
- const container = document.getElementById('diagnostics-status');
- container.setAttribute('data-loading', 'true');
- initDiagnostics(container);
- } else {
- stopDiagnosticsUpdates();
- // Don't stop error polling - it should continue on all tabs
- }
- }
- });
- }
- }, main.DIAGNOSTICS_INITIAL_DELAY);
-
- node.classList.add('fade-in');
- return node;
+ node.classList.add('fade-in');
+ return node;
}
return baseclass.extend({
- createDiagnosticsSection,
- setupDiagnosticsEventHandlers
+ createDiagnosticsSection,
+ setupDiagnosticsEventHandlers,
});
diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/utils.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/utils.js
index 46cb086..f358670 100644
--- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/utils.js
+++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/utils.js
@@ -15,138 +15,149 @@ 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([]);
+ 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]')
- );
+ const logs = result.stdout.split('\n');
+ const errors = logs.filter((log) => log.includes('[critical]'));
- resolve(errors);
- });
+ 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)
- ]);
+ const notificationContent = E('div', { class: 'alert-message error' }, [
+ E('pre', { class: 'error-log' }, error),
+ ]);
- ui.addNotification(null, notificationContent);
+ 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;
+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];
+ // 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;
}
+ };
- 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();
- }
+ 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();
+ 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 (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));
- }
+ 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);
+ });
}
- // After first check, mark as no longer initial
- isInitialCheck = false;
- } catch (error) {
- console.error('Error checking for critical messages:', error);
+ // 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);
- }
+ if (errorPollTimer) {
+ clearInterval(errorPollTimer);
+ }
- // Reset initial check flag to make sure we show errors
- isInitialCheck = false;
+ // Reset initial check flag to make sure we show errors
+ isInitialCheck = false;
- // Immediately check for errors on start
- checkForCriticalErrors();
+ // Immediately check for errors on start
+ checkForCriticalErrors();
- // Then set up periodic checks
- errorPollTimer = setInterval(checkForCriticalErrors, main.ERROR_POLL_INTERVAL);
+ // Then set up periodic checks
+ errorPollTimer = setInterval(
+ checkForCriticalErrors,
+ main.ERROR_POLL_INTERVAL,
+ );
}
// Stop polling for errors
function stopErrorPolling() {
- if (errorPollTimer) {
- clearInterval(errorPollTimer);
- errorPollTimer = null;
- }
+ if (errorPollTimer) {
+ clearInterval(errorPollTimer);
+ errorPollTimer = null;
+ }
}
return baseclass.extend({
- startErrorPolling,
- stopErrorPolling,
- checkForCriticalErrors,
- safeExec
+ startErrorPolling,
+ stopErrorPolling,
+ checkForCriticalErrors,
+ safeExec,
});