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,
-});