diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalSection.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalSection.js
new file mode 100644
index 0000000..5638d47
--- /dev/null
+++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalSection.js
@@ -0,0 +1,181 @@
+'use strict';
+'require form';
+'require baseclass';
+'require view.podkop.constants as constants';
+'require view.podkop.networkUtils as networkUtils';
+
+function createAdditionalSection(mainSection, section, network) {
+ let o = mainSection.tab('additional', _('Additional Settings'));
+
+ o = mainSection.taboption('additional', form.Flag, 'yacd', _('Yacd enable'), _('openwrt.lan:9090/ui'));
+ o.default = '0';
+ o.rmempty = false;
+ o.ucisection = 'main';
+
+ o = mainSection.taboption('additional', form.Flag, 'exclude_ntp', _('Exclude NTP'), _('For issues with open connections sing-box'));
+ 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(constants.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 = 'doh';
+ o.rmempty = false;
+ o.ucisection = 'main';
+
+ o = mainSection.taboption('additional', form.Value, 'dns_server', _('DNS Server'), _('Select or enter DNS server address'));
+ Object.entries(constants.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) {
+ if (!value) {
+ return _('DNS server address cannot be empty');
+ }
+
+ const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
+ if (ipRegex.test(value)) {
+ const parts = value.split('.');
+ for (const part of parts) {
+ const num = parseInt(part);
+ if (num < 0 || num > 255) {
+ return _('IP address parts must be between 0 and 255');
+ }
+ }
+ return true;
+ }
+
+ const domainRegex = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(\/[^\s]*)?$/;
+ if (!domainRegex.test(value)) {
+ return _('Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH');
+ }
+
+ return true;
+ };
+
+ o = mainSection.taboption('additional', form.Value, 'dns_rewrite_ttl', _('DNS Rewrite TTL'), _('Time in seconds for DNS record caching (default: 60)'));
+ o.default = '60';
+ o.rmempty = false;
+ o.ucisection = 'main';
+ o.validate = function (section_id, value) {
+ if (!value) {
+ return _('TTL value cannot be empty');
+ }
+
+ const ttl = parseInt(value);
+ if (isNaN(ttl) || ttl < 0) {
+ return _('TTL must be a positive number');
+ }
+
+ return true;
+ };
+
+ o = mainSection.taboption('additional', form.Value, 'cache_file', _('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/cache.db', 'RAM (/tmp/cache.db)');
+ o.value('/usr/share/sing-box/cache.db', 'Flash (/usr/share/sing-box/cache.db)');
+ o.default = '/tmp/cache.db';
+ o.rmempty = false;
+ o.ucisection = 'main';
+ o.validate = function (section_id, value) {
+ if (!value) {
+ return _('Cache file path cannot be empty');
+ }
+
+ if (!value.startsWith('/')) {
+ return _('Path must be absolute (start with /)');
+ }
+
+ if (!value.endsWith('cache.db')) {
+ return _('Path must end with cache.db');
+ }
+
+ const parts = value.split('/').filter(Boolean);
+ if (parts.length < 2) {
+ return _('Path must contain at least one directory (like /tmp/cache.db)');
+ }
+
+ return true;
+ };
+
+ o = mainSection.taboption('additional', form.MultiValue, 'iface', _('Source Network Interface'), _('Select the network interface from which the traffic will originate'));
+ o.ucisection = 'main';
+ o.default = 'br-lan';
+ o.load = function (section_id) {
+ return networkUtils.getNetworkInterfaces(this, section_id, ['wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan']).then(() => {
+ return this.super('load', section_id);
+ });
+ };
+
+ 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.MultiValue, 'restart_ifaces', _('Interface for monitoring'), _('Select the WAN interfaces to be monitored'));
+ o.ucisection = 'main';
+ o.depends('mon_restart_ifaces', '1');
+ o.load = function (section_id) {
+ return networkUtils.getNetworkNetworks(this, section_id, ['lan', 'loopback']).then(() => {
+ return this.super('load', section_id);
+ });
+ };
+
+ o = mainSection.taboption('additional', form.Flag, 'dont_touch_dhcp', _('Dont touch my DHCP!'), _('Podkop will not change the DHCP config'));
+ o.default = '0';
+ o.rmempty = false;
+ o.ucisection = 'main';
+
+ o = mainSection.taboption('additional', form.Flag, 'detour', _('Proxy download of lists'), _('Downloading all lists via main Proxy/VPN'));
+ o.default = '0';
+ o.rmempty = false;
+ o.ucisection = 'main';
+
+ // Extra IPs and exclusions (main section)
+ o = mainSection.taboption('basic', form.Flag, 'exclude_from_ip_enabled', _('IP for exclusion'), _('Specify local IP addresses that will never use the configured route'));
+ o.default = '0';
+ o.rmempty = false;
+ o.ucisection = 'main';
+
+ o = mainSection.taboption('basic', form.DynamicList, 'exclude_traffic_ip', _('Local IPs'), _('Enter valid IPv4 addresses'));
+ o.placeholder = 'IP';
+ o.depends('exclude_from_ip_enabled', '1');
+ o.rmempty = false;
+ o.ucisection = 'main';
+ o.validate = function (section_id, value) {
+ if (!value || value.length === 0) return true;
+ const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
+ if (!ipRegex.test(value)) return _('Invalid IP format. Use format: X.X.X.X (like 192.168.1.1)');
+ const ipParts = value.split('.');
+ for (const part of ipParts) {
+ const num = parseInt(part);
+ if (num < 0 || num > 255) return _('IP address parts must be between 0 and 255');
+ }
+ return true;
+ };
+
+ 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
+});
\ No newline at end of file
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
new file mode 100644
index 0000000..c239f10
--- /dev/null
+++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js
@@ -0,0 +1,507 @@
+'use strict';
+'require baseclass';
+'require form';
+'require ui';
+'require network';
+'require view.podkop.constants as constants';
+'require view.podkop.networkUtils as networkUtils';
+
+function createConfigSection(section, map, network) {
+ const s = section;
+
+ let o = s.tab('basic', _('Basic Settings'));
+
+ o = s.taboption('basic', form.ListValue, 'mode', _('Connection Type'), _('Select between VPN and Proxy connection methods for traffic routing'));
+ o.value('proxy', ('Proxy'));
+ o.value('vpn', ('VPN'));
+ o.value('block', ('Block'));
+ o.ucisection = s.section;
+
+ o = s.taboption('basic', form.ListValue, 'proxy_config_type', _('Configuration Type'), _('Select how to configure the proxy'));
+ o.value('url', _('Connection URL'));
+ o.value('outbound', _('Outbound Config'));
+ o.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.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';
+
+ o.renderWidget = function (section_id, option_index, cfgvalue) {
+ const original = form.TextValue.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]);
+ const container = E('div', {});
+ container.appendChild(original);
+
+ if (cfgvalue) {
+ try {
+ const activeConfig = cfgvalue.split('\n')
+ .map(line => line.trim())
+ .find(line => line && !line.startsWith('//'));
+
+ if (activeConfig) {
+ if (activeConfig.includes('#')) {
+ const label = activeConfig.split('#').pop();
+ if (label && label.trim()) {
+ const decodedLabel = decodeURIComponent(label);
+ const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Current config: ') + decodedLabel);
+ container.appendChild(descDiv);
+ } else {
+ const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description'));
+ container.appendChild(descDiv);
+ }
+ } else {
+ const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description'));
+ container.appendChild(descDiv);
+ }
+ }
+ } catch (e) {
+ console.error('Error parsing config label:', e);
+ const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description'));
+ container.appendChild(descDiv);
+ }
+ } else {
+ const defaultDesc = E('div', { 'class': 'cbi-value-description' },
+ _('Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs'));
+ container.appendChild(defaultDesc);
+ }
+
+ return container;
+ };
+
+ o.validate = function (section_id, value) {
+ 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.');
+ }
+
+ if (!activeConfig.startsWith('vless://') && !activeConfig.startsWith('ss://')) {
+ return _('URL must start with vless:// or ss://');
+ }
+
+ if (activeConfig.startsWith('ss://')) {
+ let encrypted_part;
+ try {
+ let mainPart = activeConfig.includes('?') ? activeConfig.split('?')[0] : activeConfig.split('#')[0];
+ encrypted_part = mainPart.split('/')[2].split('@')[0];
+ try {
+ let decoded = atob(encrypted_part);
+ if (!decoded.includes(':')) {
+ if (!encrypted_part.includes(':') && !encrypted_part.includes('-')) {
+ return _('Invalid Shadowsocks URL format: missing method and password separator ":"');
+ }
+ }
+ } catch (e) {
+ if (!encrypted_part.includes(':') && !encrypted_part.includes('-')) {
+ return _('Invalid Shadowsocks URL format: missing method and password separator ":"');
+ }
+ }
+ } catch (e) {
+ return _('Invalid Shadowsocks URL format');
+ }
+
+ try {
+ let serverPart = activeConfig.split('@')[1];
+ if (!serverPart) return _('Invalid Shadowsocks URL: missing server address');
+ let [server, portAndRest] = serverPart.split(':');
+ if (!server) return _('Invalid Shadowsocks URL: missing server');
+ let port = portAndRest ? portAndRest.split(/[?#]/)[0] : null;
+ if (!port) return _('Invalid Shadowsocks URL: missing port');
+ let portNum = parseInt(port);
+ if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
+ return _('Invalid port number. Must be between 1 and 65535');
+ }
+ } catch (e) {
+ return _('Invalid Shadowsocks URL: missing or invalid server/port format');
+ }
+ }
+
+ if (activeConfig.startsWith('vless://')) {
+ let uuid = activeConfig.split('/')[2].split('@')[0];
+ if (!uuid || uuid.length === 0) return _('Invalid VLESS URL: missing UUID');
+
+ try {
+ let serverPart = activeConfig.split('@')[1];
+ if (!serverPart) return _('Invalid VLESS URL: missing server address');
+ let [server, portAndRest] = serverPart.split(':');
+ if (!server) return _('Invalid VLESS URL: missing server');
+ let port = portAndRest ? portAndRest.split(/[/?#]/)[0] : null;
+ if (!port) return _('Invalid VLESS URL: missing port');
+ let portNum = parseInt(port);
+ if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
+ return _('Invalid port number. Must be between 1 and 65535');
+ }
+ } catch (e) {
+ return _('Invalid VLESS URL: missing or invalid server/port format');
+ }
+
+ let queryString = activeConfig.split('?')[1];
+ if (!queryString) return _('Invalid VLESS URL: missing query parameters');
+
+ let params = new URLSearchParams(queryString.split('#')[0]);
+ let type = params.get('type');
+ const validTypes = ['tcp', 'raw', 'udp', 'grpc', 'http', 'ws'];
+ if (!type || !validTypes.includes(type)) {
+ return _('Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws');
+ }
+
+ let security = params.get('security');
+ const validSecurities = ['tls', 'reality', 'none'];
+ if (!security || !validSecurities.includes(security)) {
+ return _('Invalid VLESS URL: security must be one of tls, reality, none');
+ }
+
+ if (security === 'reality') {
+ if (!params.get('pbk')) return _('Invalid VLESS URL: missing pbk parameter for reality security');
+ if (!params.get('fp')) return _('Invalid VLESS URL: missing fp parameter for reality security');
+ }
+
+ if (security === 'tls' && type !== 'tcp' && !params.get('sni')) {
+ return _('Invalid VLESS URL: missing sni parameter for tls security');
+ }
+ }
+
+ return true;
+ } catch (e) {
+ console.error('Validation error:', 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) {
+ if (!value || value.length === 0) return true;
+ try {
+ const parsed = JSON.parse(value);
+ if (!parsed.type || !parsed.server || !parsed.server_port) {
+ return _('JSON must contain at least type, server and server_port fields');
+ }
+ return true;
+ } catch (e) {
+ return _('Invalid JSON format');
+ }
+ };
+
+ 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 = 'main';
+
+ o = s.taboption('basic', form.ListValue, 'interface', _('Network Interface'), _('Select network interface for VPN connection'));
+ o.depends('mode', 'vpn');
+ o.ucisection = s.section;
+ o.load = function (section_id) {
+ return networkUtils.getNetworkInterfaces(this, section_id, ['br-lan', 'eth0', 'eth1', 'wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan', 'lan']).then(() => {
+ return this.super('load', section_id);
+ });
+ };
+
+ o = s.taboption('basic', form.Flag, 'domain_list_enabled', _('Community Lists'));
+ o.default = '0';
+ o.rmempty = false;
+ o.ucisection = s.section;
+
+ o = s.taboption('basic', form.DynamicList, 'domain_list', _('Service List'), _('Select predefined service for routing') + ' github.com/itdoginfo/allow-domains');
+ o.placeholder = 'Service list';
+ Object.entries(constants.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => {
+ o.value(key, _(label));
+ });
+
+ o.depends('domain_list_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 = constants.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 || !constants.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 => !constants.ALLOWED_WITH_RUSSIA_INSIDE.includes(v));
+ if (removedServices.length > 0) {
+ newValues = newValues.filter(v => constants.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(
+ constants.ALLOWED_WITH_RUSSIA_INSIDE.map(key => constants.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, 'custom_domains_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, 'custom_domains', _('User Domains'), _('Enter domain names without protocols (example: sub.example.com or example.com)'));
+ o.placeholder = 'Domains list';
+ o.depends('custom_domains_list_type', 'dynamic');
+ o.rmempty = false;
+ o.ucisection = s.section;
+ o.validate = function (section_id, value) {
+ if (!value || value.length === 0) return true;
+ const domainRegex = /^(?!-)[A-Za-z0-9-]+([-.][A-Za-z0-9-]+)*(\.[A-Za-z]{2,})?$/;
+ if (!domainRegex.test(value)) {
+ return _('Invalid domain format. Enter domain without protocol (example: sub.example.com or ru)');
+ }
+ return true;
+ };
+
+ o = s.taboption('basic', form.TextValue, 'custom_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('custom_domains_list_type', 'text');
+ o.rows = 8;
+ o.rmempty = false;
+ o.ucisection = s.section;
+ o.validate = function (section_id, value) {
+ if (!value || value.length === 0) return true;
+
+ const domainRegex = /^(?!-)[A-Za-z0-9-]+([-.][A-Za-z0-9-]+)*(\.[A-Za-z]{2,})?$/;
+ const lines = value.split(/\n/).map(line => line.trim());
+ let hasValidDomain = false;
+
+ for (const line of lines) {
+ // Skip empty lines
+ if (!line) continue;
+
+ // Extract domain part (before any //)
+ const domainPart = line.split('//')[0].trim();
+
+ // Skip if line is empty after removing comments
+ if (!domainPart) continue;
+
+ // Process each domain in the line (separated by comma or space)
+ const domains = domainPart.split(/[,\s]+/).map(d => d.trim()).filter(d => d.length > 0);
+
+ for (const domain of domains) {
+ if (!domainRegex.test(domain)) {
+ return _('Invalid domain format: %s. Enter domain without protocol').format(domain);
+ }
+ hasValidDomain = true;
+ }
+ }
+
+ if (!hasValidDomain) {
+ return _('At least one valid domain must be specified. Comments-only content is not allowed.');
+ }
+
+ return true;
+ };
+
+ o = s.taboption('basic', form.Flag, 'custom_local_domains_list_enabled', _('Local Domain Lists'), _('Use the list from the router filesystem'));
+ o.default = '0';
+ o.rmempty = false;
+ o.ucisection = s.section;
+
+ o = s.taboption('basic', form.DynamicList, 'custom_local_domains', _('Local Domain Lists Path'), _('Enter the list file path'));
+ o.placeholder = '/path/file.lst';
+ o.depends('custom_local_domains_list_enabled', '1');
+ o.rmempty = false;
+ o.ucisection = s.section;
+ o.validate = function (section_id, value) {
+ if (!value || value.length === 0) return true;
+ const pathRegex = /^\/[a-zA-Z0-9_\-\/\.]+$/;
+ if (!pathRegex.test(value)) {
+ return _('Invalid path format. Path must start with "/" and contain valid characters');
+ }
+ return true;
+ };
+
+ o = s.taboption('basic', form.Flag, 'custom_download_domains_list_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, 'custom_download_domains', _('Remote Domain URLs'), _('Enter full URLs starting with http:// or https://'));
+ o.placeholder = 'URL';
+ o.depends('custom_download_domains_list_enabled', '1');
+ o.rmempty = false;
+ o.ucisection = s.section;
+ o.validate = function (section_id, value) {
+ if (!value || value.length === 0) return true;
+ return networkUtils.validateUrl(value);
+ };
+
+ o = s.taboption('basic', form.ListValue, 'custom_subnets_list_enabled', _('User Subnet List Type'), _('Select how to add your custom subnets'));
+ o.value('disabled', _('Disabled'));
+ o.value('dynamic', _('Dynamic List'));
+ o.value('text', _('Text List (comma/space/newline separated)'));
+ o.default = 'disabled';
+ o.rmempty = false;
+ o.ucisection = s.section;
+
+ o = s.taboption('basic', form.DynamicList, 'custom_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('custom_subnets_list_enabled', 'dynamic');
+ o.rmempty = false;
+ o.ucisection = s.section;
+ o.validate = function (section_id, value) {
+ if (!value || value.length === 0) return true;
+ const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
+ if (!subnetRegex.test(value)) return _('Invalid format. Use format: X.X.X.X or X.X.X.X/Y');
+ const [ip, cidr] = value.split('/');
+ const ipParts = ip.split('.');
+ for (const part of ipParts) {
+ const num = parseInt(part);
+ if (num < 0 || num > 255) return _('IP address parts must be between 0 and 255');
+ }
+ if (cidr !== undefined) {
+ const cidrNum = parseInt(cidr);
+ if (cidrNum < 0 || cidrNum > 32) return _('CIDR must be between 0 and 32');
+ }
+ return true;
+ };
+
+ o = s.taboption('basic', form.TextValue, 'custom_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('custom_subnets_list_enabled', 'text');
+ o.rows = 10;
+ o.rmempty = false;
+ o.ucisection = s.section;
+ o.validate = function (section_id, value) {
+ if (!value || value.length === 0) return true;
+
+ const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
+ const lines = value.split(/\n/).map(line => line.trim());
+ let hasValidSubnet = false;
+
+ for (const line of lines) {
+ // Skip empty lines
+ if (!line) continue;
+
+ // Extract subnet part (before any //)
+ const subnetPart = line.split('//')[0].trim();
+
+ // Skip if line is empty after removing comments
+ if (!subnetPart) continue;
+
+ // Process each subnet in the line (separated by comma or space)
+ const subnets = subnetPart.split(/[,\s]+/).map(s => s.trim()).filter(s => s.length > 0);
+
+ for (const subnet of subnets) {
+ if (!subnetRegex.test(subnet)) {
+ return _('Invalid format: %s. Use format: X.X.X.X or X.X.X.X/Y').format(subnet);
+ }
+
+ const [ip, cidr] = subnet.split('/');
+ const ipParts = ip.split('.');
+ for (const part of ipParts) {
+ const num = parseInt(part);
+ if (num < 0 || num > 255) {
+ return _('IP parts must be between 0 and 255 in: %s').format(subnet);
+ }
+ }
+
+ if (cidr !== undefined) {
+ const cidrNum = parseInt(cidr);
+ if (cidrNum < 0 || cidrNum > 32) {
+ return _('CIDR must be between 0 and 32 in: %s').format(subnet);
+ }
+ }
+ hasValidSubnet = true;
+ }
+ }
+
+ if (!hasValidSubnet) {
+ return _('At least one valid subnet or IP must be specified. Comments-only content is not allowed.');
+ }
+
+ return true;
+ };
+
+ o = s.taboption('basic', form.Flag, 'custom_download_subnets_list_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, 'custom_download_subnets', _('Remote Subnet URLs'), _('Enter full URLs starting with http:// or https://'));
+ o.placeholder = 'URL';
+ o.depends('custom_download_subnets_list_enabled', '1');
+ o.rmempty = false;
+ o.ucisection = s.section;
+ o.validate = function (section_id, value) {
+ if (!value || value.length === 0) return true;
+ return networkUtils.validateUrl(value);
+ };
+
+ 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) {
+ if (!value || value.length === 0) return true;
+ const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
+ if (!ipRegex.test(value)) return _('Invalid IP format. Use format: X.X.X.X (like 192.168.1.1)');
+ const ipParts = value.split('.');
+ for (const part of ipParts) {
+ const num = parseInt(part);
+ if (num < 0 || num > 255) return _('IP address parts must be between 0 and 255');
+ }
+ return true;
+ };
+}
+
+return baseclass.extend({
+ createConfigSection
+});
\ No newline at end of file
diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/constants.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/constants.js
new file mode 100644
index 0000000..97379c5
--- /dev/null
+++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/constants.js
@@ -0,0 +1,89 @@
+'use strict';
+'require baseclass';
+
+const STATUS_COLORS = {
+ SUCCESS: '#4caf50',
+ ERROR: '#f44336',
+ WARNING: '#ff9800'
+};
+
+const FAKEIP_CHECK_DOMAIN = 'fakeip.podkop.fyi';
+const IP_CHECK_DOMAIN = 'ip.podkop.fyi';
+
+const REGIONAL_OPTIONS = ['russia_inside', 'russia_outside', 'ukraine_inside'];
+const ALLOWED_WITH_RUSSIA_INSIDE = [
+ 'russia_inside',
+ 'meta',
+ 'twitter',
+ 'discord',
+ 'telegram',
+ 'cloudflare',
+ 'google_ai',
+ 'google_play',
+ 'hetzner',
+ 'ovh'
+];
+
+const DOMAIN_LIST_OPTIONS = {
+ russia_inside: 'Russia inside',
+ russia_outside: 'Russia outside',
+ ukraine_inside: 'Ukraine',
+ geoblock: 'Geo Block',
+ block: 'Block',
+ porn: 'Porn',
+ news: 'News',
+ anime: 'Anime',
+ youtube: 'Youtube',
+ discord: 'Discord',
+ meta: 'Meta',
+ twitter: 'Twitter (X)',
+ hdrezka: 'HDRezka',
+ tiktok: 'Tik-Tok',
+ telegram: 'Telegram',
+ cloudflare: 'Cloudflare',
+ google_ai: 'Google AI',
+ google_play: 'Google Play',
+ hetzner: 'Hetzner ASN',
+ ovh: 'OVH ASN'
+};
+
+const UPDATE_INTERVAL_OPTIONS = {
+ '1h': 'Every hour',
+ '3h': 'Every 3 hours',
+ '12h': 'Every 12 hours',
+ '1d': 'Every day',
+ '3d': 'Every 3 days'
+};
+
+const DNS_SERVER_OPTIONS = {
+ '1.1.1.1': 'Cloudflare (1.1.1.1)',
+ '8.8.8.8': 'Google (8.8.8.8)',
+ '9.9.9.9': 'Quad9 (9.9.9.9)',
+ 'dns.adguard-dns.com': 'AdGuard Default (dns.adguard-dns.com)',
+ 'unfiltered.adguard-dns.com': 'AdGuard Unfiltered (unfiltered.adguard-dns.com)',
+ 'family.adguard-dns.com': 'AdGuard Family (family.adguard-dns.com)'
+};
+
+const DIAGNOSTICS_UPDATE_INTERVAL = 10000; // 10 seconds
+const ERROR_POLL_INTERVAL = 10000; // 10 seconds
+const COMMAND_TIMEOUT = 10000; // 10 seconds
+const FETCH_TIMEOUT = 10000; // 10 seconds
+const BUTTON_FEEDBACK_TIMEOUT = 1000; // 1 second
+const DIAGNOSTICS_INITIAL_DELAY = 100; // 100 milliseconds
+
+return baseclass.extend({
+ STATUS_COLORS,
+ FAKEIP_CHECK_DOMAIN,
+ IP_CHECK_DOMAIN,
+ REGIONAL_OPTIONS,
+ ALLOWED_WITH_RUSSIA_INSIDE,
+ DOMAIN_LIST_OPTIONS,
+ UPDATE_INTERVAL_OPTIONS,
+ DNS_SERVER_OPTIONS,
+ DIAGNOSTICS_UPDATE_INTERVAL,
+ ERROR_POLL_INTERVAL,
+ COMMAND_TIMEOUT,
+ FETCH_TIMEOUT,
+ BUTTON_FEEDBACK_TIMEOUT,
+ DIAGNOSTICS_INITIAL_DELAY
+});
diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticSection.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticSection.js
new file mode 100644
index 0000000..6831020
--- /dev/null
+++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticSection.js
@@ -0,0 +1,950 @@
+'use strict';
+'require baseclass';
+'require form';
+'require ui';
+'require uci';
+'require fs';
+'require view.podkop.constants as constants';
+
+// Helper Functions
+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');
+}
+
+const 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, constants.BUTTON_FEEDBACK_TIMEOUT);
+ } catch (err) {
+ ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message));
+ }
+ document.body.removeChild(textarea);
+};
+
+// IP masking function
+const maskIP = (ip) => {
+ if (!ip) return '';
+ const parts = ip.split('.');
+ if (parts.length !== 4) return ip;
+ return ['XX', 'XX', 'XX', parts[3]].join('.');
+};
+
+async function safeExec(command, args = [], timeout = constants.COMMAND_TIMEOUT) {
+ 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);
+ return result;
+ } catch (error) {
+ console.warn(`Command execution failed or timed out: ${command} ${args.join(' ')}`);
+ return { stdout: '', stderr: error.message };
+ }
+}
+
+// Status Check Functions
+async function checkFakeIP() {
+ const createStatus = (state, message, color) => ({
+ state,
+ message: _(message),
+ color: constants.STATUS_COLORS[color]
+ });
+
+ try {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), constants.FETCH_TIMEOUT);
+
+ try {
+ const response = await fetch(`https://${constants.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');
+ }
+}
+
+async function checkFakeIPCLI() {
+ const createStatus = (state, message, color) => ({
+ state,
+ message: _(message),
+ color: constants.STATUS_COLORS[color]
+ });
+
+ try {
+ const singboxStatusResult = await safeExec('/usr/bin/podkop', ['get_sing_box_status']);
+ const singboxStatus = JSON.parse(singboxStatusResult.stdout || '{"running":0,"dns_configured":0}');
+
+ if (!singboxStatus.running) {
+ return createStatus('not_working', 'sing-box not running', 'ERROR');
+ }
+
+ const result = await safeExec('nslookup', ['-timeout=2', constants.FAKEIP_CHECK_DOMAIN, '127.0.0.42']);
+
+ if (result.stdout && result.stdout.includes('198.18')) {
+ return createStatus('working', 'working on router', 'SUCCESS');
+ } else {
+ return createStatus('not_working', 'not working on router', 'ERROR');
+ }
+ } catch (error) {
+ return createStatus('error', 'CLI check error', 'WARNING');
+ }
+}
+
+function checkDNSAvailability() {
+ const createStatus = (state, message, color) => ({
+ state,
+ message: _(message),
+ color: constants.STATUS_COLORS[color]
+ });
+
+ return new Promise(async (resolve) => {
+ try {
+ const dnsStatusResult = await safeExec('/usr/bin/podkop', ['check_dns_available']);
+ 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 resolve({
+ remote: createStatus('error', 'DNS check error', 'WARNING'),
+ local: createStatus('error', 'DNS check error', 'WARNING')
+ });
+ }
+ });
+}
+
+async function checkBypass() {
+ const createStatus = (state, message, color) => ({
+ state,
+ message: _(message),
+ color: constants.STATUS_COLORS[color]
+ });
+
+ return new Promise(async (resolve) => {
+ try {
+ let configMode = 'proxy'; // Default fallback
+ try {
+ const data = await uci.load('podkop');
+ configMode = uci.get('podkop', 'main', 'mode') || 'proxy';
+ } catch (e) {
+ console.error('Error getting mode from UCI:', e);
+ }
+
+ // Check if sing-box is running
+ const singboxStatusResult = await safeExec('/usr/bin/podkop', ['get_sing_box_status']);
+ const singboxStatus = JSON.parse(singboxStatusResult.stdout || '{"running":0,"dns_configured":0}');
+
+ if (!singboxStatus.running) {
+ return resolve(createStatus('not_working', `${configMode} not running`, 'ERROR'));
+ }
+
+ // Fetch IP from first endpoint
+ let ip1 = null;
+ try {
+ const controller1 = new AbortController();
+ const timeoutId1 = setTimeout(() => controller1.abort(), constants.FETCH_TIMEOUT);
+
+ const response1 = await fetch(`https://${constants.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller1.signal });
+ const data1 = await response1.json();
+ clearTimeout(timeoutId1);
+
+ ip1 = data1.IP;
+ } catch (error) {
+ return resolve(createStatus('error', 'First endpoint check failed', 'WARNING'));
+ }
+
+ // Fetch IP from second endpoint
+ let ip2 = null;
+ try {
+ const controller2 = new AbortController();
+ const timeoutId2 = setTimeout(() => controller2.abort(), constants.FETCH_TIMEOUT);
+
+ const response2 = await fetch(`https://${constants.IP_CHECK_DOMAIN}/check`, { signal: controller2.signal });
+ const data2 = await response2.json();
+ clearTimeout(timeoutId2);
+
+ ip2 = data2.IP;
+ } catch (error) {
+ return resolve(createStatus('not_working', `${configMode} not working`, 'ERROR'));
+ }
+
+ // Compare IPs
+ if (ip1 && ip2) {
+ if (ip1 !== ip2) {
+ return resolve(createStatus('working', `${configMode} working correctly`, 'SUCCESS'));
+ } else {
+ return resolve(createStatus('not_working', `${configMode} routing incorrect`, 'ERROR'));
+ }
+ } else {
+ return resolve(createStatus('error', 'IP comparison failed', 'WARNING'));
+ }
+ } catch (error) {
+ return resolve(createStatus('error', 'Bypass check error', 'WARNING'));
+ }
+ });
+}
+
+// Error Handling
+async function getPodkopErrors() {
+ try {
+ const result = await safeExec('/usr/bin/podkop', ['check_logs']);
+ if (!result || !result.stdout) return [];
+
+ const logs = result.stdout.split('\n');
+ const errors = logs.filter(log =>
+ log.includes('[critical]')
+ );
+
+ console.log('Found errors:', errors);
+ return errors;
+ } catch (error) {
+ console.error('Error getting podkop logs:', error);
+ return [];
+ }
+}
+
+function showErrorNotification(error, isMultiple = false) {
+ const notificationContent = E('div', { 'class': 'alert-message error' }, [
+ E('pre', { 'class': 'error-log' }, error)
+ ]);
+
+ ui.addNotification(null, notificationContent);
+}
+
+// Modal Functions
+const createModalContent = (title, content) => {
+ return [
+ 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', { style: 'margin: 0;' }, content)
+ ]),
+ E('div', {
+ 'class': 'right',
+ style: 'margin-top: 1em;'
+ }, [
+ E('button', {
+ 'class': 'btn',
+ 'click': ev => copyToClipboard('```txt\n' + content + '\n```', ev.target)
+ }, _('Copy to Clipboard')),
+ E('button', {
+ 'class': 'btn',
+ 'click': ui.hideModal
+ }, _('Close'))
+ ])
+ ];
+};
+
+const showConfigModal = async (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'))
+ ])
+ ]);
+
+ 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') {
+ const res = await safeExec('/usr/bin/podkop', [command]);
+ formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
+
+ try {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), constants.FETCH_TIMEOUT);
+
+ const response = await fetch(`https://${constants.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal });
+ const data = await response.json();
+ 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
+ const bypassResponse = await fetch(`https://${constants.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal });
+ const bypassData = await bypassResponse.json();
+ const bypassResponse2 = await fetch(`https://${constants.IP_CHECK_DOMAIN}/check`, { signal: controller.signal });
+ const bypassData2 = await bypassResponse2.json();
+
+ 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);
+ }
+ } else {
+ const res = await safeExec('/usr/bin/podkop', [command]);
+ 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));
+ },
+
+ createActionButton: function (config) {
+ return this.createButton({
+ label: config.label,
+ additionalClass: `cbi-button-${config.type || ''}`,
+ onClick: () => safeExec('/usr/bin/podkop', [config.action])
+ .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])
+ .then(() => config.reload && location.reload()),
+ style: config.style
+ });
+ },
+
+ createModalButton: function (config) {
+ return this.createButton({
+ label: config.label,
+ onClick: () => showConfigModal(config.command, config.title),
+ style: config.style
+ });
+ }
+};
+
+// Status Panel Factory
+const createStatusPanel = (title, status, buttons, extraData = {}) => {
+ const headerContent = [
+ E('strong', {}, _(title)),
+ status && E('br'),
+ status && E('span', {
+ 'style': `color: ${title === 'Sing-box Status' ?
+ (status.running && !status.enabled ? constants.STATUS_COLORS.SUCCESS : constants.STATUS_COLORS.ERROR) :
+ title === 'Podkop Status' ?
+ (status.enabled ? constants.STATUS_COLORS.SUCCESS : constants.STATUS_COLORS.ERROR) :
+ (status.running ? constants.STATUS_COLORS.SUCCESS : constants.STATUS_COLORS.ERROR)
+ }`
+ }, [
+ title === 'Sing-box Status' ?
+ (status.running && !status.enabled ? '✔ running' : '✘ ' + status.status) :
+ title === 'Podkop Status' ?
+ (status.enabled ? '✔ Autostart enabled' : '✘ Autostart disabled') :
+ (status.running ? '✔' : '✘') + ' ' + status.status
+ ])
+ ].filter(Boolean);
+
+ return E('div', {
+ 'class': 'panel',
+ 'style': 'flex: 1; padding: 15px;'
+ }, [
+ E('div', { 'class': 'panel-heading' }, headerContent),
+ E('div', {
+ 'class': 'panel-body',
+ 'style': 'display: flex; flex-direction: column; gap: 8px;'
+ }, title === 'Podkop Status' ? [
+ ButtonFactory.createActionButton({
+ label: 'Restart Podkop',
+ type: 'apply',
+ action: 'restart',
+ reload: true
+ }),
+ ButtonFactory.createActionButton({
+ label: 'Stop Podkop',
+ type: 'apply',
+ action: 'stop',
+ reload: true
+ }),
+ ButtonFactory.createInitActionButton({
+ label: status.enabled ? 'Disable Autostart' : 'Enable Autostart',
+ type: status.enabled ? 'remove' : 'apply',
+ action: status.enabled ? 'disable' : 'enable',
+ reload: true
+ }),
+ ButtonFactory.createModalButton({
+ label: E('strong', _('Global check')),
+ command: 'global_check',
+ title: _('Global check')
+ }),
+ ButtonFactory.createModalButton({
+ label: 'View Logs',
+ command: 'check_logs',
+ title: 'Podkop Logs'
+ }),
+ ButtonFactory.createModalButton({
+ label: _('Update Lists'),
+ command: 'list_update',
+ title: _('Lists Update Results')
+ })
+ ] : title === _('FakeIP Status') ? [
+ E('div', { style: 'margin-bottom: 5px;' }, [
+ E('div', {}, [
+ E('span', { style: `color: ${extraData.fakeipStatus?.color}` }, [
+ extraData.fakeipStatus?.state === 'working' ? '✔' : extraData.fakeipStatus?.state === 'not_working' ? '✘' : '!',
+ ' ',
+ extraData.fakeipStatus?.state === 'working' ? _('works in browser') : _('not works in browser')
+ ])
+ ]),
+ E('div', {}, [
+ E('span', { style: `color: ${extraData.fakeipCLIStatus?.color}` }, [
+ extraData.fakeipCLIStatus?.state === 'working' ? '✔' : extraData.fakeipCLIStatus?.state === 'not_working' ? '✘' : '!',
+ ' ',
+ extraData.fakeipCLIStatus?.state === 'working' ? _('works on router') : _('not works on router')
+ ])
+ ])
+ ]),
+ E('div', { style: 'margin-bottom: 5px;' }, [
+ E('div', {}, [
+ E('strong', {}, _('DNS Status')),
+ E('br'),
+ E('span', { style: `color: ${extraData.dnsStatus?.remote?.color}` }, [
+ extraData.dnsStatus?.remote?.state === 'available' ? '✔' : extraData.dnsStatus?.remote?.state === 'unavailable' ? '✘' : '!',
+ ' ',
+ extraData.dnsStatus?.remote?.message
+ ]),
+ E('br'),
+ E('span', { style: `color: ${extraData.dnsStatus?.local?.color}` }, [
+ extraData.dnsStatus?.local?.state === 'available' ? '✔' : extraData.dnsStatus?.local?.state === 'unavailable' ? '✘' : '!',
+ ' ',
+ extraData.dnsStatus?.local?.message
+ ])
+ ])
+ ]),
+ E('div', { style: 'margin-bottom: 5px;' }, [
+ E('div', {}, [
+ E('strong', {}, extraData.configName),
+ E('br'),
+ E('span', { style: `color: ${extraData.bypassStatus?.color}` }, [
+ extraData.bypassStatus?.state === 'working' ? '✔' : extraData.bypassStatus?.state === 'not_working' ? '✘' : '!',
+ ' ',
+ extraData.bypassStatus?.message
+ ])
+ ])
+ ])
+ ] : buttons)
+ ]);
+};
+
+// Create the status section
+let createStatusSection = function (podkopStatus, singboxStatus, podkop, luci, singbox, system, fakeipStatus, fakeipCLIStatus, dnsStatus, bypassStatus, configName) {
+ return E('div', { 'class': 'cbi-section' }, [
+ E('div', { 'class': 'table', style: 'display: flex; gap: 20px;' }, [
+ // Podkop Status Panel
+ createStatusPanel('Podkop Status', podkopStatus, [
+ ButtonFactory.createActionButton({
+ label: 'Restart Podkop',
+ type: 'apply',
+ action: 'restart',
+ reload: true
+ }),
+ ButtonFactory.createActionButton({
+ label: 'Stop Podkop',
+ type: 'apply',
+ action: 'stop',
+ reload: true
+ }),
+ ButtonFactory.createInitActionButton({
+ label: podkopStatus.enabled ? 'Disable Autostart' : 'Enable Autostart',
+ type: podkopStatus.enabled ? 'remove' : 'apply',
+ action: podkopStatus.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
+ createStatusPanel('Sing-box Status', singboxStatus, [
+ 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
+ createStatusPanel(_('FakeIP Status'), null, null, {
+ fakeipStatus,
+ fakeipCLIStatus,
+ dnsStatus,
+ bypassStatus,
+ configName
+ }),
+
+ // Version Information Panel
+ createStatusPanel(_('Version Information'), null, [
+ E('div', { 'style': 'margin-top: 10px; font-family: monospace; white-space: pre-wrap;' }, [
+ E('strong', {}, _('Podkop: ')), podkop.stdout ? podkop.stdout.trim() : _('Unknown'), '\n',
+ E('strong', {}, _('LuCI App: ')), luci.stdout ? luci.stdout.trim() : _('Unknown'), '\n',
+ E('strong', {}, _('Sing-box: ')), singbox.stdout ? singbox.stdout.trim() : _('Unknown'), '\n',
+ E('strong', {}, _('OpenWrt Version: ')), system.stdout ? system.stdout.split('\n')[1].trim() : _('Unknown'), '\n',
+ E('strong', {}, _('Device Model: ')), system.stdout ? system.stdout.split('\n')[4].trim() : _('Unknown')
+ ])
+ ])
+ ])
+ ]);
+};
+
+// Diagnostics Update Functions
+let diagnosticsUpdateTimer = null;
+let errorPollTimer = null;
+let lastErrorsSet = new Set();
+let isInitialCheck = true;
+
+function startDiagnosticsUpdates() {
+ if (diagnosticsUpdateTimer) {
+ clearInterval(diagnosticsUpdateTimer);
+ }
+
+ const container = document.getElementById('diagnostics-status');
+ if (container) {
+ container.innerHTML = _('Loading diagnostics...');
+ }
+
+ updateDiagnostics();
+ diagnosticsUpdateTimer = setInterval(updateDiagnostics, constants.DIAGNOSTICS_UPDATE_INTERVAL);
+}
+
+function stopDiagnosticsUpdates() {
+ if (diagnosticsUpdateTimer) {
+ clearInterval(diagnosticsUpdateTimer);
+ diagnosticsUpdateTimer = null;
+ }
+
+ // Reset the loading state when stopping updates
+ const container = document.getElementById('diagnostics-status');
+ if (container) {
+ container.removeAttribute('data-loading');
+ }
+}
+
+function startErrorPolling() {
+ if (errorPollTimer) {
+ clearInterval(errorPollTimer);
+ }
+
+ async function checkErrors() {
+ const result = await safeExec('/usr/bin/podkop', ['check_logs']);
+ if (!result || !result.stdout) return;
+
+ const logs = result.stdout;
+
+ const errorLines = logs.split('\n').filter(line =>
+ line.includes('[critical]')
+ );
+
+ if (errorLines.length > 0) {
+ const currentErrors = new Set(errorLines);
+
+ if (isInitialCheck) {
+ if (errorLines.length > 0) {
+ showErrorNotification(errorLines.join('\n'), true);
+ }
+ isInitialCheck = false;
+ } else {
+ const newErrors = [...currentErrors].filter(error => !lastErrorsSet.has(error));
+
+ newErrors.forEach(error => {
+ showErrorNotification(error, false);
+ });
+ }
+ lastErrorsSet = currentErrors;
+ }
+ }
+
+ checkErrors();
+
+ errorPollTimer = setInterval(checkErrors, constants.ERROR_POLL_INTERVAL);
+}
+
+function stopErrorPolling() {
+ if (errorPollTimer) {
+ clearInterval(errorPollTimer);
+ errorPollTimer = null;
+ }
+}
+
+async function updateDiagnostics() {
+ try {
+ const results = {
+ podkopStatus: null,
+ singboxStatus: null,
+ podkop: null,
+ luci: null,
+ singbox: null,
+ system: null,
+ fakeipStatus: null,
+ fakeipCLIStatus: null,
+ dnsStatus: null,
+ bypassStatus: null
+ };
+
+ // Perform all checks independently of each other
+ const checks = [
+ safeExec('/usr/bin/podkop', ['get_status'])
+ .then(result => results.podkopStatus = result)
+ .catch(() => results.podkopStatus = { stdout: '{"enabled":0,"status":"error"}' }),
+
+ safeExec('/usr/bin/podkop', ['get_sing_box_status'])
+ .then(result => results.singboxStatus = result)
+ .catch(() => results.singboxStatus = { stdout: '{"running":0,"enabled":0,"status":"error"}' }),
+
+ safeExec('/usr/bin/podkop', ['show_version'])
+ .then(result => results.podkop = result)
+ .catch(() => results.podkop = { stdout: 'error' }),
+
+ safeExec('/usr/bin/podkop', ['show_luci_version'])
+ .then(result => results.luci = result)
+ .catch(() => results.luci = { stdout: 'error' }),
+
+ safeExec('/usr/bin/podkop', ['show_sing_box_version'])
+ .then(result => results.singbox = result)
+ .catch(() => results.singbox = { stdout: 'error' }),
+
+ safeExec('/usr/bin/podkop', ['show_system_info'])
+ .then(result => results.system = result)
+ .catch(() => results.system = { stdout: 'error' }),
+
+ checkFakeIP()
+ .then(result => results.fakeipStatus = result)
+ .catch(() => results.fakeipStatus = { state: 'error', message: 'check error', color: constants.STATUS_COLORS.WARNING }),
+
+ checkFakeIPCLI()
+ .then(result => results.fakeipCLIStatus = result)
+ .catch(() => results.fakeipCLIStatus = { state: 'error', message: 'check error', color: constants.STATUS_COLORS.WARNING }),
+
+ checkDNSAvailability()
+ .then(result => results.dnsStatus = result)
+ .catch(() => results.dnsStatus = {
+ remote: { state: 'error', message: 'DNS check error', color: constants.STATUS_COLORS.WARNING },
+ local: { state: 'error', message: 'DNS check error', color: constants.STATUS_COLORS.WARNING }
+ }),
+
+ checkBypass()
+ .then(result => results.bypassStatus = result)
+ .catch(() => results.bypassStatus = { state: 'error', message: 'check error', color: constants.STATUS_COLORS.WARNING })
+ ];
+
+ // Waiting for all the checks to be completed
+ await Promise.allSettled(checks);
+
+ const container = document.getElementById('diagnostics-status');
+ if (!container) return;
+
+ let configName = _('Main config');
+ try {
+ 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 (e) {
+ console.error('Error getting config name from UCI:', e);
+ }
+
+ const parsedPodkopStatus = JSON.parse(results.podkopStatus.stdout || '{"enabled":0,"status":"error"}');
+ const parsedSingboxStatus = JSON.parse(results.singboxStatus.stdout || '{"running":0,"enabled":0,"status":"error"}');
+
+ const statusSection = createStatusSection(
+ parsedPodkopStatus,
+ parsedSingboxStatus,
+ results.podkop,
+ results.luci,
+ results.singbox,
+ results.system,
+ results.fakeipStatus,
+ results.fakeipCLIStatus,
+ results.dnsStatus,
+ results.bypassStatus,
+ configName
+ );
+
+ container.innerHTML = '';
+ container.appendChild(statusSection);
+
+ // Updating individual status items
+ const updateStatusElement = (elementId, status, template) => {
+ const element = document.getElementById(elementId);
+ if (element) {
+ element.innerHTML = template(status);
+ }
+ };
+
+ updateStatusElement('fakeip-status', results.fakeipStatus,
+ status => E('span', { 'style': `color: ${status.color}` }, [
+ status.state === 'working' ? '✔ ' : status.state === 'not_working' ? '✘ ' : '! ',
+ status.message
+ ]).outerHTML
+ );
+
+ updateStatusElement('fakeip-cli-status', results.fakeipCLIStatus,
+ status => E('span', { 'style': `color: ${status.color}` }, [
+ status.state === 'working' ? '✔ ' : status.state === 'not_working' ? '✘ ' : '! ',
+ status.message
+ ]).outerHTML
+ );
+
+ updateStatusElement('dns-remote-status', results.dnsStatus.remote,
+ status => E('span', { 'style': `color: ${status.color}` }, [
+ status.state === 'available' ? '✔ ' : status.state === 'unavailable' ? '✘ ' : '! ',
+ status.message
+ ]).outerHTML
+ );
+
+ updateStatusElement('dns-local-status', results.dnsStatus.local,
+ status => E('span', { 'style': `color: ${status.color}` }, [
+ status.state === 'available' ? '✔ ' : status.state === 'unavailable' ? '✘ ' : '! ',
+ status.message
+ ]).outerHTML
+ );
+
+ } catch (e) {
+ const container = document.getElementById('diagnostics-status');
+ if (container) {
+ container.innerHTML = E('div', { 'class': 'alert-message warning' }, [
+ E('strong', {}, _('Error loading diagnostics')),
+ E('br'),
+ E('pre', {}, e.toString())
+ ]).outerHTML;
+ }
+ }
+}
+
+function createDiagnosticsSection(mainSection) {
+ let o = mainSection.tab('diagnostics', _('Diagnostics'));
+
+ o = mainSection.taboption('diagnostics', form.DummyValue, '_status');
+ o.rawhtml = true;
+ o.cfgvalue = () => E('div', {
+ id: 'diagnostics-status',
+ 'style': 'cursor: pointer;'
+ }, _('Click to load diagnostics...'));
+}
+
+function setupDiagnosticsEventHandlers(node) {
+ const titleDiv = E('h2', { 'class': 'cbi-map-title' }, _('Podkop'));
+ node.insertBefore(titleDiv, node.firstChild);
+
+ document.addEventListener('visibilitychange', function () {
+ const diagnosticsContainer = document.getElementById('diagnostics-status');
+ if (document.hidden) {
+ stopDiagnosticsUpdates();
+ stopErrorPolling();
+ } else if (diagnosticsContainer && diagnosticsContainer.hasAttribute('data-loading')) {
+ startDiagnosticsUpdates();
+ startErrorPolling();
+ }
+ });
+
+ setTimeout(() => {
+ const diagnosticsContainer = document.getElementById('diagnostics-status');
+ if (diagnosticsContainer) {
+ diagnosticsContainer.addEventListener('click', function () {
+ if (!this.hasAttribute('data-loading')) {
+ this.setAttribute('data-loading', 'true');
+ startDiagnosticsUpdates();
+ startErrorPolling();
+ }
+ });
+ }
+
+ 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');
+ if (container && !container.hasAttribute('data-loading')) {
+ container.setAttribute('data-loading', 'true');
+ startDiagnosticsUpdates();
+ startErrorPolling();
+ }
+ } else {
+ stopDiagnosticsUpdates();
+ stopErrorPolling();
+ }
+ }
+ });
+
+ const activeTab = tabs[0].querySelector('.cbi-tab[data-tab="diagnostics"]');
+ if (activeTab) {
+ const container = document.getElementById('diagnostics-status');
+ if (container && !container.hasAttribute('data-loading')) {
+ container.setAttribute('data-loading', 'true');
+ startDiagnosticsUpdates();
+ startErrorPolling();
+ }
+ }
+ }
+ }, constants.DIAGNOSTICS_INITIAL_DELAY);
+
+ node.classList.add('fade-in');
+ return node;
+}
+
+return baseclass.extend({
+ createDiagnosticsSection,
+ setupDiagnosticsEventHandlers
+});
\ No newline at end of file
diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/networkUtils.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/networkUtils.js
new file mode 100644
index 0000000..d41b6e6
--- /dev/null
+++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/networkUtils.js
@@ -0,0 +1,56 @@
+'use strict';
+'require baseclass';
+'require network';
+
+function validateUrl(url, protocols = ['http:', 'https:']) {
+ try {
+ const parsedUrl = new URL(url);
+ if (!protocols.includes(parsedUrl.protocol)) {
+ return _('URL must use one of the following protocols: ') + protocols.join(', ');
+ }
+ return true;
+ } catch (e) {
+ return _('Invalid URL format');
+ }
+}
+
+function getNetworkInterfaces(o, section_id, excludeInterfaces = []) {
+ return network.getDevices().then(devices => {
+ o.keylist = [];
+ o.vallist = [];
+
+ devices.forEach(device => {
+ if (device.dev && device.dev.name) {
+ const deviceName = device.dev.name;
+ if (!excludeInterfaces.includes(deviceName)) {
+ o.value(deviceName, deviceName);
+ }
+ }
+ });
+ }).catch(error => {
+ console.error('Failed to get network devices:', error);
+ });
+}
+
+function getNetworkNetworks(o, section_id, excludeInterfaces = []) {
+ return network.getNetworks().then(networks => {
+ o.keylist = [];
+ o.vallist = [];
+
+ networks.forEach(net => {
+ const name = net.getName();
+ const ifname = net.getIfname();
+ if (name && !excludeInterfaces.includes(name)) {
+ o.value(name, ifname ? `${name} (${ifname})` : name);
+ }
+ });
+ }).catch(error => {
+ console.error('Failed to get networks:', error);
+ });
+}
+
+return baseclass.extend({
+ getNetworkInterfaces,
+ getNetworkNetworks,
+ validateUrl
+});
\ No newline at end of file
diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js
index 230c773..32b1943 100644
--- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js
+++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js
@@ -1,1212 +1,11 @@
'use strict';
'require view';
'require form';
-'require ui';
'require network';
-'require fs';
-'require uci';
-
-const STATUS_COLORS = {
- SUCCESS: '#4caf50',
- ERROR: '#f44336',
- WARNING: '#ff9800'
-};
-
-const FAKEIP_CHECK_DOMAIN = 'fakeip.podkop.fyi';
-const IP_CHECK_DOMAIN = 'ip.podkop.fyi';
-
-const REGIONAL_OPTIONS = ['russia_inside', 'russia_outside', 'ukraine_inside'];
-const ALLOWED_WITH_RUSSIA_INSIDE = [
- 'russia_inside',
- 'meta',
- 'twitter',
- 'discord',
- 'telegram',
- 'cloudflare',
- 'google_ai',
- 'google_play',
- 'hetzner',
- 'ovh'
-];
-
-const DOMAIN_LIST_OPTIONS = {
- russia_inside: 'Russia inside',
- russia_outside: 'Russia outside',
- ukraine_inside: 'Ukraine',
- geoblock: 'Geo Block',
- block: 'Block',
- porn: 'Porn',
- news: 'News',
- anime: 'Anime',
- youtube: 'Youtube',
- discord: 'Discord',
- meta: 'Meta',
- twitter: 'Twitter (X)',
- hdrezka: 'HDRezka',
- tiktok: 'Tik-Tok',
- telegram: 'Telegram',
- cloudflare: 'Cloudflare',
- google_ai: 'Google AI',
- google_play: 'Google Play',
- hetzner: 'Hetzner ASN',
- ovh: 'OVH ASN'
-};
-
-const DIAGNOSTICS_UPDATE_INTERVAL = 10000; // 10 seconds
-const ERROR_POLL_INTERVAL = 10000; // 10 seconds
-const COMMAND_TIMEOUT = 10000; // 10 seconds
-const FETCH_TIMEOUT = 10000; // 10 seconds
-const BUTTON_FEEDBACK_TIMEOUT = 1000; // 1 second
-const DIAGNOSTICS_INITIAL_DELAY = 100; // 100 milliseconds
-
-async function safeExec(command, args = [], timeout = COMMAND_TIMEOUT) {
- 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);
- return result;
- } catch (error) {
- console.warn(`Command execution failed or timed out: ${command} ${args.join(' ')}`);
- return { stdout: '', stderr: error.message };
- }
-}
-
-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');
-}
-
-function getNetworkInterfaces(o, section_id, excludeInterfaces = []) {
- return network.getDevices().then(devices => {
- o.keylist = [];
- o.vallist = [];
-
- devices.forEach(device => {
- if (device.dev && device.dev.name) {
- const deviceName = device.dev.name;
- if (!excludeInterfaces.includes(deviceName)) {
- o.value(deviceName, deviceName);
- }
- }
- });
- }).catch(error => {
- console.error('Failed to get network devices:', error);
- });
-}
-
-function getNetworkNetworks(o, section_id, excludeInterfaces = []) {
- return network.getNetworks().then(networks => {
- o.keylist = [];
- o.vallist = [];
-
- networks.forEach(net => {
- const name = net.getName();
- const ifname = net.getIfname();
- if (name && !excludeInterfaces.includes(name)) {
- o.value(name, ifname ? `${name} (${ifname})` : name);
- }
- });
- }).catch(error => {
- console.error('Failed to get networks:', error);
- });
-}
-
-function createConfigSection(section, map, network) {
- const s = section;
-
- let o = s.tab('basic', _('Basic Settings'));
-
- o = s.taboption('basic', form.ListValue, 'mode', _('Connection Type'), _('Select between VPN and Proxy connection methods for traffic routing'));
- o.value('proxy', ('Proxy'));
- o.value('vpn', ('VPN'));
- o.value('block', ('Block'));
- o.ucisection = s.section;
-
- o = s.taboption('basic', form.ListValue, 'proxy_config_type', _('Configuration Type'), _('Select how to configure the proxy'));
- o.value('url', _('Connection URL'));
- o.value('outbound', _('Outbound Config'));
- o.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.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';
-
- o.renderWidget = function (section_id, option_index, cfgvalue) {
- const original = form.TextValue.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]);
- const container = E('div', {});
- container.appendChild(original);
-
- if (cfgvalue) {
- try {
- const activeConfig = cfgvalue.split('\n')
- .map(line => line.trim())
- .find(line => line && !line.startsWith('//'));
-
- if (activeConfig) {
- if (activeConfig.includes('#')) {
- const label = activeConfig.split('#').pop();
- if (label && label.trim()) {
- const decodedLabel = decodeURIComponent(label);
- const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Current config: ') + decodedLabel);
- container.appendChild(descDiv);
- } else {
- const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description'));
- container.appendChild(descDiv);
- }
- } else {
- const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description'));
- container.appendChild(descDiv);
- }
- }
- } catch (e) {
- console.error('Error parsing config label:', e);
- const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description'));
- container.appendChild(descDiv);
- }
- } else {
- const defaultDesc = E('div', { 'class': 'cbi-value-description' },
- _('Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs'));
- container.appendChild(defaultDesc);
- }
-
- return container;
- };
-
- o.validate = function (section_id, value) {
- 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.');
- }
-
- if (!activeConfig.startsWith('vless://') && !activeConfig.startsWith('ss://')) {
- return _('URL must start with vless:// or ss://');
- }
-
- if (activeConfig.startsWith('ss://')) {
- let encrypted_part;
- try {
- let mainPart = activeConfig.includes('?') ? activeConfig.split('?')[0] : activeConfig.split('#')[0];
- encrypted_part = mainPart.split('/')[2].split('@')[0];
- try {
- let decoded = atob(encrypted_part);
- if (!decoded.includes(':')) {
- if (!encrypted_part.includes(':') && !encrypted_part.includes('-')) {
- return _('Invalid Shadowsocks URL format: missing method and password separator ":"');
- }
- }
- } catch (e) {
- if (!encrypted_part.includes(':') && !encrypted_part.includes('-')) {
- return _('Invalid Shadowsocks URL format: missing method and password separator ":"');
- }
- }
- } catch (e) {
- return _('Invalid Shadowsocks URL format');
- }
-
- try {
- let serverPart = activeConfig.split('@')[1];
- if (!serverPart) return _('Invalid Shadowsocks URL: missing server address');
- let [server, portAndRest] = serverPart.split(':');
- if (!server) return _('Invalid Shadowsocks URL: missing server');
- let port = portAndRest ? portAndRest.split(/[?#]/)[0] : null;
- if (!port) return _('Invalid Shadowsocks URL: missing port');
- let portNum = parseInt(port);
- if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
- return _('Invalid port number. Must be between 1 and 65535');
- }
- } catch (e) {
- return _('Invalid Shadowsocks URL: missing or invalid server/port format');
- }
- }
-
- if (activeConfig.startsWith('vless://')) {
- let uuid = activeConfig.split('/')[2].split('@')[0];
- if (!uuid || uuid.length === 0) return _('Invalid VLESS URL: missing UUID');
-
- try {
- let serverPart = activeConfig.split('@')[1];
- if (!serverPart) return _('Invalid VLESS URL: missing server address');
- let [server, portAndRest] = serverPart.split(':');
- if (!server) return _('Invalid VLESS URL: missing server');
- let port = portAndRest ? portAndRest.split(/[/?#]/)[0] : null;
- if (!port) return _('Invalid VLESS URL: missing port');
- let portNum = parseInt(port);
- if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
- return _('Invalid port number. Must be between 1 and 65535');
- }
- } catch (e) {
- return _('Invalid VLESS URL: missing or invalid server/port format');
- }
-
- let queryString = activeConfig.split('?')[1];
- if (!queryString) return _('Invalid VLESS URL: missing query parameters');
-
- let params = new URLSearchParams(queryString.split('#')[0]);
- let type = params.get('type');
- const validTypes = ['tcp', 'raw', 'udp', 'grpc', 'http', 'ws'];
- if (!type || !validTypes.includes(type)) {
- return _('Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws');
- }
-
- let security = params.get('security');
- const validSecurities = ['tls', 'reality', 'none'];
- if (!security || !validSecurities.includes(security)) {
- return _('Invalid VLESS URL: security must be one of tls, reality, none');
- }
-
- if (security === 'reality') {
- if (!params.get('pbk')) return _('Invalid VLESS URL: missing pbk parameter for reality security');
- if (!params.get('fp')) return _('Invalid VLESS URL: missing fp parameter for reality security');
- }
-
- if (security === 'tls' && type !== 'tcp' && !params.get('sni')) {
- return _('Invalid VLESS URL: missing sni parameter for tls security');
- }
- }
-
- return true;
- } catch (e) {
- console.error('Validation error:', 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) {
- if (!value || value.length === 0) return true;
- try {
- const parsed = JSON.parse(value);
- if (!parsed.type || !parsed.server || !parsed.server_port) {
- return _('JSON must contain at least type, server and server_port fields');
- }
- return true;
- } catch (e) {
- return _('Invalid JSON format');
- }
- };
-
- 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 = 'main';
-
- o = s.taboption('basic', form.ListValue, 'interface', _('Network Interface'), _('Select network interface for VPN connection'));
- o.depends('mode', 'vpn');
- o.ucisection = s.section;
- o.load = function (section_id) {
- return getNetworkInterfaces(this, section_id, ['br-lan', 'eth0', 'eth1', 'wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan', 'lan']).then(() => {
- return this.super('load', section_id);
- });
- };
-
- o = s.taboption('basic', form.Flag, 'domain_list_enabled', _('Community Lists'));
- o.default = '0';
- o.rmempty = false;
- o.ucisection = s.section;
-
- o = s.taboption('basic', form.DynamicList, 'domain_list', _('Service List'), _('Select predefined service for routing') + ' github.com/itdoginfo/allow-domains');
- o.placeholder = 'Service list';
- Object.entries(DOMAIN_LIST_OPTIONS).forEach(([key, label]) => {
- o.value(key, _(label));
- });
-
- o.depends('domain_list_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 = 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 || !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 => !ALLOWED_WITH_RUSSIA_INSIDE.includes(v));
- if (removedServices.length > 0) {
- newValues = newValues.filter(v => 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(
- ALLOWED_WITH_RUSSIA_INSIDE.map(key => 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, 'custom_domains_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, 'custom_domains', _('User Domains'), _('Enter domain names without protocols (example: sub.example.com or example.com)'));
- o.placeholder = 'Domains list';
- o.depends('custom_domains_list_type', 'dynamic');
- o.rmempty = false;
- o.ucisection = s.section;
- o.validate = function (section_id, value) {
- if (!value || value.length === 0) return true;
- const domainRegex = /^(?!-)[A-Za-z0-9-]+([-.][A-Za-z0-9-]+)*(\.[A-Za-z]{2,})?$/;
- if (!domainRegex.test(value)) {
- return _('Invalid domain format. Enter domain without protocol (example: sub.example.com or ru)');
- }
- return true;
- };
-
- o = s.taboption('basic', form.TextValue, 'custom_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('custom_domains_list_type', 'text');
- o.rows = 8;
- o.rmempty = false;
- o.ucisection = s.section;
- o.validate = function (section_id, value) {
- if (!value || value.length === 0) return true;
-
- const domainRegex = /^(?!-)[A-Za-z0-9-]+([-.][A-Za-z0-9-]+)*(\.[A-Za-z]{2,})?$/;
- const lines = value.split(/\n/).map(line => line.trim());
- let hasValidDomain = false;
-
- for (const line of lines) {
- // Skip empty lines
- if (!line) continue;
-
- // Extract domain part (before any //)
- const domainPart = line.split('//')[0].trim();
-
- // Skip if line is empty after removing comments
- if (!domainPart) continue;
-
- // Process each domain in the line (separated by comma or space)
- const domains = domainPart.split(/[,\s]+/).map(d => d.trim()).filter(d => d.length > 0);
-
- for (const domain of domains) {
- if (!domainRegex.test(domain)) {
- return _('Invalid domain format: %s. Enter domain without protocol').format(domain);
- }
- hasValidDomain = true;
- }
- }
-
- if (!hasValidDomain) {
- return _('At least one valid domain must be specified. Comments-only content is not allowed.');
- }
-
- return true;
- };
-
- o = s.taboption('basic', form.Flag, 'custom_local_domains_list_enabled', _('Local Domain Lists'), _('Use the list from the router filesystem'));
- o.default = '0';
- o.rmempty = false;
- o.ucisection = s.section;
-
- o = s.taboption('basic', form.DynamicList, 'custom_local_domains', _('Local Domain Lists Path'), _('Enter the list file path'));
- o.placeholder = '/path/file.lst';
- o.depends('custom_local_domains_list_enabled', '1');
- o.rmempty = false;
- o.ucisection = s.section;
- o.validate = function (section_id, value) {
- if (!value || value.length === 0) return true;
- const pathRegex = /^\/[a-zA-Z0-9_\-\/\.]+$/;
- if (!pathRegex.test(value)) {
- return _('Invalid path format. Path must start with "/" and contain valid characters');
- }
- return true;
- };
-
- o = s.taboption('basic', form.Flag, 'custom_download_domains_list_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, 'custom_download_domains', _('Remote Domain URLs'), _('Enter full URLs starting with http:// or https://'));
- o.placeholder = 'URL';
- o.depends('custom_download_domains_list_enabled', '1');
- o.rmempty = false;
- o.ucisection = s.section;
- o.validate = function (section_id, value) {
- if (!value || value.length === 0) return true;
- return validateUrl(value);
- };
-
- o = s.taboption('basic', form.ListValue, 'custom_subnets_list_enabled', _('User Subnet List Type'), _('Select how to add your custom subnets'));
- o.value('disabled', _('Disabled'));
- o.value('dynamic', _('Dynamic List'));
- o.value('text', _('Text List (comma/space/newline separated)'));
- o.default = 'disabled';
- o.rmempty = false;
- o.ucisection = s.section;
-
- o = s.taboption('basic', form.DynamicList, 'custom_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('custom_subnets_list_enabled', 'dynamic');
- o.rmempty = false;
- o.ucisection = s.section;
- o.validate = function (section_id, value) {
- if (!value || value.length === 0) return true;
- const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
- if (!subnetRegex.test(value)) return _('Invalid format. Use format: X.X.X.X or X.X.X.X/Y');
- const [ip, cidr] = value.split('/');
- const ipParts = ip.split('.');
- for (const part of ipParts) {
- const num = parseInt(part);
- if (num < 0 || num > 255) return _('IP address parts must be between 0 and 255');
- }
- if (cidr !== undefined) {
- const cidrNum = parseInt(cidr);
- if (cidrNum < 0 || cidrNum > 32) return _('CIDR must be between 0 and 32');
- }
- return true;
- };
-
- o = s.taboption('basic', form.TextValue, 'custom_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('custom_subnets_list_enabled', 'text');
- o.rows = 10;
- o.rmempty = false;
- o.ucisection = s.section;
- o.validate = function (section_id, value) {
- if (!value || value.length === 0) return true;
-
- const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
- const lines = value.split(/\n/).map(line => line.trim());
- let hasValidSubnet = false;
-
- for (const line of lines) {
- // Skip empty lines
- if (!line) continue;
-
- // Extract subnet part (before any //)
- const subnetPart = line.split('//')[0].trim();
-
- // Skip if line is empty after removing comments
- if (!subnetPart) continue;
-
- // Process each subnet in the line (separated by comma or space)
- const subnets = subnetPart.split(/[,\s]+/).map(s => s.trim()).filter(s => s.length > 0);
-
- for (const subnet of subnets) {
- if (!subnetRegex.test(subnet)) {
- return _('Invalid format: %s. Use format: X.X.X.X or X.X.X.X/Y').format(subnet);
- }
-
- const [ip, cidr] = subnet.split('/');
- const ipParts = ip.split('.');
- for (const part of ipParts) {
- const num = parseInt(part);
- if (num < 0 || num > 255) {
- return _('IP parts must be between 0 and 255 in: %s').format(subnet);
- }
- }
-
- if (cidr !== undefined) {
- const cidrNum = parseInt(cidr);
- if (cidrNum < 0 || cidrNum > 32) {
- return _('CIDR must be between 0 and 32 in: %s').format(subnet);
- }
- }
- hasValidSubnet = true;
- }
- }
-
- if (!hasValidSubnet) {
- return _('At least one valid subnet or IP must be specified. Comments-only content is not allowed.');
- }
-
- return true;
- };
-
- o = s.taboption('basic', form.Flag, 'custom_download_subnets_list_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, 'custom_download_subnets', _('Remote Subnet URLs'), _('Enter full URLs starting with http:// or https://'));
- o.placeholder = 'URL';
- o.depends('custom_download_subnets_list_enabled', '1');
- o.rmempty = false;
- o.ucisection = s.section;
- o.validate = function (section_id, value) {
- if (!value || value.length === 0) return true;
- return validateUrl(value);
- };
-
- 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) {
- if (!value || value.length === 0) return true;
- const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
- if (!ipRegex.test(value)) return _('Invalid IP format. Use format: X.X.X.X (like 192.168.1.1)');
- const ipParts = value.split('.');
- for (const part of ipParts) {
- const num = parseInt(part);
- if (num < 0 || num > 255) return _('IP address parts must be between 0 and 255');
- }
- return true;
- };
-}
-
-// Utility functions
-const 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, BUTTON_FEEDBACK_TIMEOUT);
- } catch (err) {
- ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message));
- }
- document.body.removeChild(textarea);
-};
-
-const validateUrl = (url, protocols = ['http:', 'https:']) => {
- try {
- const parsedUrl = new URL(url);
- if (!protocols.includes(parsedUrl.protocol)) {
- return _('URL must use one of the following protocols: ') + protocols.join(', ');
- }
- return true;
- } catch (e) {
- return _('Invalid URL format');
- }
-};
-
-// UI Helper functions
-const createModalContent = (title, content) => {
- return [
- 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', { style: 'margin: 0;' }, content)
- ]),
- E('div', {
- 'class': 'right',
- style: 'margin-top: 1em;'
- }, [
- E('button', {
- 'class': 'btn',
- 'click': ev => copyToClipboard('```txt\n' + content + '\n```', ev.target)
- }, _('Copy to Clipboard')),
- E('button', {
- 'class': 'btn',
- 'click': ui.hideModal
- }, _('Close'))
- ])
- ];
-};
-
-// Add IP masking function before showConfigModal
-const maskIP = (ip) => {
- if (!ip) return '';
- const parts = ip.split('.');
- if (parts.length !== 4) return ip;
- return ['XX', 'XX', 'XX', parts[3]].join('.');
-};
-
-const showConfigModal = async (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'))
- ])
- ]);
-
- 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') {
- const res = await safeExec('/usr/bin/podkop', [command]);
- formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
-
- try {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
-
- const response = await fetch(`https://${FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal });
- const data = await response.json();
- 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
- const bypassResponse = await fetch(`https://${FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal });
- const bypassData = await bypassResponse.json();
- const bypassResponse2 = await fetch(`https://${IP_CHECK_DOMAIN}/check`, { signal: controller.signal });
- const bypassData2 = await bypassResponse2.json();
-
- 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);
- }
- } else {
- const res = await safeExec('/usr/bin/podkop', [command]);
- 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));
- },
-
- createActionButton: function (config) {
- return this.createButton({
- label: config.label,
- additionalClass: `cbi-button-${config.type || ''}`,
- onClick: () => safeExec('/usr/bin/podkop', [config.action])
- .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])
- .then(() => config.reload && location.reload()),
- style: config.style
- });
- },
-
- createModalButton: function (config) {
- return this.createButton({
- label: config.label,
- onClick: () => showConfigModal(config.command, config.title),
- style: config.style
- });
- }
-};
-
-// Status Panel Factory
-const createStatusPanel = (title, status, buttons, extraData = {}) => {
- const headerContent = [
- E('strong', {}, _(title)),
- status && E('br'),
- status && E('span', {
- 'style': `color: ${title === 'Sing-box Status' ?
- (status.running && !status.enabled ? STATUS_COLORS.SUCCESS : STATUS_COLORS.ERROR) :
- title === 'Podkop Status' ?
- (status.enabled ? STATUS_COLORS.SUCCESS : STATUS_COLORS.ERROR) :
- (status.running ? STATUS_COLORS.SUCCESS : STATUS_COLORS.ERROR)
- }`
- }, [
- title === 'Sing-box Status' ?
- (status.running && !status.enabled ? '✔ running' : '✘ ' + status.status) :
- title === 'Podkop Status' ?
- (status.enabled ? '✔ Autostart enabled' : '✘ Autostart disabled') :
- (status.running ? '✔' : '✘') + ' ' + status.status
- ])
- ].filter(Boolean);
-
- return E('div', {
- 'class': 'panel',
- 'style': 'flex: 1; padding: 15px;'
- }, [
- E('div', { 'class': 'panel-heading' }, headerContent),
- E('div', {
- 'class': 'panel-body',
- 'style': 'display: flex; flex-direction: column; gap: 8px;'
- }, title === 'Podkop Status' ? [
- ButtonFactory.createActionButton({
- label: 'Restart Podkop',
- type: 'apply',
- action: 'restart',
- reload: true
- }),
- ButtonFactory.createActionButton({
- label: 'Stop Podkop',
- type: 'apply',
- action: 'stop',
- reload: true
- }),
- ButtonFactory.createInitActionButton({
- label: status.enabled ? 'Disable Autostart' : 'Enable Autostart',
- type: status.enabled ? 'remove' : 'apply',
- action: status.enabled ? 'disable' : 'enable',
- reload: true
- }),
- ButtonFactory.createModalButton({
- label: E('strong', _('Global check')),
- command: 'global_check',
- title: _('Global check')
- }),
- ButtonFactory.createModalButton({
- label: 'View Logs',
- command: 'check_logs',
- title: 'Podkop Logs'
- }),
- ButtonFactory.createModalButton({
- label: _('Update Lists'),
- command: 'list_update',
- title: _('Lists Update Results')
- })
- ] : title === _('FakeIP Status') ? [
- E('div', { style: 'margin-bottom: 5px;' }, [
- E('div', {}, [
- E('span', { style: `color: ${extraData.fakeipStatus?.color}` }, [
- extraData.fakeipStatus?.state === 'working' ? '✔' : extraData.fakeipStatus?.state === 'not_working' ? '✘' : '!',
- ' ',
- extraData.fakeipStatus?.state === 'working' ? _('works in browser') : _('not works in browser')
- ])
- ]),
- E('div', {}, [
- E('span', { style: `color: ${extraData.fakeipCLIStatus?.color}` }, [
- extraData.fakeipCLIStatus?.state === 'working' ? '✔' : extraData.fakeipCLIStatus?.state === 'not_working' ? '✘' : '!',
- ' ',
- extraData.fakeipCLIStatus?.state === 'working' ? _('works on router') : _('not works on router')
- ])
- ])
- ]),
- E('div', { style: 'margin-bottom: 5px;' }, [
- E('div', {}, [
- E('strong', {}, _('DNS Status')),
- E('br'),
- E('span', { style: `color: ${extraData.dnsStatus?.remote?.color}` }, [
- extraData.dnsStatus?.remote?.state === 'available' ? '✔' : extraData.dnsStatus?.remote?.state === 'unavailable' ? '✘' : '!',
- ' ',
- extraData.dnsStatus?.remote?.message
- ]),
- E('br'),
- E('span', { style: `color: ${extraData.dnsStatus?.local?.color}` }, [
- extraData.dnsStatus?.local?.state === 'available' ? '✔' : extraData.dnsStatus?.local?.state === 'unavailable' ? '✘' : '!',
- ' ',
- extraData.dnsStatus?.local?.message
- ])
- ])
- ]),
- E('div', { style: 'margin-bottom: 5px;' }, [
- E('div', {}, [
- E('strong', {}, extraData.configName),
- E('br'),
- E('span', { style: `color: ${extraData.bypassStatus?.color}` }, [
- extraData.bypassStatus?.state === 'working' ? '✔' : extraData.bypassStatus?.state === 'not_working' ? '✘' : '!',
- ' ',
- extraData.bypassStatus?.message
- ])
- ])
- ])
- ] : buttons)
- ]);
-};
-
-// Update the status section creation
-let createStatusSection = function (podkopStatus, singboxStatus, podkop, luci, singbox, system, fakeipStatus, fakeipCLIStatus, dnsStatus, bypassStatus, configName) {
- return E('div', { 'class': 'cbi-section' }, [
- E('div', { 'class': 'table', style: 'display: flex; gap: 20px;' }, [
- // Podkop Status Panel
- createStatusPanel('Podkop Status', podkopStatus, [
- ButtonFactory.createActionButton({
- label: 'Restart Podkop',
- type: 'apply',
- action: 'restart',
- reload: true
- }),
- ButtonFactory.createActionButton({
- label: 'Stop Podkop',
- type: 'apply',
- action: 'stop',
- reload: true
- }),
- ButtonFactory.createInitActionButton({
- label: podkopStatus.enabled ? 'Disable Autostart' : 'Enable Autostart',
- type: podkopStatus.enabled ? 'remove' : 'apply',
- action: podkopStatus.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
- createStatusPanel('Sing-box Status', singboxStatus, [
- 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
- createStatusPanel(_('FakeIP Status'), null, null, {
- fakeipStatus,
- fakeipCLIStatus,
- dnsStatus,
- bypassStatus,
- configName
- }),
-
- // Version Information Panel
- createStatusPanel(_('Version Information'), null, [
- E('div', { 'style': 'margin-top: 10px; font-family: monospace; white-space: pre-wrap;' }, [
- E('strong', {}, _('Podkop: ')), podkop.stdout ? podkop.stdout.trim() : _('Unknown'), '\n',
- E('strong', {}, _('LuCI App: ')), luci.stdout ? luci.stdout.trim() : _('Unknown'), '\n',
- E('strong', {}, _('Sing-box: ')), singbox.stdout ? singbox.stdout.trim() : _('Unknown'), '\n',
- E('strong', {}, _('OpenWrt Version: ')), system.stdout ? system.stdout.split('\n')[1].trim() : _('Unknown'), '\n',
- E('strong', {}, _('Device Model: ')), system.stdout ? system.stdout.split('\n')[4].trim() : _('Unknown')
- ])
- ])
- ])
- ]);
-};
-
-function checkDNSAvailability() {
- const createStatus = (state, message, color) => ({
- state,
- message: _(message),
- color: STATUS_COLORS[color]
- });
-
- return new Promise(async (resolve) => {
- try {
- const dnsStatusResult = await safeExec('/usr/bin/podkop', ['check_dns_available']);
- 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 resolve({
- remote: createStatus('error', 'DNS check error', 'WARNING'),
- local: createStatus('error', 'DNS check error', 'WARNING')
- });
- }
- });
-}
-
-async function getPodkopErrors() {
- try {
- const result = await safeExec('/usr/bin/podkop', ['check_logs']);
- if (!result || !result.stdout) return [];
-
- const logs = result.stdout.split('\n');
- const errors = logs.filter(log =>
- // log.includes('saved for future filters') ||
- log.includes('[critical]')
- );
-
- console.log('Found errors:', errors);
- return errors;
- } catch (error) {
- console.error('Error getting podkop logs:', error);
- return [];
- }
-}
-
-let errorPollTimer = null;
-let lastErrorsSet = new Set();
-let isInitialCheck = true;
-
-function showErrorNotification(error, isMultiple = false) {
- const notificationContent = E('div', { 'class': 'alert-message error' }, [
- E('pre', { 'class': 'error-log' }, error)
- ]);
-
- ui.addNotification(null, notificationContent);
-}
-
-function startErrorPolling() {
- if (errorPollTimer) {
- clearInterval(errorPollTimer);
- }
-
- async function checkErrors() {
- const result = await safeExec('/usr/bin/podkop', ['check_logs']);
- if (!result || !result.stdout) return;
-
- const logs = result.stdout;
-
- const errorLines = logs.split('\n').filter(line =>
- // line.includes('saved for future filters') ||
- line.includes('[critical]')
- );
-
- if (errorLines.length > 0) {
- const currentErrors = new Set(errorLines);
-
- if (isInitialCheck) {
- if (errorLines.length > 0) {
- showErrorNotification(errorLines.join('\n'), true);
- }
- isInitialCheck = false;
- } else {
- const newErrors = [...currentErrors].filter(error => !lastErrorsSet.has(error));
-
- newErrors.forEach(error => {
- showErrorNotification(error, false);
- });
- }
- lastErrorsSet = currentErrors;
- }
- }
-
- checkErrors();
-
- errorPollTimer = setInterval(checkErrors, ERROR_POLL_INTERVAL);
-}
-
-function stopErrorPolling() {
- if (errorPollTimer) {
- clearInterval(errorPollTimer);
- errorPollTimer = null;
- }
-}
-
-async function checkFakeIP() {
- const createStatus = (state, message, color) => ({
- state,
- message: _(message),
- color: STATUS_COLORS[color]
- });
-
- try {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
-
- try {
- const response = await fetch(`https://${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');
- }
-}
-
-async function checkFakeIPCLI() {
- const createStatus = (state, message, color) => ({
- state,
- message: _(message),
- color: STATUS_COLORS[color]
- });
-
- try {
- const singboxStatusResult = await safeExec('/usr/bin/podkop', ['get_sing_box_status']);
- const singboxStatus = JSON.parse(singboxStatusResult.stdout || '{"running":0,"dns_configured":0}');
-
- if (!singboxStatus.running) {
- return createStatus('not_working', 'sing-box not running', 'ERROR');
- }
-
- const result = await safeExec('nslookup', ['-timeout=2', FAKEIP_CHECK_DOMAIN, '127.0.0.42']);
-
- if (result.stdout && result.stdout.includes('198.18')) {
- return createStatus('working', 'working on router', 'SUCCESS');
- } else {
- return createStatus('not_working', 'not working on router', 'ERROR');
- }
- } catch (error) {
- return createStatus('error', 'CLI check error', 'WARNING');
- }
-}
+'require view.podkop.networkUtils as networkUtils';
+'require view.podkop.configSection as config';
+'require view.podkop.diagnosticSection as diagnostic';
+'require view.podkop.additionalSection as additional';
return view.extend({
async render() {
@@ -1239,456 +38,7 @@ return view.extend({
// Main Section
const mainSection = m.section(form.TypedSection, 'main');
mainSection.anonymous = true;
- createConfigSection(mainSection, m, network);
-
- // Additional Settings Tab (main section)
- let o = mainSection.tab('additional', _('Additional Settings'));
-
- o = mainSection.taboption('additional', form.Flag, 'yacd', _('Yacd enable'), _('openwrt.lan:9090/ui'));
- o.default = '0';
- o.rmempty = false;
- o.ucisection = 'main';
-
- o = mainSection.taboption('additional', form.Flag, 'exclude_ntp', _('Exclude NTP'), _('For issues with open connections sing-box'));
- 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'));
- o.value('1h', _('Every hour'));
- o.value('3h', _('Every 3 hours'));
- o.value('12h', _('Every 12 hours'));
- o.value('1d', _('Every day'));
- o.value('3d', _('Every 3 days'));
- 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 = 'doh';
- o.rmempty = false;
- o.ucisection = 'main';
-
- o = mainSection.taboption('additional', form.Value, 'dns_server', _('DNS Server'), _('Select or enter DNS server address'));
- o.value('1.1.1.1', 'Cloudflare (1.1.1.1)');
- o.value('8.8.8.8', 'Google (8.8.8.8)');
- o.value('9.9.9.9', 'Quad9 (9.9.9.9)');
- o.value('dns.adguard-dns.com', 'AdGuard Default (dns.adguard-dns.com)');
- o.value('unfiltered.adguard-dns.com', 'AdGuard Unfiltered (unfiltered.adguard-dns.com)');
- o.value('family.adguard-dns.com', 'AdGuard Family (family.adguard-dns.com)');
- o.default = '8.8.8.8';
- o.rmempty = false;
- o.ucisection = 'main';
- o.validate = function (section_id, value) {
- if (!value) {
- return _('DNS server address cannot be empty');
- }
-
- const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
- if (ipRegex.test(value)) {
- const parts = value.split('.');
- for (const part of parts) {
- const num = parseInt(part);
- if (num < 0 || num > 255) {
- return _('IP address parts must be between 0 and 255');
- }
- }
- return true;
- }
-
- const domainRegex = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(\/[^\s]*)?$/;
- if (!domainRegex.test(value)) {
- return _('Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH');
- }
-
- return true;
- };
-
- o = mainSection.taboption('additional', form.Value, 'dns_rewrite_ttl', _('DNS Rewrite TTL'), _('Time in seconds for DNS record caching (default: 60)'));
- o.default = '60';
- o.rmempty = false;
- o.ucisection = 'main';
- o.validate = function (section_id, value) {
- if (!value) {
- return _('TTL value cannot be empty');
- }
-
- const ttl = parseInt(value);
- if (isNaN(ttl) || ttl < 0) {
- return _('TTL must be a positive number');
- }
-
- return true;
- };
-
- o = mainSection.taboption('additional', form.Value, 'cache_file', _('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/cache.db', 'RAM (/tmp/cache.db)');
- o.value('/usr/share/sing-box/cache.db', 'Flash (/usr/share/sing-box/cache.db)');
- o.default = '/tmp/cache.db';
- o.rmempty = false;
- o.ucisection = 'main';
- o.validate = function (section_id, value) {
- if (!value) {
- return _('Cache file path cannot be empty');
- }
-
- if (!value.startsWith('/')) {
- return _('Path must be absolute (start with /)');
- }
-
- if (!value.endsWith('cache.db')) {
- return _('Path must end with cache.db');
- }
-
- const parts = value.split('/').filter(Boolean);
- if (parts.length < 2) {
- return _('Path must contain at least one directory (like /tmp/cache.db)');
- }
-
- return true;
- };
-
- o = mainSection.taboption('additional', form.MultiValue, 'iface', _('Source Network Interface'), _('Select the network interface from which the traffic will originate'));
- o.ucisection = 'main';
- o.default = 'br-lan';
- o.load = function (section_id) {
- return getNetworkInterfaces(this, section_id, ['wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan']).then(() => {
- return this.super('load', section_id);
- });
- };
-
- 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.MultiValue, 'restart_ifaces', _('Interface for monitoring'), _('Select the WAN interfaces to be monitored'));
- o.ucisection = 'main';
- o.depends('mon_restart_ifaces', '1');
- o.load = function (section_id) {
- return getNetworkNetworks(this, section_id, ['lan', 'loopback']).then(() => {
- return this.super('load', section_id);
- });
- };
-
- o = mainSection.taboption('additional', form.Flag, 'dont_touch_dhcp', _('Dont touch my DHCP!'), _('Podkop will not change the DHCP config'));
- o.default = '0';
- o.rmempty = false;
- o.ucisection = 'main';
-
- o = mainSection.taboption('additional', form.Flag, 'detour', _('Proxy download of lists'), _('Downloading all lists via main Proxy/VPN'));
- o.default = '0';
- o.rmempty = false;
- o.ucisection = 'main';
-
- // Extra IPs and exclusions (main section)
- o = mainSection.taboption('basic', form.Flag, 'exclude_from_ip_enabled', _('IP for exclusion'), _('Specify local IP addresses that will never use the configured route'));
- o.default = '0';
- o.rmempty = false;
- o.ucisection = 'main';
-
- o = mainSection.taboption('basic', form.DynamicList, 'exclude_traffic_ip', _('Local IPs'), _('Enter valid IPv4 addresses'));
- o.placeholder = 'IP';
- o.depends('exclude_from_ip_enabled', '1');
- o.rmempty = false;
- o.ucisection = 'main';
- o.validate = function (section_id, value) {
- if (!value || value.length === 0) return true;
- const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
- if (!ipRegex.test(value)) return _('Invalid IP format. Use format: X.X.X.X (like 192.168.1.1)');
- const ipParts = value.split('.');
- for (const part of ipParts) {
- const num = parseInt(part);
- if (num < 0 || num > 255) return _('IP address parts must be between 0 and 255');
- }
- return true;
- };
-
- o = mainSection.taboption('basic', form.Flag, 'socks5', _('Mixed enable'), _('Browser port: 2080'));
- o.default = '0';
- o.rmempty = false;
- o.ucisection = 'main';
-
- // Diagnostics Tab (main section)
- o = mainSection.tab('diagnostics', _('Diagnostics'));
-
- o = mainSection.taboption('diagnostics', form.DummyValue, '_status');
- o.rawhtml = true;
- o.cfgvalue = () => E('div', {
- id: 'diagnostics-status',
- 'style': 'cursor: pointer;'
- }, _('Click to load diagnostics...'));
-
- let diagnosticsUpdateTimer = null;
-
- function startDiagnosticsUpdates() {
- if (diagnosticsUpdateTimer) {
- clearInterval(diagnosticsUpdateTimer);
- }
-
- const container = document.getElementById('diagnostics-status');
- if (container) {
- container.innerHTML = _('Loading diagnostics...');
- }
-
- updateDiagnostics();
- diagnosticsUpdateTimer = setInterval(updateDiagnostics, DIAGNOSTICS_UPDATE_INTERVAL);
- }
-
- function stopDiagnosticsUpdates() {
- if (diagnosticsUpdateTimer) {
- clearInterval(diagnosticsUpdateTimer);
- diagnosticsUpdateTimer = null;
- }
-
- // Reset the loading state when stopping updates
- const container = document.getElementById('diagnostics-status');
- if (container) {
- container.removeAttribute('data-loading');
- }
- }
-
- function checkBypass() {
- const createStatus = (state, message, color) => ({
- state,
- message: _(message),
- color: STATUS_COLORS[color]
- });
-
- return new Promise(async (resolve) => {
- try {
- let configMode = 'proxy'; // Default fallback
- try {
- const formData = document.querySelector('form.map-podkop');
- if (formData) {
- const modeSelect = formData.querySelector('select[name="cbid.podkop.main.mode"]');
- if (modeSelect && modeSelect.value) {
- configMode = modeSelect.value;
- }
- }
- } catch (formError) {
- console.error('Error getting mode from form:', formError);
- }
-
- // Check if sing-box is running
- const singboxStatusResult = await safeExec('/usr/bin/podkop', ['get_sing_box_status']);
- const singboxStatus = JSON.parse(singboxStatusResult.stdout || '{"running":0,"dns_configured":0}');
-
- if (!singboxStatus.running) {
- return resolve(createStatus('not_working', `${configMode} not running`, 'ERROR'));
- }
-
- // Fetch IP from first endpoint
- let ip1 = null;
- try {
- const controller1 = new AbortController();
- const timeoutId1 = setTimeout(() => controller1.abort(), FETCH_TIMEOUT);
-
- const response1 = await fetch(`https://${FAKEIP_CHECK_DOMAIN}/check`, { signal: controller1.signal });
- const data1 = await response1.json();
- clearTimeout(timeoutId1);
-
- ip1 = data1.IP;
- } catch (error) {
- return resolve(createStatus('error', 'First endpoint check failed', 'WARNING'));
- }
-
- // Fetch IP from second endpoint
- let ip2 = null;
- try {
- const controller2 = new AbortController();
- const timeoutId2 = setTimeout(() => controller2.abort(), FETCH_TIMEOUT);
-
- const response2 = await fetch(`https://${IP_CHECK_DOMAIN}/check`, { signal: controller2.signal });
- const data2 = await response2.json();
- clearTimeout(timeoutId2);
-
- ip2 = data2.IP;
- } catch (error) {
- return resolve(createStatus('not_working', `${configMode} not working`, 'ERROR'));
- }
-
- // Compare IPs
- if (ip1 && ip2) {
- if (ip1 !== ip2) {
- return resolve(createStatus('working', `${configMode} working correctly`, 'SUCCESS'));
- } else {
- return resolve(createStatus('not_working', `${configMode} routing incorrect`, 'ERROR'));
- }
- } else {
- return resolve(createStatus('error', 'IP comparison failed', 'WARNING'));
- }
- } catch (error) {
- return resolve(createStatus('error', 'Bypass check error', 'WARNING'));
- }
- });
- }
-
- async function updateDiagnostics() {
- try {
- const results = {
- podkopStatus: null,
- singboxStatus: null,
- podkop: null,
- luci: null,
- singbox: null,
- system: null,
- fakeipStatus: null,
- fakeipCLIStatus: null,
- dnsStatus: null,
- bypassStatus: null
- };
-
- // Perform all checks independently of each other
- const checks = [
- safeExec('/usr/bin/podkop', ['get_status'])
- .then(result => results.podkopStatus = result)
- .catch(() => results.podkopStatus = { stdout: '{"enabled":0,"status":"error"}' }),
-
- safeExec('/usr/bin/podkop', ['get_sing_box_status'])
- .then(result => results.singboxStatus = result)
- .catch(() => results.singboxStatus = { stdout: '{"running":0,"enabled":0,"status":"error"}' }),
-
- safeExec('/usr/bin/podkop', ['show_version'])
- .then(result => results.podkop = result)
- .catch(() => results.podkop = { stdout: 'error' }),
-
- safeExec('/usr/bin/podkop', ['show_luci_version'])
- .then(result => results.luci = result)
- .catch(() => results.luci = { stdout: 'error' }),
-
- safeExec('/usr/bin/podkop', ['show_sing_box_version'])
- .then(result => results.singbox = result)
- .catch(() => results.singbox = { stdout: 'error' }),
-
- safeExec('/usr/bin/podkop', ['show_system_info'])
- .then(result => results.system = result)
- .catch(() => results.system = { stdout: 'error' }),
-
- checkFakeIP()
- .then(result => results.fakeipStatus = result)
- .catch(() => results.fakeipStatus = { state: 'error', message: 'check error', color: STATUS_COLORS.WARNING }),
-
- checkFakeIPCLI()
- .then(result => results.fakeipCLIStatus = result)
- .catch(() => results.fakeipCLIStatus = { state: 'error', message: 'check error', color: STATUS_COLORS.WARNING }),
-
- checkDNSAvailability()
- .then(result => results.dnsStatus = result)
- .catch(() => results.dnsStatus = {
- remote: createStatus('error', 'DNS check error', 'WARNING'),
- local: createStatus('error', 'DNS check error', 'WARNING')
- }),
-
- checkBypass()
- .then(result => results.bypassStatus = result)
- .catch(() => results.bypassStatus = { state: 'error', message: 'check error', color: STATUS_COLORS.WARNING })
- ];
-
- // Waiting for all the checks to be completed
- await Promise.allSettled(checks);
-
- const container = document.getElementById('diagnostics-status');
- if (!container) return;
-
- let configName = _('Main config');
- try {
- 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 (e) {
- console.error('Error getting config name from UCI:', e);
- }
-
- const parsedPodkopStatus = JSON.parse(results.podkopStatus.stdout || '{"enabled":0,"status":"error"}');
- const parsedSingboxStatus = JSON.parse(results.singboxStatus.stdout || '{"running":0,"enabled":0,"status":"error"}');
-
- const statusSection = createStatusSection(
- parsedPodkopStatus,
- parsedSingboxStatus,
- results.podkop,
- results.luci,
- results.singbox,
- results.system,
- results.fakeipStatus,
- results.fakeipCLIStatus,
- results.dnsStatus,
- results.bypassStatus,
- configName
- );
-
- container.innerHTML = '';
- container.appendChild(statusSection);
-
- // Updating individual status items
- const updateStatusElement = (elementId, status, template) => {
- const element = document.getElementById(elementId);
- if (element) {
- element.innerHTML = template(status);
- }
- };
-
- updateStatusElement('fakeip-status', results.fakeipStatus,
- status => E('span', { 'style': `color: ${status.color}` }, [
- status.state === 'working' ? '✔ ' : status.state === 'not_working' ? '✘ ' : '! ',
- status.message
- ]).outerHTML
- );
-
- updateStatusElement('fakeip-cli-status', results.fakeipCLIStatus,
- status => E('span', { 'style': `color: ${status.color}` }, [
- status.state === 'working' ? '✔ ' : status.state === 'not_working' ? '✘ ' : '! ',
- status.message
- ]).outerHTML
- );
-
- updateStatusElement('dns-remote-status', results.dnsStatus.remote,
- status => E('span', { 'style': `color: ${status.color}` }, [
- status.state === 'available' ? '✔ ' : status.state === 'unavailable' ? '✘ ' : '! ',
- status.message
- ]).outerHTML
- );
-
- updateStatusElement('dns-local-status', results.dnsStatus.local,
- status => E('span', { 'style': `color: ${status.color}` }, [
- status.state === 'available' ? '✔ ' : status.state === 'unavailable' ? '✘ ' : '! ',
- status.message
- ]).outerHTML
- );
-
- } catch (e) {
- const container = document.getElementById('diagnostics-status');
- if (container) {
- container.innerHTML = E('div', { 'class': 'alert-message warning' }, [
- E('strong', {}, _('Error loading diagnostics')),
- E('br'),
- E('pre', {}, e.toString())
- ]).outerHTML;
- }
- }
- }
+ config.createConfigSection(mainSection, m, network);
// Extra Section
const extraSection = m.section(form.TypedSection, 'extra', _('Extra configurations'));
@@ -1696,70 +46,15 @@ return view.extend({
extraSection.addremove = true;
extraSection.addbtntitle = _('Add Section');
extraSection.multiple = true;
- createConfigSection(extraSection, m, network);
+ config.createConfigSection(extraSection, m, network);
- const map_promise = m.render().then(node => {
- const titleDiv = E('h2', { 'class': 'cbi-map-title' }, _('Podkop'));
- node.insertBefore(titleDiv, node.firstChild);
+ // Additional Settings Tab (main section)
+ additional.createAdditionalSection(mainSection, network);
- document.addEventListener('visibilitychange', function () {
- const diagnosticsContainer = document.getElementById('diagnostics-status');
- if (document.hidden) {
- stopDiagnosticsUpdates();
- stopErrorPolling();
- } else if (diagnosticsContainer && diagnosticsContainer.hasAttribute('data-loading')) {
- startDiagnosticsUpdates();
- startErrorPolling();
- }
- });
+ // Diagnostics Tab (main section)
+ diagnostic.createDiagnosticsSection(mainSection);
- setTimeout(() => {
- const diagnosticsContainer = document.getElementById('diagnostics-status');
- if (diagnosticsContainer) {
- diagnosticsContainer.addEventListener('click', function () {
- if (!this.hasAttribute('data-loading')) {
- this.setAttribute('data-loading', 'true');
- startDiagnosticsUpdates();
- startErrorPolling();
- }
- });
- }
-
- 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');
- if (container && !container.hasAttribute('data-loading')) {
- container.setAttribute('data-loading', 'true');
- startDiagnosticsUpdates();
- startErrorPolling();
- }
- } else {
- stopDiagnosticsUpdates();
- stopErrorPolling();
- }
- }
- });
-
- const activeTab = tabs[0].querySelector('.cbi-tab[data-tab="diagnostics"]');
- if (activeTab) {
- const container = document.getElementById('diagnostics-status');
- if (container && !container.hasAttribute('data-loading')) {
- container.setAttribute('data-loading', 'true');
- startDiagnosticsUpdates();
- startErrorPolling();
- }
- }
- }
- }, DIAGNOSTICS_INITIAL_DELAY);
-
- node.classList.add('fade-in');
- return node;
- });
+ const map_promise = m.render().then(node => diagnostic.setupDiagnosticsEventHandlers(node));
return map_promise;
}