diff --git a/fe-app-podkop/src/styles.ts b/fe-app-podkop/src/styles.ts index b135ef5..6c3f706 100644 --- a/fe-app-podkop/src/styles.ts +++ b/fe-app-podkop/src/styles.ts @@ -24,10 +24,30 @@ export const GlobalStyles = ` display: none; } -#cbi-podkop-main-_status > div { +#cbi-podkop-dashboard-_mount_node > div { width: 100%; } +#cbi-podkop-dashboard > h3 { + display: none; +} + +#cbi-podkop-settings > h3 { + display: none; +} + +#cbi-podkop-section > h3:nth-child(1) { + display: none; +} + +.cbi-section-remove { + margin-bottom: -32px; +} + +.cbi-value { + margin-bottom: 20px !important; +} + /* Dashboard styles */ .pdk_dashboard-page { diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js deleted file mode 100644 index f06ae90..0000000 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js +++ /dev/null @@ -1,362 +0,0 @@ -'use strict'; -'require form'; -'require baseclass'; -'require tools.widgets as widgets'; -'require view.podkop.main as main'; - -function createAdditionalSection(mainSection) { - let o = mainSection.tab('additional', _('Additional Settings')); - - o = mainSection.taboption( - 'additional', - form.Flag, - 'yacd', - _('Yacd enable'), - `${main.getClashUIUrl()}`, - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'additional', - form.Flag, - 'exclude_ntp', - _('Exclude NTP'), - _('Allows you to exclude NTP protocol traffic from the tunnel'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'additional', - form.Flag, - 'quic_disable', - _('QUIC disable'), - _('For issues with the video stream'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'additional', - form.ListValue, - 'update_interval', - _('List Update Frequency'), - _('Select how often the lists will be updated'), - ); - Object.entries(main.UPDATE_INTERVAL_OPTIONS).forEach(([key, label]) => { - o.value(key, _(label)); - }); - o.default = '1d'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'additional', - form.ListValue, - 'dns_type', - _('DNS Protocol Type'), - _('Select DNS protocol to use'), - ); - o.value('doh', _('DNS over HTTPS (DoH)')); - o.value('dot', _('DNS over TLS (DoT)')); - o.value('udp', _('UDP (Unprotected DNS)')); - o.default = 'udp'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'additional', - form.Value, - 'dns_server', - _('DNS Server'), - _('Select or enter DNS server address'), - ); - Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => { - o.value(key, _(label)); - }); - o.default = '8.8.8.8'; - o.rmempty = false; - o.ucisection = 'main'; - o.validate = function (section_id, value) { - const validation = main.validateDNS(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = mainSection.taboption( - 'additional', - form.Value, - 'bootstrap_dns_server', - _('Bootstrap DNS server'), - _( - 'The DNS server used to look up the IP address of an upstream DNS server', - ), - ); - Object.entries(main.BOOTSTRAP_DNS_SERVER_OPTIONS).forEach(([key, label]) => { - o.value(key, _(label)); - }); - o.default = '77.88.8.8'; - o.rmempty = false; - o.ucisection = 'main'; - o.validate = function (section_id, value) { - const validation = main.validateDNS(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = mainSection.taboption( - 'additional', - form.Value, - 'dns_rewrite_ttl', - _('DNS Rewrite TTL'), - _('Time in seconds for DNS record caching (default: 60)'), - ); - o.default = '60'; - o.rmempty = false; - o.ucisection = 'main'; - o.validate = function (section_id, value) { - if (!value) { - return _('TTL value cannot be empty'); - } - - const ttl = parseInt(value); - if (isNaN(ttl) || ttl < 0) { - return _('TTL must be a positive number'); - } - - return true; - }; - - o = mainSection.taboption( - 'additional', - form.ListValue, - 'config_path', - _('Config File Path'), - _( - 'Select path for sing-box config file. Change this ONLY if you know what you are doing', - ), - ); - o.value('/etc/sing-box/config.json', 'Flash (/etc/sing-box/config.json)'); - o.value('/tmp/sing-box/config.json', 'RAM (/tmp/sing-box/config.json)'); - o.default = '/etc/sing-box/config.json'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'additional', - form.Value, - 'cache_path', - _('Cache File Path'), - _( - 'Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing', - ), - ); - o.value('/tmp/sing-box/cache.db', 'RAM (/tmp/sing-box/cache.db)'); - o.value( - '/usr/share/sing-box/cache.db', - 'Flash (/usr/share/sing-box/cache.db)', - ); - o.default = '/tmp/sing-box/cache.db'; - o.rmempty = false; - o.ucisection = 'main'; - o.validate = function (section_id, value) { - if (!value) { - return _('Cache file path cannot be empty'); - } - - if (!value.startsWith('/')) { - return _('Path must be absolute (start with /)'); - } - - if (!value.endsWith('cache.db')) { - return _('Path must end with cache.db'); - } - - const parts = value.split('/').filter(Boolean); - if (parts.length < 2) { - return _('Path must contain at least one directory (like /tmp/cache.db)'); - } - - return true; - }; - - o = mainSection.taboption( - 'additional', - widgets.DeviceSelect, - 'iface', - _('Source Network Interface'), - _('Select the network interface from which the traffic will originate'), - ); - o.ucisection = 'main'; - o.default = 'br-lan'; - o.noaliases = true; - o.nobridges = false; - o.noinactive = false; - o.multiple = true; - o.filter = function (section_id, value) { - // Block specific interface names from being selectable - const blocked = ['wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan']; - if (blocked.includes(value)) { - return false; - } - - // Try to find the device object by its name - const device = this.devices.find((dev) => dev.getName() === value); - - // If no device is found, allow the value - if (!device) { - return true; - } - - // Check the type of the device - const type = device.getType(); - - // Consider any Wi-Fi / wireless / wlan device as invalid - const isWireless = - type === 'wifi' || type === 'wireless' || type.includes('wlan'); - - // Allow only non-wireless devices - return !isWireless; - }; - - o = mainSection.taboption( - 'additional', - form.Flag, - 'mon_restart_ifaces', - _('Interface monitoring'), - _('Interface monitoring for bad WAN'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'additional', - widgets.NetworkSelect, - 'restart_ifaces', - _('Interface for monitoring'), - _('Select the WAN interfaces to be monitored'), - ); - o.ucisection = 'main'; - o.depends('mon_restart_ifaces', '1'); - o.multiple = true; - o.filter = function (section_id, value) { - // Reject if the value is in the blocked list ['lan', 'loopback'] - if (['lan', 'loopback'].includes(value)) { - return false; - } - - // Reject if the value starts with '@' (means it's an alias/reference) - if (value.startsWith('@')) { - return false; - } - - // Otherwise allow it - return true; - }; - - o = mainSection.taboption( - 'additional', - form.Value, - 'procd_reload_delay', - _('Interface Monitoring Delay'), - _('Delay in milliseconds before reloading podkop after interface UP'), - ); - o.ucisection = 'main'; - o.depends('mon_restart_ifaces', '1'); - o.default = '2000'; - o.rmempty = false; - o.validate = function (section_id, value) { - if (!value) { - return _('Delay value cannot be empty'); - } - return true; - }; - - o = mainSection.taboption( - 'additional', - form.Flag, - 'dont_touch_dhcp', - _('Dont touch my DHCP!'), - _('Podkop will not change the DHCP config'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'additional', - form.Flag, - 'detour', - _('Proxy download of lists'), - _('Downloading all lists via main Proxy/VPN'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; - - // Extra IPs and exclusions (main section) - o = mainSection.taboption( - 'basic', - form.Flag, - 'exclude_from_ip_enabled', - _('IP for exclusion'), - _('Specify local IP addresses that will never use the configured route'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; - - o = mainSection.taboption( - 'basic', - form.DynamicList, - 'exclude_traffic_ip', - _('Local IPs'), - _('Enter valid IPv4 addresses'), - ); - o.placeholder = 'IP'; - o.depends('exclude_from_ip_enabled', '1'); - o.rmempty = false; - o.ucisection = 'main'; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateIPV4(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = mainSection.taboption( - 'basic', - form.Flag, - 'socks5', - _('Mixed enable'), - _('Browser port: 2080'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = 'main'; -} - -return baseclass.extend({ - createAdditionalSection, -}); 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 deleted file mode 100644 index 64c1134..0000000 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js +++ /dev/null @@ -1,779 +0,0 @@ -'use strict'; -'require baseclass'; -'require form'; -'require ui'; -'require network'; -'require view.podkop.main as main'; -'require tools.widgets as widgets'; - -function createConfigSection(section) { - const s = section; - - let o = s.tab('basic', _('Basic Settings')); - - o = s.taboption( - 'basic', - form.ListValue, - 'mode', - _('Connection Type'), - _('Select between VPN and Proxy connection methods for traffic routing'), - ); - o.value('proxy', 'Proxy'); - o.value('vpn', 'VPN'); - o.value('block', 'Block'); - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.ListValue, - 'proxy_config_type', - _('Configuration Type'), - _('Select how to configure the proxy'), - ); - o.value('url', _('Connection URL')); - o.value('outbound', _('Outbound Config')); - o.value('urltest', _('URLTest')); - o.default = 'url'; - o.depends('mode', 'proxy'); - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.TextValue, - 'proxy_string', - _('Proxy Configuration URL'), - '', - ); - o.depends('proxy_config_type', 'url'); - o.rows = 5; - // Enable soft wrapping for multi-line proxy URLs (e.g., for URLTest proxy links) - o.wrap = 'soft'; - // Render as a textarea to allow multiple proxy URLs/configs - o.textarea = true; - o.rmempty = false; - o.ucisection = s.section; - o.sectionDescriptions = new Map(); - o.placeholder = - 'vless://uuid@server:port?type=tcp&security=tls#main\n// backup ss://method:pass@server:port\n// backup2 vless://uuid@server:port?type=grpc&security=reality#alt\n// backup3 trojan://04agAQapcl@127.0.0.1:33641?type=tcp&security=none#trojan-tcp-none'; - - o.renderWidget = function (section_id, option_index, cfgvalue) { - const original = form.TextValue.prototype.renderWidget.apply(this, [ - section_id, - option_index, - cfgvalue, - ]); - const container = E('div', {}); - container.appendChild(original); - - if (cfgvalue) { - try { - const activeConfig = cfgvalue - .split('\n') - .map((line) => line.trim()) - .find((line) => line && !line.startsWith('//')); - - if (activeConfig) { - if (activeConfig.includes('#')) { - const label = activeConfig.split('#').pop(); - if (label && label.trim()) { - const decodedLabel = decodeURIComponent(label); - const descDiv = E( - 'div', - { class: 'cbi-value-description' }, - _('Current config: ') + decodedLabel, - ); - container.appendChild(descDiv); - } else { - const descDiv = E( - 'div', - { class: 'cbi-value-description' }, - _('Config without description'), - ); - container.appendChild(descDiv); - } - } else { - const descDiv = E( - 'div', - { class: 'cbi-value-description' }, - _('Config without description'), - ); - container.appendChild(descDiv); - } - } - } catch (e) { - console.error('Error parsing config label:', e); - const descDiv = E( - 'div', - { class: 'cbi-value-description' }, - _('Config without description'), - ); - container.appendChild(descDiv); - } - } else { - const defaultDesc = E( - 'div', - { class: 'cbi-value-description' }, - _( - 'Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs', - ), - ); - container.appendChild(defaultDesc); - } - - return container; - }; - - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - try { - const activeConfigs = main.splitProxyString(value); - - if (!activeConfigs.length) { - return _( - 'No active configuration found. One configuration is required.', - ); - } - - if (activeConfigs.length > 1) { - return _( - 'Multiply active configurations found. Please leave one configuration.', - ); - } - - const validation = main.validateProxyUrl(activeConfigs[0]); - - if (validation.valid) { - return true; - } - - return validation.message; - } catch (e) { - return `${_('Invalid URL format:')} ${e?.message}`; - } - }; - - o = s.taboption( - 'basic', - form.TextValue, - 'outbound_json', - _('Outbound Configuration'), - _('Enter complete outbound configuration in JSON format'), - ); - o.depends('proxy_config_type', 'outbound'); - o.rows = 10; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateOutboundJson(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.DynamicList, - 'urltest_proxy_links', - _('URLTest Proxy Links'), - ); - o.depends('proxy_config_type', 'urltest'); - o.placeholder = 'vless://, ss://, trojan:// links'; - o.rmempty = false; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateProxyUrl(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.Flag, - 'ss_uot', - _('Shadowsocks UDP over TCP'), - _('Apply for SS2022'), - ); - o.default = '0'; - o.depends('mode', 'proxy'); - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - widgets.DeviceSelect, - 'interface', - _('Network Interface'), - _('Select network interface for VPN connection'), - ); - o.depends('mode', 'vpn'); - o.ucisection = s.section; - o.noaliases = true; - o.nobridges = false; - o.noinactive = false; - o.filter = function (section_id, value) { - // Blocked interface names that should never be selectable - const blockedInterfaces = [ - 'br-lan', - 'eth0', - 'eth1', - 'wan', - 'phy0-ap0', - 'phy1-ap0', - 'pppoe-wan', - 'lan', - ]; - - // Reject immediately if the value matches any blocked interface - if (blockedInterfaces.includes(value)) { - return false; - } - - // Try to find the device object with the given name - const device = this.devices.find((dev) => dev.getName() === value); - - // If no device is found, allow the value - if (!device) { - return true; - } - - // Get the device type (e.g., "wifi", "ethernet", etc.) - const type = device.getType(); - - // Reject wireless-related devices - const isWireless = - type === 'wifi' || type === 'wireless' || type.includes('wlan'); - - return !isWireless; - }; - - o = s.taboption( - 'basic', - form.Flag, - 'domain_resolver_enabled', - _('Domain Resolver'), - _('Enable built-in DNS resolver for domains handled by this section'), - ); - o.default = '0'; - o.rmempty = false; - o.depends('mode', 'vpn'); - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.ListValue, - 'domain_resolver_dns_type', - _('DNS Protocol Type'), - _('Select the DNS protocol type for the domain resolver'), - ); - o.value('doh', _('DNS over HTTPS (DoH)')); - o.value('dot', _('DNS over TLS (DoT)')); - o.value('udp', _('UDP (Unprotected DNS)')); - o.default = 'udp'; - o.rmempty = false; - o.depends('domain_resolver_enabled', '1'); - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.Value, - 'domain_resolver_dns_server', - _('DNS Server'), - _('Select or enter DNS server address'), - ); - Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => { - o.value(key, _(label)); - }); - o.default = '8.8.8.8'; - o.rmempty = false; - o.depends('domain_resolver_enabled', '1'); - o.ucisection = s.section; - o.validate = function (section_id, value) { - const validation = main.validateDNS(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.Flag, - 'community_lists_enabled', - _('Community Lists'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.DynamicList, - 'community_lists', - _('Service List'), - _('Select predefined service for routing') + - ' github.com/itdoginfo/allow-domains', - ); - o.placeholder = 'Service list'; - Object.entries(main.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => { - o.value(key, _(label)); - }); - o.depends('community_lists_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - - let lastValues = []; - let isProcessing = false; - - o.onchange = function (ev, section_id, value) { - if (isProcessing) return; - isProcessing = true; - - try { - const values = Array.isArray(value) ? value : [value]; - let newValues = [...values]; - let notifications = []; - - const selectedRegionalOptions = main.REGIONAL_OPTIONS.filter((opt) => - newValues.includes(opt), - ); - - if (selectedRegionalOptions.length > 1) { - const lastSelected = - selectedRegionalOptions[selectedRegionalOptions.length - 1]; - const removedRegions = selectedRegionalOptions.slice(0, -1); - newValues = newValues.filter( - (v) => v === lastSelected || !main.REGIONAL_OPTIONS.includes(v), - ); - notifications.push( - E('p', { class: 'alert-message warning' }, [ - E('strong', {}, _('Regional options cannot be used together')), - E('br'), - _( - 'Warning: %s cannot be used together with %s. Previous selections have been removed.', - ).format(removedRegions.join(', '), lastSelected), - ]), - ); - } - - if (newValues.includes('russia_inside')) { - const removedServices = newValues.filter( - (v) => !main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v), - ); - if (removedServices.length > 0) { - newValues = newValues.filter((v) => - main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v), - ); - notifications.push( - E('p', { class: 'alert-message warning' }, [ - E('strong', {}, _('Russia inside restrictions')), - E('br'), - _( - 'Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.', - ).format( - main.ALLOWED_WITH_RUSSIA_INSIDE.map( - (key) => main.DOMAIN_LIST_OPTIONS[key], - ) - .filter((label) => label !== 'Russia inside') - .join(', '), - removedServices.join(', '), - ), - ]), - ); - } - } - - if (JSON.stringify(newValues.sort()) !== JSON.stringify(values.sort())) { - this.getUIElement(section_id).setValue(newValues); - } - - notifications.forEach((notification) => - ui.addNotification(null, notification), - ); - lastValues = newValues; - } catch (e) { - console.error('Error in onchange handler:', e); - } finally { - isProcessing = false; - } - }; - - o = s.taboption( - 'basic', - form.ListValue, - 'user_domain_list_type', - _('User Domain List Type'), - _('Select how to add your custom domains'), - ); - o.value('disabled', _('Disabled')); - o.value('dynamic', _('Dynamic List')); - o.value('text', _('Text List')); - o.default = 'disabled'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.DynamicList, - 'user_domains', - _('User Domains'), - _( - 'Enter domain names without protocols (example: sub.example.com or example.com)', - ), - ); - o.placeholder = 'Domains list'; - o.depends('user_domain_list_type', 'dynamic'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateDomain(value, true); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.TextValue, - 'user_domains_text', - _('User Domains List'), - _( - 'Enter domain names separated by comma, space or newline. You can add comments after //', - ), - ); - o.placeholder = - 'example.com, sub.example.com\n// Social networks\ndomain.com test.com // personal domains'; - o.depends('user_domain_list_type', 'text'); - o.rows = 8; - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const domains = main.parseValueList(value); - - if (!domains.length) { - return _( - 'At least one valid domain must be specified. Comments-only content is not allowed.', - ); - } - - const { valid, results } = main.bulkValidate(domains, row => main.validateDomain(row, true)); - - if (!valid) { - const errors = results - .filter((validation) => !validation.valid) // Leave only failed validations - .map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors - - return [_('Validation errors:'), ...errors].join('\n'); - } - - return true; - }; - - o = s.taboption( - 'basic', - form.Flag, - 'local_domain_lists_enabled', - _('Local Domain Lists'), - _('Use the list from the router filesystem'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.DynamicList, - 'local_domain_lists', - _('Local Domain List Paths'), - _('Enter the list file path'), - ); - o.placeholder = '/path/file.lst'; - o.depends('local_domain_lists_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validatePath(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.Flag, - 'remote_domain_lists_enabled', - _('Remote Domain Lists'), - _('Download and use domain lists from remote URLs'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.DynamicList, - 'remote_domain_lists', - _('Remote Domain URLs'), - _('Enter full URLs starting with http:// or https://'), - ); - o.placeholder = 'URL'; - o.depends('remote_domain_lists_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateUrl(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.Flag, - 'local_subnet_lists_enabled', - _('Local Subnet Lists'), - _('Use the list from the router filesystem'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.DynamicList, - 'local_subnet_lists', - _('Local Subnet List Paths'), - _('Enter the list file path'), - ); - o.placeholder = '/path/file.lst'; - o.depends('local_subnet_lists_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validatePath(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.ListValue, - 'user_subnet_list_type', - _('User Subnet List Type'), - _('Select how to add your custom subnets'), - ); - o.value('disabled', _('Disabled')); - o.value('dynamic', _('Dynamic List')); - o.value('text', _('Text List (comma/space/newline separated)')); - o.default = 'disabled'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.DynamicList, - 'user_subnets', - _('User Subnets'), - _( - 'Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses', - ), - ); - o.placeholder = 'IP or subnet'; - o.depends('user_subnet_list_type', 'dynamic'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateSubnet(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.TextValue, - 'user_subnets_text', - _('User Subnets List'), - _( - 'Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //', - ), - ); - o.placeholder = - '103.21.244.0/22\n// Google DNS\n8.8.8.8\n1.1.1.1/32, 9.9.9.9 // Cloudflare and Quad9'; - o.depends('user_subnet_list_type', 'text'); - o.rows = 10; - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const subnets = main.parseValueList(value); - - if (!subnets.length) { - return _( - 'At least one valid subnet or IP must be specified. Comments-only content is not allowed.', - ); - } - - const { valid, results } = main.bulkValidate(subnets, main.validateSubnet); - - if (!valid) { - const errors = results - .filter((validation) => !validation.valid) // Leave only failed validations - .map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors - - return [_('Validation errors:'), ...errors].join('\n'); - } - - return true; - }; - - o = s.taboption( - 'basic', - form.Flag, - 'remote_subnet_lists_enabled', - _('Remote Subnet Lists'), - _('Download and use subnet lists from remote URLs'), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.DynamicList, - 'remote_subnet_lists', - _('Remote Subnet URLs'), - _('Enter full URLs starting with http:// or https://'), - ); - o.placeholder = 'URL'; - o.depends('remote_subnet_lists_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateUrl(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; - - o = s.taboption( - 'basic', - form.Flag, - 'all_traffic_from_ip_enabled', - _('IP for full redirection'), - _( - 'Specify local IP addresses whose traffic will always use the configured route', - ), - ); - o.default = '0'; - o.rmempty = false; - o.ucisection = s.section; - - o = s.taboption( - 'basic', - form.DynamicList, - 'all_traffic_ip', - _('Local IPs'), - _('Enter valid IPv4 addresses'), - ); - o.placeholder = 'IP'; - o.depends('all_traffic_from_ip_enabled', '1'); - o.rmempty = false; - o.ucisection = s.section; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateSubnet(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; -} - -return baseclass.extend({ - createConfigSection, -}); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/dashboard.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/dashboard.js new file mode 100644 index 0000000..423b47f --- /dev/null +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/dashboard.js @@ -0,0 +1,22 @@ +'use strict'; +'require baseclass'; +'require form'; +'require ui'; +'require uci'; +'require fs'; +'require view.podkop.main as main'; + +function createDashboardContent(section) { + const o = section.option(form.DummyValue, '_mount_node'); + o.rawhtml = true; + o.cfgvalue = () => { + main.initDashboardController(); + return main.renderDashboard(); + }; +} + +const EntryPoint = { + createDashboardContent, +}; + +return baseclass.extend(EntryPoint); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/dashboardTab.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/dashboardTab.js deleted file mode 100644 index a5056dc..0000000 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/dashboardTab.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; -'require baseclass'; -'require form'; -'require ui'; -'require uci'; -'require fs'; -'require view.podkop.utils as utils'; -'require view.podkop.main as main'; - -function createDashboardSection(mainSection) { - let o = mainSection.tab('dashboard', _('Dashboard')); - - o = mainSection.taboption('dashboard', form.DummyValue, '_status'); - o.rawhtml = true; - o.cfgvalue = () => { - main.initDashboardController(); - - return main.renderDashboard(); - }; -} - -const EntryPoint = { - createDashboardSection, -}; - -return baseclass.extend(EntryPoint); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticTab.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticTab.js deleted file mode 100644 index 1de1f76..0000000 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticTab.js +++ /dev/null @@ -1,1220 +0,0 @@ -'use strict'; -'require baseclass'; -'require form'; -'require ui'; -'require uci'; -'require fs'; -'require view.podkop.utils as utils'; -'require view.podkop.main as main'; - -// Cache system for network requests -const fetchCache = {}; - -// Helper function to fetch with cache -async function cachedFetch(url, options = {}) { - const cacheKey = url; - const currentTime = Date.now(); - - // If we have a valid cached response, return it - if ( - fetchCache[cacheKey] && - currentTime - fetchCache[cacheKey].timestamp < main.CACHE_TIMEOUT - ) { - console.log(`Using cached response for ${url}`); - return Promise.resolve(fetchCache[cacheKey].response.clone()); - } - - // Otherwise, make a new request - try { - const response = await fetch(url, options); - - // Cache the response - fetchCache[cacheKey] = { - response: response.clone(), - timestamp: currentTime, - }; - - return response; - } catch (error) { - throw error; - } -} - -// Helper functions for command execution with prioritization - Using from utils.js now -function safeExec( - command, - args, - priority, - callback, - timeout = main.COMMAND_TIMEOUT, -) { - return utils.safeExec(command, args, priority, callback, timeout); -} - -// Helper functions for handling checks -function runCheck(checkFunction, priority, callback) { - // Default to highest priority execution if priority is not provided or invalid - let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY; - - // If priority is a string, try to get the corresponding delay value - if ( - typeof priority === 'string' && - main.COMMAND_SCHEDULING[priority] !== undefined - ) { - schedulingDelay = main.COMMAND_SCHEDULING[priority]; - } - - const executeCheck = async () => { - try { - const result = await checkFunction(); - if (callback && typeof callback === 'function') { - callback(result); - } - return result; - } catch (error) { - if (callback && typeof callback === 'function') { - callback({ error }); - } - return { error }; - } - }; - - if (callback && typeof callback === 'function') { - setTimeout(executeCheck, schedulingDelay); - return; - } else { - return executeCheck(); - } -} - -function runAsyncTask(taskFunction, priority) { - // Default to highest priority execution if priority is not provided or invalid - let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY; - - // If priority is a string, try to get the corresponding delay value - if ( - typeof priority === 'string' && - main.COMMAND_SCHEDULING[priority] !== undefined - ) { - schedulingDelay = main.COMMAND_SCHEDULING[priority]; - } - - setTimeout(async () => { - try { - await taskFunction(); - } catch (error) { - console.error('Async task error:', error); - } - }, schedulingDelay); -} - -// Helper Functions for UI and formatting -function createStatus(state, message, color) { - return { - state, - message: _(message), - color: main.STATUS_COLORS[color], - }; -} - -function formatDiagnosticOutput(output) { - if (typeof output !== 'string') return ''; - return output - .trim() - .replace(/\x1b\[[0-9;]*m/g, '') - .replace(/\r\n/g, '\n') - .replace(/\r/g, '\n'); -} - -function copyToClipboard(text, button) { - const textarea = document.createElement('textarea'); - textarea.value = text; - document.body.appendChild(textarea); - textarea.select(); - try { - document.execCommand('copy'); - const originalText = button.textContent; - button.textContent = _('Copied!'); - setTimeout( - () => (button.textContent = originalText), - main.BUTTON_FEEDBACK_TIMEOUT, - ); - } catch (err) { - ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message)); - } - document.body.removeChild(textarea); -} - -// IP masking function -function maskIP(ip) { - if (!ip) return ''; - const parts = ip.split('.'); - if (parts.length !== 4) return ip; - return ['XX', 'XX', 'XX', parts[3]].join('.'); -} - -// Status Check Functions -async function checkFakeIP() { - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), main.FETCH_TIMEOUT); - - try { - const response = await cachedFetch( - `https://${main.FAKEIP_CHECK_DOMAIN}/check`, - { signal: controller.signal }, - ); - const data = await response.json(); - clearTimeout(timeoutId); - - if (data.fakeip === true) { - return createStatus('working', 'working', 'SUCCESS'); - } else { - return createStatus('not_working', 'not working', 'ERROR'); - } - } catch (fetchError) { - clearTimeout(timeoutId); - const message = - fetchError.name === 'AbortError' ? 'timeout' : 'check error'; - return createStatus('error', message, 'WARNING'); - } - } catch (error) { - return createStatus('error', 'check error', 'WARNING'); - } -} - -async function checkFakeIPCLI() { - try { - return new Promise((resolve) => { - safeExec( - 'nslookup', - ['-timeout=2', main.FAKEIP_CHECK_DOMAIN, '127.0.0.42'], - 'P0_PRIORITY', - (result) => { - if (result.stdout && result.stdout.includes('198.18')) { - resolve(createStatus('working', 'working on router', 'SUCCESS')); - } else { - resolve( - createStatus('not_working', 'not working on router', 'ERROR'), - ); - } - }, - ); - }); - } catch (error) { - return createStatus('error', 'CLI check error', 'WARNING'); - } -} - -function checkDNSAvailability() { - return new Promise(async (resolve) => { - try { - safeExec( - '/usr/bin/podkop', - ['check_dns_available'], - 'P0_PRIORITY', - (dnsStatusResult) => { - if (!dnsStatusResult || !dnsStatusResult.stdout) { - return resolve({ - remote: createStatus('error', 'DNS check timeout', 'WARNING'), - local: createStatus('error', 'DNS check timeout', 'WARNING'), - }); - } - - try { - const dnsStatus = JSON.parse(dnsStatusResult.stdout); - - const remoteStatus = dnsStatus.is_available - ? createStatus( - 'available', - `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) available`, - 'SUCCESS', - ) - : createStatus( - 'unavailable', - `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) unavailable`, - 'ERROR', - ); - - const localStatus = dnsStatus.local_dns_working - ? createStatus('available', 'Router DNS working', 'SUCCESS') - : createStatus('unavailable', 'Router DNS not working', 'ERROR'); - - return resolve({ - remote: remoteStatus, - local: localStatus, - }); - } catch (parseError) { - return resolve({ - remote: createStatus('error', 'DNS check parse error', 'WARNING'), - local: createStatus('error', 'DNS check parse error', 'WARNING'), - }); - } - }, - ); - } catch (error) { - return resolve({ - remote: createStatus('error', 'DNS check error', 'WARNING'), - local: createStatus('error', 'DNS check error', 'WARNING'), - }); - } - }); -} - -async function checkBypass() { - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), main.FETCH_TIMEOUT); - - try { - const response1 = await cachedFetch( - `https://${main.FAKEIP_CHECK_DOMAIN}/check`, - { signal: controller.signal }, - ); - const data1 = await response1.json(); - - const response2 = await cachedFetch( - `https://${main.IP_CHECK_DOMAIN}/check`, - { signal: controller.signal }, - ); - const data2 = await response2.json(); - - clearTimeout(timeoutId); - - if (data1.IP && data2.IP) { - if (data1.IP !== data2.IP) { - return createStatus('working', 'working', 'SUCCESS'); - } else { - return createStatus( - 'not_working', - 'same IP for both domains', - 'ERROR', - ); - } - } else { - return createStatus('error', 'check error (no IP)', 'WARNING'); - } - } catch (fetchError) { - clearTimeout(timeoutId); - const message = - fetchError.name === 'AbortError' ? 'timeout' : 'check error'; - return createStatus('error', message, 'WARNING'); - } - } catch (error) { - return createStatus('error', 'check error', 'WARNING'); - } -} - -function showConfigModal(command, title) { - // Create and show modal immediately with loading state - const modalContent = E('div', { class: 'panel-body' }, [ - E( - 'div', - { - class: 'panel-body', - style: - 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; ' + - 'font-family: monospace; white-space: pre-wrap; word-wrap: break-word; ' + - 'line-height: 1.5; font-size: 14px;', - }, - [ - E( - 'pre', - { - id: 'modal-content-pre', - style: 'margin: 0;', - }, - _('Loading...'), - ), - ], - ), - E( - 'div', - { - class: 'right', - style: 'margin-top: 1em;', - }, - [ - E( - 'button', - { - class: 'btn', - id: 'copy-button', - click: (ev) => - copyToClipboard( - '```txt\n' + - document.getElementById('modal-content-pre').innerText + - '\n```', - ev.target, - ), - }, - _('Copy to Clipboard'), - ), - E( - 'button', - { - class: 'btn', - click: ui.hideModal, - }, - _('Close'), - ), - ], - ), - ]); - - ui.showModal(_(title), modalContent); - - // Function to update modal content - const updateModalContent = (content) => { - const pre = document.getElementById('modal-content-pre'); - if (pre) { - pre.textContent = content; - } - }; - - try { - let formattedOutput = ''; - - if (command === 'global_check') { - safeExec('/usr/bin/podkop', [command, `${main.PODKOP_LUCI_APP_VERSION}`], 'P0_PRIORITY', (res) => { - formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); - - try { - const controller = new AbortController(); - const timeoutId = setTimeout( - () => controller.abort(), - main.FETCH_TIMEOUT, - ); - - cachedFetch(`https://${main.FAKEIP_CHECK_DOMAIN}/check`, { - signal: controller.signal, - }) - .then((response) => response.json()) - .then((data) => { - clearTimeout(timeoutId); - - if (data.fakeip === true) { - formattedOutput += - '\n✅ ' + _('FakeIP is working in browser!') + '\n'; - } else { - formattedOutput += - '\n❌ ' + _('FakeIP is not working in browser') + '\n'; - formattedOutput += - _('Check DNS server on current device (PC, phone)') + '\n'; - formattedOutput += _('Its must be router!') + '\n'; - } - - // Bypass check - cachedFetch(`https://${main.FAKEIP_CHECK_DOMAIN}/check`, { - signal: controller.signal, - }) - .then((bypassResponse) => bypassResponse.json()) - .then((bypassData) => { - cachedFetch(`https://${main.IP_CHECK_DOMAIN}/check`, { - signal: controller.signal, - }) - .then((bypassResponse2) => bypassResponse2.json()) - .then((bypassData2) => { - formattedOutput += '━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'; - - if ( - bypassData.IP && - bypassData2.IP && - bypassData.IP !== bypassData2.IP - ) { - formattedOutput += - '✅ ' + _('Proxy working correctly') + '\n'; - formattedOutput += - _('Direct IP: ') + maskIP(bypassData.IP) + '\n'; - formattedOutput += - _('Proxy IP: ') + maskIP(bypassData2.IP) + '\n'; - } else if (bypassData.IP === bypassData2.IP) { - formattedOutput += - '❌ ' + - _('Proxy is not working - same IP for both domains') + - '\n'; - formattedOutput += - _('IP: ') + maskIP(bypassData.IP) + '\n'; - } else { - formattedOutput += - '❌ ' + _('Proxy check failed') + '\n'; - } - - updateModalContent(formattedOutput); - }) - .catch((error) => { - formattedOutput += - '\n❌ ' + - _('Check failed: ') + - (error.name === 'AbortError' - ? _('timeout') - : error.message) + - '\n'; - updateModalContent(formattedOutput); - }); - }) - .catch((error) => { - formattedOutput += - '\n❌ ' + - _('Check failed: ') + - (error.name === 'AbortError' - ? _('timeout') - : error.message) + - '\n'; - updateModalContent(formattedOutput); - }); - }) - .catch((error) => { - formattedOutput += - '\n❌ ' + - _('Check failed: ') + - (error.name === 'AbortError' ? _('timeout') : error.message) + - '\n'; - updateModalContent(formattedOutput); - }); - } catch (error) { - formattedOutput += - '\n❌ ' + _('Check failed: ') + error.message + '\n'; - updateModalContent(formattedOutput); - } - }); - } else { - safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', (res) => { - formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); - updateModalContent(formattedOutput); - }); - } - } catch (error) { - updateModalContent(_('Error: ') + error.message); - } -} - -// Button Factory -const ButtonFactory = { - createButton: function (config) { - return E( - 'button', - { - class: `btn ${config.additionalClass || ''}`.trim(), - click: config.onClick, - style: config.style || '', - }, - _(config.label), - ); - }, - - createActionButton: function (config) { - return this.createButton({ - label: config.label, - additionalClass: `cbi-button-${config.type || ''}`, - onClick: () => - safeExec('/usr/bin/podkop', [config.action], 'P0_PRIORITY').then( - () => config.reload && location.reload(), - ), - style: config.style, - }); - }, - - createInitActionButton: function (config) { - return this.createButton({ - label: config.label, - additionalClass: `cbi-button-${config.type || ''}`, - onClick: () => - safeExec('/etc/init.d/podkop', [config.action], 'P0_PRIORITY').then( - () => config.reload && location.reload(), - ), - style: config.style, - }); - }, - - createModalButton: function (config) { - return this.createButton({ - label: config.label, - onClick: () => showConfigModal(config.command, config.title), - additionalClass: `cbi-button-${config.type || ''}`, - style: config.style, - }); - }, -}; - -// Create a loading placeholder for status text -function createLoadingStatusText() { - return E('span', { class: 'loading-indicator' }, _('Loading...')); -} - -// Create the status section with buttons loaded immediately but status indicators loading asynchronously -let createStatusSection = async function () { - // Get initial podkop status - let initialPodkopStatus = { enabled: false }; - try { - const result = await fs.exec('/usr/bin/podkop', ['get_status']); - if (result && result.stdout) { - const status = JSON.parse(result.stdout); - initialPodkopStatus.enabled = status.enabled === 1; - } - } catch (e) { - console.error('Error getting initial podkop status:', e); - } - - return E('div', { class: 'cbi-section' }, [ - E('div', { class: 'table', style: 'display: flex; gap: 20px;' }, [ - // Podkop Status Panel - E( - 'div', - { - id: 'podkop-status-panel', - class: 'panel', - style: 'flex: 1; padding: 15px;', - }, - [ - E('div', { class: 'panel-heading' }, [ - E('strong', {}, _('Podkop Status')), - E('br'), - E('span', { id: 'podkop-status-text' }, createLoadingStatusText()), - ]), - E( - 'div', - { - class: 'panel-body', - style: 'display: flex; flex-direction: column; gap: 8px;', - }, - [ - ButtonFactory.createActionButton({ - label: 'Restart Podkop', - type: 'apply', - action: 'restart', - reload: true, - }), - ButtonFactory.createActionButton({ - label: 'Stop Podkop', - type: 'apply', - action: 'stop', - reload: true, - }), - // Autostart button - create with initial state - ButtonFactory.createInitActionButton({ - label: initialPodkopStatus.enabled - ? 'Disable Autostart' - : 'Enable Autostart', - type: initialPodkopStatus.enabled ? 'remove' : 'apply', - action: initialPodkopStatus.enabled ? 'disable' : 'enable', - reload: true, - }), - ButtonFactory.createModalButton({ - label: _('Global check'), - command: 'global_check', - title: _('Click here for all the info'), - }), - ButtonFactory.createModalButton({ - label: 'View Logs', - command: 'check_logs', - title: 'Podkop Logs', - }), - ButtonFactory.createModalButton({ - label: _('Update Lists'), - command: 'list_update', - title: _('Lists Update Results'), - }), - ], - ), - ], - ), - - // Sing-box Status Panel - E( - 'div', - { - id: 'singbox-status-panel', - class: 'panel', - style: 'flex: 1; padding: 15px;', - }, - [ - E('div', { class: 'panel-heading' }, [ - E('strong', {}, _('Sing-box Status')), - E('br'), - E('span', { id: 'singbox-status-text' }, createLoadingStatusText()), - ]), - E( - 'div', - { - class: 'panel-body', - style: 'display: flex; flex-direction: column; gap: 8px;', - }, - [ - ButtonFactory.createModalButton({ - label: 'Show Config', - command: 'show_sing_box_config', - title: 'Sing-box Configuration', - }), - ButtonFactory.createModalButton({ - label: 'View Logs', - command: 'check_sing_box_logs', - title: 'Sing-box Logs', - }), - ButtonFactory.createModalButton({ - label: 'Check Connections', - command: 'check_sing_box_connections', - title: 'Active Connections', - }), - ButtonFactory.createModalButton({ - label: _('Check NFT Rules'), - command: 'check_nft', - title: _('NFT Rules'), - }), - ButtonFactory.createModalButton({ - label: _('Check DNSMasq'), - command: 'check_dnsmasq', - title: _('DNSMasq Configuration'), - }), - ], - ), - ], - ), - - // FakeIP Status Panel - E( - 'div', - { - id: 'fakeip-status-panel', - class: 'panel', - style: 'flex: 1; padding: 15px;', - }, - [ - E('div', { class: 'panel-heading' }, [ - E('strong', {}, _('FakeIP Status')), - ]), - E( - 'div', - { - class: 'panel-body', - style: 'display: flex; flex-direction: column; gap: 8px;', - }, - [ - E('div', { style: 'margin-bottom: 5px;' }, [ - E('div', {}, [ - E( - 'span', - { id: 'fakeip-browser-status' }, - createLoadingStatusText(), - ), - ]), - E('div', {}, [ - E( - 'span', - { id: 'fakeip-router-status' }, - createLoadingStatusText(), - ), - ]), - ]), - E('div', { style: 'margin-bottom: 5px;' }, [ - E('div', {}, [ - E('strong', {}, _('DNS Status')), - E('br'), - E( - 'span', - { id: 'dns-remote-status' }, - createLoadingStatusText(), - ), - E('br'), - E( - 'span', - { id: 'dns-local-status' }, - createLoadingStatusText(), - ), - ]), - ]), - E('div', { style: 'margin-bottom: 5px;' }, [ - E('div', {}, [ - E('strong', { id: 'config-name-text' }, _('Main config')), - E('br'), - E('span', { id: 'bypass-status' }, createLoadingStatusText()), - ]), - ]), - ], - ), - ], - ), - - // Version Information Panel - E( - 'div', - { - id: 'version-info-panel', - class: 'panel', - style: 'flex: 1; padding: 15px;', - }, - [ - E('div', { class: 'panel-heading' }, [ - E('strong', {}, _('Version Information')), - ]), - E('div', { class: 'panel-body' }, [ - E( - 'div', - { - style: - 'margin-top: 10px; font-family: monospace; white-space: pre-wrap;', - }, - [ - E('strong', {}, _('Podkop: ')), - E('span', { id: 'podkop-version' }, _('Loading...')), - '\n', - E('strong', {}, _('LuCI App: ')), - E('span', { id: 'luci-version' }, _('Loading...')), - '\n', - E('strong', {}, _('Sing-box: ')), - E('span', { id: 'singbox-version' }, _('Loading...')), - '\n', - E('strong', {}, _('OpenWrt Version: ')), - E('span', { id: 'openwrt-version' }, _('Loading...')), - '\n', - E('strong', {}, _('Device Model: ')), - E('span', { id: 'device-model' }, _('Loading...')), - ], - ), - ]), - ], - ), - ]), - ]); -}; - -// Global variables for tracking state -let diagnosticsUpdateTimer = null; -let isInitialCheck = true; -showConfigModal.busy = false; - -function startDiagnosticsUpdates() { - if (diagnosticsUpdateTimer) { - clearInterval(diagnosticsUpdateTimer); - } - - // Immediately update when started - updateDiagnostics(); - - // Then set up periodic updates - diagnosticsUpdateTimer = setInterval( - updateDiagnostics, - main.DIAGNOSTICS_UPDATE_INTERVAL, - ); -} - -function stopDiagnosticsUpdates() { - if (diagnosticsUpdateTimer) { - clearInterval(diagnosticsUpdateTimer); - diagnosticsUpdateTimer = null; - } -} - -// Update individual text element with new content -function updateTextElement(elementId, content) { - const element = document.getElementById(elementId); - if (element) { - element.innerHTML = ''; - element.appendChild(content); - } -} - -async function updateDiagnostics() { - // Podkop Status check - safeExec('/usr/bin/podkop', ['get_status'], 'P0_PRIORITY', (result) => { - try { - const parsedPodkopStatus = JSON.parse( - result.stdout || '{"enabled":0,"status":"error"}', - ); - - // Update Podkop status text - updateTextElement( - 'podkop-status-text', - E( - 'span', - { - style: `color: ${parsedPodkopStatus.enabled ? main.STATUS_COLORS.SUCCESS : main.STATUS_COLORS.ERROR}`, - }, - [ - parsedPodkopStatus.enabled - ? '✔ Autostart enabled' - : '✘ Autostart disabled', - ], - ), - ); - - // Update autostart button - const autostartButton = parsedPodkopStatus.enabled - ? ButtonFactory.createInitActionButton({ - label: 'Disable Autostart', - type: 'remove', - action: 'disable', - reload: true, - }) - : ButtonFactory.createInitActionButton({ - label: 'Enable Autostart', - type: 'apply', - action: 'enable', - reload: true, - }); - - // Find the autostart button and replace it - const panel = document.getElementById('podkop-status-panel'); - if (panel) { - const buttons = panel.querySelectorAll('.cbi-button'); - if (buttons.length >= 3) { - buttons[2].parentNode.replaceChild(autostartButton, buttons[2]); - } - } - } catch (error) { - updateTextElement( - 'podkop-status-text', - E('span', { style: `color: ${main.STATUS_COLORS.ERROR}` }, '✘ Error'), - ); - } - }); - - // Sing-box Status check - safeExec( - '/usr/bin/podkop', - ['get_sing_box_status'], - 'P0_PRIORITY', - (result) => { - try { - const parsedSingboxStatus = JSON.parse( - result.stdout || '{"running":0,"enabled":0,"status":"error"}', - ); - - // Update Sing-box status text - updateTextElement( - 'singbox-status-text', - E( - 'span', - { - style: `color: ${ - parsedSingboxStatus.running && !parsedSingboxStatus.enabled - ? main.STATUS_COLORS.SUCCESS - : main.STATUS_COLORS.ERROR - }`, - }, - [ - parsedSingboxStatus.running && !parsedSingboxStatus.enabled - ? '✔ running' - : '✘ ' + parsedSingboxStatus.status, - ], - ), - ); - } catch (error) { - updateTextElement( - 'singbox-status-text', - E('span', { style: `color: ${main.STATUS_COLORS.ERROR}` }, '✘ Error'), - ); - } - }, - ); - - // Version Information checks - safeExec('/usr/bin/podkop', ['show_version'], 'P2_PRIORITY', (result) => { - updateTextElement( - 'podkop-version', - document.createTextNode( - result.stdout ? result.stdout.trim() : _('Unknown'), - ), - ); - }); - - updateTextElement( - 'luci-version', - document.createTextNode( - `${main.PODKOP_LUCI_APP_VERSION}` - ) - ); - - safeExec( - '/usr/bin/podkop', - ['show_sing_box_version'], - 'P2_PRIORITY', - (result) => { - updateTextElement( - 'singbox-version', - document.createTextNode( - result.stdout ? result.stdout.trim() : _('Unknown'), - ), - ); - }, - ); - - safeExec('/usr/bin/podkop', ['show_system_info'], 'P2_PRIORITY', (result) => { - if (result.stdout) { - updateTextElement( - 'openwrt-version', - document.createTextNode(result.stdout.split('\n')[1].trim()), - ); - updateTextElement( - 'device-model', - document.createTextNode(result.stdout.split('\n')[4].trim()), - ); - } else { - updateTextElement( - 'openwrt-version', - document.createTextNode(_('Unknown')), - ); - updateTextElement('device-model', document.createTextNode(_('Unknown'))); - } - }); - - // FakeIP and DNS status checks - runCheck(checkFakeIP, 'P3_PRIORITY', (result) => { - updateTextElement( - 'fakeip-browser-status', - E( - 'span', - { - style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}`, - }, - [ - result.error - ? '! ' - : result.state === 'working' - ? '✔ ' - : result.state === 'not_working' - ? '✘ ' - : '! ', - result.error - ? 'check error' - : result.state === 'working' - ? _('works in browser') - : _('does not work in browser'), - ], - ), - ); - }); - - runCheck(checkFakeIPCLI, 'P8_PRIORITY', (result) => { - updateTextElement( - 'fakeip-router-status', - E( - 'span', - { - style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}`, - }, - [ - result.error - ? '! ' - : result.state === 'working' - ? '✔ ' - : result.state === 'not_working' - ? '✘ ' - : '! ', - result.error - ? 'check error' - : result.state === 'working' - ? _('works on router') - : _('does not work on router'), - ], - ), - ); - }); - - runCheck(checkDNSAvailability, 'P4_PRIORITY', (result) => { - if (result.error) { - updateTextElement( - 'dns-remote-status', - E( - 'span', - { style: `color: ${main.STATUS_COLORS.WARNING}` }, - '! DNS check error', - ), - ); - updateTextElement( - 'dns-local-status', - E( - 'span', - { style: `color: ${main.STATUS_COLORS.WARNING}` }, - '! DNS check error', - ), - ); - } else { - updateTextElement( - 'dns-remote-status', - E('span', { style: `color: ${result.remote.color}` }, [ - result.remote.state === 'available' - ? '✔ ' - : result.remote.state === 'unavailable' - ? '✘ ' - : '! ', - result.remote.message, - ]), - ); - - updateTextElement( - 'dns-local-status', - E('span', { style: `color: ${result.local.color}` }, [ - result.local.state === 'available' - ? '✔ ' - : result.local.state === 'unavailable' - ? '✘ ' - : '! ', - result.local.message, - ]), - ); - } - }); - - runCheck( - checkBypass, - 'P1_PRIORITY', - (result) => { - updateTextElement( - 'bypass-status', - E( - 'span', - { - style: `color: ${result.error ? main.STATUS_COLORS.WARNING : result.color}`, - }, - [ - result.error - ? '! ' - : result.state === 'working' - ? '✔ ' - : result.state === 'not_working' - ? '✘ ' - : '! ', - result.error ? 'check error' : result.message, - ], - ), - ); - }, - 'P1_PRIORITY', - ); - - // Config name - runAsyncTask(async () => { - try { - let configName = _('Main config'); - const data = await uci.load('podkop'); - const proxyString = uci.get('podkop', 'main', 'proxy_string'); - - if (proxyString) { - const activeConfig = proxyString - .split('\n') - .map((line) => line.trim()) - .find((line) => line && !line.startsWith('//')); - - if (activeConfig) { - if (activeConfig.includes('#')) { - const label = activeConfig.split('#').pop(); - if (label && label.trim()) { - configName = _('Config: ') + decodeURIComponent(label); - } - } - } - } - - updateTextElement( - 'config-name-text', - document.createTextNode(configName), - ); - } catch (e) { - console.error('Error getting config name from UCI:', e); - } - }, 'P1_PRIORITY'); -} - -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', - 'data-loading': 'true', - }); -} - -function setupDiagnosticsEventHandlers(node) { - const titleDiv = E('h2', { class: 'cbi-map-title' }, _('Podkop')); - node.insertBefore(titleDiv, node.firstChild); - - // Function to initialize diagnostics - function initDiagnostics(container) { - if (container && container.hasAttribute('data-loading')) { - container.innerHTML = ''; - showConfigModal.busy = false; - createStatusSection().then((section) => { - container.appendChild(section); - startDiagnosticsUpdates(); - // Start error polling when diagnostics tab is active - utils.startErrorPolling(); - }); - } - } - - document.addEventListener('visibilitychange', function () { - const diagnosticsContainer = document.getElementById('diagnostics-status'); - const diagnosticsTab = document.querySelector( - '.cbi-tab[data-tab="diagnostics"]', - ); - - if ( - document.hidden || - !diagnosticsTab || - !diagnosticsTab.classList.contains('cbi-tab-active') - ) { - stopDiagnosticsUpdates(); - // Don't stop error polling here - it's managed in podkop.js for all tabs - } else if ( - diagnosticsContainer && - diagnosticsContainer.hasAttribute('data-loading') - ) { - startDiagnosticsUpdates(); - // Ensure error polling is running when diagnostics tab is active - utils.startErrorPolling(); - } - }); - - setTimeout(() => { - const diagnosticsContainer = document.getElementById('diagnostics-status'); - const diagnosticsTab = document.querySelector( - '.cbi-tab[data-tab="diagnostics"]', - ); - const otherTabs = document.querySelectorAll( - '.cbi-tab:not([data-tab="diagnostics"])', - ); - - // Check for direct page load case - const noActiveTabsExist = !Array.from(otherTabs).some((tab) => - tab.classList.contains('cbi-tab-active'), - ); - - if ( - diagnosticsContainer && - diagnosticsTab && - (diagnosticsTab.classList.contains('cbi-tab-active') || noActiveTabsExist) - ) { - initDiagnostics(diagnosticsContainer); - } - - const tabs = node.querySelectorAll('.cbi-tabmenu'); - if (tabs.length > 0) { - tabs[0].addEventListener('click', function (e) { - const tab = e.target.closest('.cbi-tab'); - if (tab) { - const tabName = tab.getAttribute('data-tab'); - if (tabName === 'diagnostics') { - const container = document.getElementById('diagnostics-status'); - container.setAttribute('data-loading', 'true'); - initDiagnostics(container); - } else { - stopDiagnosticsUpdates(); - // Don't stop error polling - it should continue on all tabs - } - } - }); - } - }, main.DIAGNOSTICS_INITIAL_DELAY); - - node.classList.add('fade-in'); - return node; -} - -return baseclass.extend({ - createDiagnosticsSection, - setupDiagnosticsEventHandlers, -}); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js index 2f5f206..b173c39 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js @@ -247,10 +247,30 @@ var GlobalStyles = ` display: none; } -#cbi-podkop-main-_status > div { +#cbi-podkop-dashboard-_mount_node > div { width: 100%; } +#cbi-podkop-dashboard > h3 { + display: none; +} + +#cbi-podkop-settings > h3 { + display: none; +} + +#cbi-podkop-section > h3:nth-child(1) { + display: none; +} + +.cbi-section-remove { + margin-bottom: -32px; +} + +.cbi-value { + margin-bottom: 20px !important; +} + /* Dashboard styles */ .pdk_dashboard-page { 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 c84ff91..74f8766 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,82 +1,68 @@ 'use strict'; 'require view'; 'require form'; +'require baseclass'; 'require network'; -'require view.podkop.configSection as configSection'; -'require view.podkop.diagnosticTab as diagnosticTab'; -'require view.podkop.additionalTab as additionalTab'; -'require view.podkop.dashboardTab as dashboardTab'; -'require view.podkop.utils as utils'; 'require view.podkop.main as main'; -const EntryNode = { +// Settings content +'require view.podkop.settings as settings'; + +// Sections content +'require view.podkop.section as section'; + +// Dashboard content +'require view.podkop.dashboard as dashboard'; + + +const EntryPoint = { async render() { main.injectGlobalStyles(); - const podkopFormMap = new form.Map('podkop', '', null, ['main', 'extra']); + const podkopMap = new form.Map('podkop', _('Podkop Settings'), _('Configuration for Podkop service')); + // Enable tab views + podkopMap.tabbed = true; - // Main Section - const mainSection = podkopFormMap.section(form.TypedSection, 'main'); - mainSection.anonymous = true; - configSection.createConfigSection(mainSection); + // Settings tab + const settingsSection = podkopMap.section(form.TypedSection, 'settings', _('Settings')); + settingsSection.anonymous = true; + settingsSection.addremove = false; + // Make it named [ config settings 'settings' ] + settingsSection.cfgsections = function () { return ['settings']; }; - // Additional Settings Tab (main section) - additionalTab.createAdditionalSection(mainSection); + // Render settings content + settings.createSettingsContent(settingsSection); - // Diagnostics Tab (main section) - diagnosticTab.createDiagnosticsSection(mainSection); - const podkopFormMapPromise = podkopFormMap.render().then((node) => { - // Set up diagnostics event handlers - diagnosticTab.setupDiagnosticsEventHandlers(node); - // Start critical error polling for all tabs - utils.startErrorPolling(); + // Sections tab + const sectionsSection = podkopMap.section(form.TypedSection, 'section', _('Sections')); + sectionsSection.anonymous = false; + sectionsSection.addremove = true; + sectionsSection.template = 'cbi/simpleform'; - // Add event listener to keep error polling active when switching tabs - 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) { - // Ensure error polling continues when switching tabs - utils.startErrorPolling(); - } - }); - } + // Render section content + section.createSectionContent(sectionsSection); - // Add visibility change handler to manage error polling - document.addEventListener('visibilitychange', function () { - if (document.hidden) { - utils.stopErrorPolling(); - } else { - utils.startErrorPolling(); - } - }); - return node; - }); + // Dashboard tab + const dashboardSection = podkopMap.section(form.TypedSection, 'dashboard', _('Dashboard')); + dashboardSection.anonymous = true; + dashboardSection.addremove = false; + // dashboardSection.title = ''; + dashboardSection.cfgsections = function () { return ['dashboard']; }; + + // Render dashboard content + dashboard.createDashboardContent(dashboardSection); - // Extra Section - const extraSection = podkopFormMap.section( - form.TypedSection, - 'extra', - _('Extra configurations'), - ); - extraSection.anonymous = false; - extraSection.addremove = true; - extraSection.addbtntitle = _('Add Section'); - extraSection.multiple = true; - configSection.createConfigSection(extraSection); - // Initial dashboard render - dashboardTab.createDashboardSection(mainSection); // Inject core service main.coreService(); - return podkopFormMapPromise; - }, -}; + return podkopMap.render(); + } +} -return view.extend(EntryNode); + +return view.extend(EntryPoint); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js new file mode 100644 index 0000000..085a318 --- /dev/null +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js @@ -0,0 +1,586 @@ +'use strict'; +'require form'; +'require baseclass'; +'require tools.widgets as widgets'; +'require view.podkop.main as main'; + +function createSectionContent(section) { + let o = section.option( + form.ListValue, + 'mode', + _('Connection Type'), + _('Select between VPN and Proxy connection methods for traffic routing'), + ); + o.value('proxy', 'Proxy'); + o.value('vpn', 'VPN'); + o.value('block', 'Block'); + + + o = section.option( + form.ListValue, + 'proxy_config_type', + _('Configuration Type'), + _('Select how to configure the proxy'), + ); + o.value('url', _('Connection URL')); + o.value('outbound', _('Outbound Config')); + o.value('urltest', _('URLTest')); + o.default = 'url'; + o.depends('mode', 'proxy'); + + o = section.option( + form.TextValue, + 'proxy_string', + _('Proxy Configuration URL'), + '', + ); + o.depends('proxy_config_type', 'url'); + o.rows = 5; + // Enable soft wrapping for multi-line proxy URLs (e.g., for URLTest proxy links) + o.wrap = 'soft'; + // Render as a textarea to allow multiple proxy URLs/configs + o.textarea = true; + o.rmempty = false; + o.sectionDescriptions = new Map(); + o.placeholder = + 'vless://uuid@server:port?type=tcp&security=tls#main\n// backup ss://method:pass@server:port\n// backup2 vless://uuid@server:port?type=grpc&security=reality#alt\n// backup3 trojan://04agAQapcl@127.0.0.1:33641?type=tcp&security=none#trojan-tcp-none'; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + try { + const activeConfigs = main.splitProxyString(value); + + if (!activeConfigs.length) { + return _( + 'No active configuration found. One configuration is required.', + ); + } + + if (activeConfigs.length > 1) { + return _( + 'Multiply active configurations found. Please leave one configuration.', + ); + } + + const validation = main.validateProxyUrl(activeConfigs[0]); + + if (validation.valid) { + return true; + } + + return validation.message; + } catch (e) { + return `${_('Invalid URL format:')} ${e?.message}`; + } + }; + + o = section.option( + form.TextValue, + 'outbound_json', + _('Outbound Configuration'), + _('Enter complete outbound configuration in JSON format'), + ); + o.depends('proxy_config_type', 'outbound'); + o.rows = 10; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateOutboundJson(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.DynamicList, + 'urltest_proxy_links', + _('URLTest Proxy Links'), + ); + o.depends('proxy_config_type', 'urltest'); + o.placeholder = 'vless://, ss://, trojan:// links'; + o.rmempty = false; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateProxyUrl(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.Flag, + 'ss_uot', + _('Shadowsocks UDP over TCP'), + _('Apply for SS2022'), + ); + o.default = '0'; + o.depends('mode', 'proxy'); + o.rmempty = false; + + o = section.option( + widgets.DeviceSelect, + 'interface', + _('Network Interface'), + _('Select network interface for VPN connection'), + ); + o.depends('mode', 'vpn'); + o.noaliases = true; + o.nobridges = false; + o.noinactive = false; + o.filter = function (section_id, value) { + // Blocked interface names that should never be selectable + const blockedInterfaces = [ + 'br-lan', + 'eth0', + 'eth1', + 'wan', + 'phy0-ap0', + 'phy1-ap0', + 'pppoe-wan', + 'lan', + ]; + + // Reject immediately if the value matches any blocked interface + if (blockedInterfaces.includes(value)) { + return false; + } + + // Try to find the device object with the given name + const device = this.devices.find((dev) => dev.getName() === value); + + // If no device is found, allow the value + if (!device) { + return true; + } + + // Get the device type (e.g., "wifi", "ethernet", etc.) + const type = device.getType(); + + // Reject wireless-related devices + const isWireless = + type === 'wifi' || type === 'wireless' || type.includes('wlan'); + + return !isWireless; + }; + + o = section.option( + form.Flag, + 'domain_resolver_enabled', + _('Domain Resolver'), + _('Enable built-in DNS resolver for domains handled by this section'), + ); + o.default = '0'; + o.rmempty = false; + o.depends('mode', 'vpn'); + + o = section.option( + form.ListValue, + 'domain_resolver_dns_type', + _('DNS Protocol Type'), + _('Select the DNS protocol type for the domain resolver'), + ); + o.value('doh', _('DNS over HTTPS (DoH)')); + o.value('dot', _('DNS over TLS (DoT)')); + o.value('udp', _('UDP (Unprotected DNS)')); + o.default = 'udp'; + o.rmempty = false; + o.depends('domain_resolver_enabled', '1'); + + o = section.option( + form.Value, + 'domain_resolver_dns_server', + _('DNS Server'), + _('Select or enter DNS server address'), + ); + Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => { + o.value(key, _(label)); + }); + o.default = '8.8.8.8'; + o.rmempty = false; + o.depends('domain_resolver_enabled', '1'); + o.validate = function (section_id, value) { + const validation = main.validateDNS(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.Flag, + 'community_lists_enabled', + _('Community Lists'), + ); + o.default = '0'; + o.rmempty = false; + + o = section.option( + form.DynamicList, + 'community_lists', + _('Service List'), + _('Select predefined service for routing') + + ' github.com/itdoginfo/allow-domains', + ); + o.placeholder = 'Service list'; + Object.entries(main.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => { + o.value(key, _(label)); + }); + o.depends('community_lists_enabled', '1'); + o.rmempty = false; + + o = section.option( + form.ListValue, + 'user_domain_list_type', + _('User Domain List Type'), + _('Select how to add your custom domains'), + ); + o.value('disabled', _('Disabled')); + o.value('dynamic', _('Dynamic List')); + o.value('text', _('Text List')); + o.default = 'disabled'; + o.rmempty = false; + + o = section.option( + form.DynamicList, + 'user_domains', + _('User Domains'), + _( + 'Enter domain names without protocols (example: sub.example.com or example.com)', + ), + ); + o.placeholder = 'Domains list'; + o.depends('user_domain_list_type', 'dynamic'); + o.rmempty = false; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateDomain(value, true); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.TextValue, + 'user_domains_text', + _('User Domains List'), + _( + 'Enter domain names separated by comma, space or newline. You can add comments after //', + ), + ); + o.placeholder = + 'example.com, sub.example.com\n// Social networks\ndomain.com test.com // personal domains'; + o.depends('user_domain_list_type', 'text'); + o.rows = 8; + o.rmempty = false; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const domains = main.parseValueList(value); + + if (!domains.length) { + return _( + 'At least one valid domain must be specified. Comments-only content is not allowed.', + ); + } + + const { valid, results } = main.bulkValidate(domains, row => main.validateDomain(row, true)); + + if (!valid) { + const errors = results + .filter((validation) => !validation.valid) // Leave only failed validations + .map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors + + return [_('Validation errors:'), ...errors].join('\n'); + } + + return true; + }; + + o = section.option( + form.Flag, + 'local_domain_lists_enabled', + _('Local Domain Lists'), + _('Use the list from the router filesystem'), + ); + o.default = '0'; + o.rmempty = false; + + o = section.option( + form.DynamicList, + 'local_domain_lists', + _('Local Domain List Paths'), + _('Enter the list file path'), + ); + o.placeholder = '/path/file.lst'; + o.depends('local_domain_lists_enabled', '1'); + o.rmempty = false; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validatePath(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.Flag, + 'remote_domain_lists_enabled', + _('Remote Domain Lists'), + _('Download and use domain lists from remote URLs'), + ); + o.default = '0'; + o.rmempty = false; + + o = section.option( + form.DynamicList, + 'remote_domain_lists', + _('Remote Domain URLs'), + _('Enter full URLs starting with http:// or https://'), + ); + o.placeholder = 'URL'; + o.depends('remote_domain_lists_enabled', '1'); + o.rmempty = false; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateUrl(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.Flag, + 'local_subnet_lists_enabled', + _('Local Subnet Lists'), + _('Use the list from the router filesystem'), + ); + o.default = '0'; + o.rmempty = false; + + o = section.option( + form.DynamicList, + 'local_subnet_lists', + _('Local Subnet List Paths'), + _('Enter the list file path'), + ); + o.placeholder = '/path/file.lst'; + o.depends('local_subnet_lists_enabled', '1'); + o.rmempty = false; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validatePath(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.ListValue, + 'user_subnet_list_type', + _('User Subnet List Type'), + _('Select how to add your custom subnets'), + ); + o.value('disabled', _('Disabled')); + o.value('dynamic', _('Dynamic List')); + o.value('text', _('Text List (comma/space/newline separated)')); + o.default = 'disabled'; + o.rmempty = false; + + o = section.option( + form.DynamicList, + 'user_subnets', + _('User Subnets'), + _( + 'Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses', + ), + ); + o.placeholder = 'IP or subnet'; + o.depends('user_subnet_list_type', 'dynamic'); + o.rmempty = false; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateSubnet(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.TextValue, + 'user_subnets_text', + _('User Subnets List'), + _( + 'Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //', + ), + ); + o.placeholder = + '103.21.244.0/22\n// Google DNS\n8.8.8.8\n1.1.1.1/32, 9.9.9.9 // Cloudflare and Quad9'; + o.depends('user_subnet_list_type', 'text'); + o.rows = 10; + o.rmempty = false; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const subnets = main.parseValueList(value); + + if (!subnets.length) { + return _( + 'At least one valid subnet or IP must be specified. Comments-only content is not allowed.', + ); + } + + const { valid, results } = main.bulkValidate(subnets, main.validateSubnet); + + if (!valid) { + const errors = results + .filter((validation) => !validation.valid) // Leave only failed validations + .map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors + + return [_('Validation errors:'), ...errors].join('\n'); + } + + return true; + }; + + o = section.option( + form.Flag, + 'remote_subnet_lists_enabled', + _('Remote Subnet Lists'), + _('Download and use subnet lists from remote URLs'), + ); + o.default = '0'; + o.rmempty = false; + + o = section.option( + form.DynamicList, + 'remote_subnet_lists', + _('Remote Subnet URLs'), + _('Enter full URLs starting with http:// or https://'), + ); + o.placeholder = 'URL'; + o.depends('remote_subnet_lists_enabled', '1'); + o.rmempty = false; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateUrl(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.Flag, + 'all_traffic_from_ip_enabled', + _('IP for full redirection'), + _( + 'Specify local IP addresses whose traffic will always use the configured route', + ), + ); + o.default = '0'; + o.rmempty = false; + + o = section.option( + form.DynamicList, + 'all_traffic_ip', + _('Local IPs'), + _('Enter valid IPv4 addresses'), + ); + o.placeholder = 'IP'; + o.depends('all_traffic_from_ip_enabled', '1'); + o.rmempty = false; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateSubnet(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.Flag, + 'socks5', + _('Mixed enable'), + _('Browser port: 2080'), + ); + o.default = '0'; + o.rmempty = false; +} + +const EntryPoint = { + createSectionContent, +} + +return baseclass.extend(EntryPoint); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js new file mode 100644 index 0000000..0aeb3c7 --- /dev/null +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js @@ -0,0 +1,283 @@ +'use strict'; +'require form'; +'require baseclass'; +'require tools.widgets as widgets'; +'require view.podkop.main as main'; + +function createSettingsContent(section) { + let o = section.option( + form.Flag, + 'yacd', + _('Yacd enable'), + `${main.getBaseUrl()}:9090/ui`, + ); + o.default = '0'; + o.rmempty = false; + + o = section.option( + form.Flag, + 'exclude_ntp', + _('Exclude NTP'), + _('Allows you to exclude NTP protocol traffic from the tunnel'), + ); + o.default = '0'; + o.rmempty = false; + + o = section.option( + form.Flag, + 'quic_disable', + _('QUIC disable'), + _('For issues with the video stream'), + ); + o.default = '0'; + o.rmempty = false; + + o = section.option( + form.ListValue, + 'update_interval', + _('List Update Frequency'), + _('Select how often the lists will be updated'), + ); + Object.entries(main.UPDATE_INTERVAL_OPTIONS).forEach(([key, label]) => { + o.value(key, _(label)); + }); + o.default = '1d'; + o.rmempty = false; + + o = section.option( + form.ListValue, + 'dns_type', + _('DNS Protocol Type'), + _('Select DNS protocol to use'), + ); + o.value('doh', _('DNS over HTTPS (DoH)')); + o.value('dot', _('DNS over TLS (DoT)')); + o.value('udp', _('UDP (Unprotected DNS)')); + o.default = 'udp'; + o.rmempty = false; + + o = section.option( + form.Value, + 'dns_server', + _('DNS Server'), + _('Select or enter DNS server address'), + ); + Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => { + o.value(key, _(label)); + }); + o.default = '8.8.8.8'; + o.rmempty = false; + o.validate = function (section_id, value) { + const validation = main.validateDNS(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.Value, + 'bootstrap_dns_server', + _('Bootstrap DNS server'), + _( + 'The DNS server used to look up the IP address of an upstream DNS server', + ), + ); + Object.entries(main.BOOTSTRAP_DNS_SERVER_OPTIONS).forEach(([key, label]) => { + o.value(key, _(label)); + }); + o.default = '77.88.8.8'; + o.rmempty = false; + o.validate = function (section_id, value) { + const validation = main.validateDNS(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.Value, + 'dns_rewrite_ttl', + _('DNS Rewrite TTL'), + _('Time in seconds for DNS record caching (default: 60)'), + ); + o.default = '60'; + o.rmempty = false; + o.validate = function (section_id, value) { + if (!value) { + return _('TTL value cannot be empty'); + } + + const ttl = parseInt(value); + if (isNaN(ttl) || ttl < 0) { + return _('TTL must be a positive number'); + } + + return true; + }; + + o = section.option( + form.ListValue, + 'config_path', + _('Config File Path'), + _( + 'Select path for sing-box config file. Change this ONLY if you know what you are doing', + ), + ); + o.value('/etc/sing-box/config.json', 'Flash (/etc/sing-box/config.json)'); + o.value('/tmp/sing-box/config.json', 'RAM (/tmp/sing-box/config.json)'); + o.default = '/etc/sing-box/config.json'; + o.rmempty = false; + + o = section.option( + form.Value, + 'cache_path', + _('Cache File Path'), + _( + 'Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing', + ), + ); + o.value('/tmp/sing-box/cache.db', 'RAM (/tmp/sing-box/cache.db)'); + o.value( + '/usr/share/sing-box/cache.db', + 'Flash (/usr/share/sing-box/cache.db)', + ); + o.default = '/tmp/sing-box/cache.db'; + o.rmempty = false; + o.validate = function (section_id, value) { + if (!value) { + return _('Cache file path cannot be empty'); + } + + if (!value.startsWith('/')) { + return _('Path must be absolute (start with /)'); + } + + if (!value.endsWith('cache.db')) { + return _('Path must end with cache.db'); + } + + const parts = value.split('/').filter(Boolean); + if (parts.length < 2) { + return _('Path must contain at least one directory (like /tmp/cache.db)'); + } + + return true; + }; + + o = section.option( + widgets.DeviceSelect, + 'iface', + _('Source Network Interface'), + _('Select the network interface from which the traffic will originate'), + ); + o.default = 'br-lan'; + o.noaliases = true; + o.nobridges = false; + o.noinactive = false; + o.multiple = true; + o.filter = function (section_id, value) { + // Block specific interface names from being selectable + const blocked = ['wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan']; + if (blocked.includes(value)) { + return false; + } + + // Try to find the device object by its name + const device = this.devices.find((dev) => dev.getName() === value); + + // If no device is found, allow the value + if (!device) { + return true; + } + + // Check the type of the device + const type = device.getType(); + + // Consider any Wi-Fi / wireless / wlan device as invalid + const isWireless = + type === 'wifi' || type === 'wireless' || type.includes('wlan'); + + // Allow only non-wireless devices + return !isWireless; + }; + + o = section.option( + form.Flag, + 'mon_restart_ifaces', + _('Interface monitoring'), + _('Interface monitoring for bad WAN'), + ); + o.default = '0'; + o.rmempty = false; + + o = section.option( + widgets.NetworkSelect, + 'restart_ifaces', + _('Interface for monitoring'), + _('Select the WAN interfaces to be monitored'), + ); + o.depends('mon_restart_ifaces', '1'); + o.multiple = true; + o.filter = function (section_id, value) { + // Reject if the value is in the blocked list ['lan', 'loopback'] + if (['lan', 'loopback'].includes(value)) { + return false; + } + + // Reject if the value starts with '@' (means it's an alias/reference) + if (value.startsWith('@')) { + return false; + } + + // Otherwise allow it + return true; + }; + + o = section.option( + form.Value, + 'procd_reload_delay', + _('Interface Monitoring Delay'), + _('Delay in milliseconds before reloading podkop after interface UP'), + ); + o.depends('mon_restart_ifaces', '1'); + o.default = '2000'; + o.rmempty = false; + o.validate = function (section_id, value) { + if (!value) { + return _('Delay value cannot be empty'); + } + return true; + }; + + o = section.option( + form.Flag, + 'dont_touch_dhcp', + _('Dont touch my DHCP!'), + _('Podkop will not change the DHCP config'), + ); + o.default = '0'; + o.rmempty = false; + + o = section.option( + form.Flag, + 'detour', + _('Proxy download of lists'), + _('Downloading all lists via main Proxy/VPN'), + ); + o.default = '0'; + o.rmempty = false; + + +} + +const EntryPoint = { + createSettingsContent, +} + +return baseclass.extend(EntryPoint); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/utils.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/utils.js deleted file mode 100644 index f358670..0000000 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/utils.js +++ /dev/null @@ -1,163 +0,0 @@ -'use strict'; -'require baseclass'; -'require ui'; -'require fs'; -'require view.podkop.main as main'; - -// Flag to track if this is the first error check -let isInitialCheck = true; - -// Set to track which errors we've already seen -const lastErrorsSet = new Set(); - -// Timer for periodic error polling -let errorPollTimer = null; - -// Helper function to fetch errors from the podkop command -async function getPodkopErrors() { - return new Promise((resolve) => { - safeExec('/usr/bin/podkop', ['check_logs'], 'P0_PRIORITY', (result) => { - if (!result || !result.stdout) return resolve([]); - - const logs = result.stdout.split('\n'); - const errors = logs.filter((log) => log.includes('[critical]')); - - resolve(errors); - }); - }); -} - -// Show error notification to the user -function showErrorNotification(error, isMultiple = false) { - const notificationContent = E('div', { class: 'alert-message error' }, [ - E('pre', { class: 'error-log' }, error), - ]); - - ui.addNotification(null, notificationContent); -} - -// Helper function for command execution with prioritization -function safeExec( - command, - args, - priority, - callback, - timeout = main.COMMAND_TIMEOUT, -) { - // Default to highest priority execution if priority is not provided or invalid - let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY; - - // If priority is a string, try to get the corresponding delay value - if ( - typeof priority === 'string' && - main.COMMAND_SCHEDULING[priority] !== undefined - ) { - schedulingDelay = main.COMMAND_SCHEDULING[priority]; - } - - const executeCommand = async () => { - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); - - const result = await Promise.race([ - fs.exec(command, args), - new Promise((_, reject) => { - controller.signal.addEventListener('abort', () => { - reject(new Error('Command execution timed out')); - }); - }), - ]); - - clearTimeout(timeoutId); - - if (callback && typeof callback === 'function') { - callback(result); - } - - return result; - } catch (error) { - console.warn( - `Command execution failed or timed out: ${command} ${args.join(' ')}`, - ); - const errorResult = { stdout: '', stderr: error.message, error: error }; - - if (callback && typeof callback === 'function') { - callback(errorResult); - } - - return errorResult; - } - }; - - if (callback && typeof callback === 'function') { - setTimeout(executeCommand, schedulingDelay); - return; - } else { - return executeCommand(); - } -} - -// Check for critical errors and show notifications -async function checkForCriticalErrors() { - try { - const errors = await getPodkopErrors(); - - if (errors && errors.length > 0) { - // Filter out errors we've already seen - const newErrors = errors.filter((error) => !lastErrorsSet.has(error)); - - if (newErrors.length > 0) { - // On initial check, just store errors without showing notifications - if (!isInitialCheck) { - // Show each new error as a notification - newErrors.forEach((error) => { - showErrorNotification(error, newErrors.length > 1); - }); - } - - // Add new errors to our set of seen errors - newErrors.forEach((error) => lastErrorsSet.add(error)); - } - } - - // After first check, mark as no longer initial - isInitialCheck = false; - } catch (error) { - console.error('Error checking for critical messages:', error); - } -} - -// Start polling for errors at regular intervals -function startErrorPolling() { - if (errorPollTimer) { - clearInterval(errorPollTimer); - } - - // Reset initial check flag to make sure we show errors - isInitialCheck = false; - - // Immediately check for errors on start - checkForCriticalErrors(); - - // Then set up periodic checks - errorPollTimer = setInterval( - checkForCriticalErrors, - main.ERROR_POLL_INTERVAL, - ); -} - -// Stop polling for errors -function stopErrorPolling() { - if (errorPollTimer) { - clearInterval(errorPollTimer); - errorPollTimer = null; - } -} - -return baseclass.extend({ - startErrorPolling, - stopErrorPolling, - checkForCriticalErrors, - safeExec, -});