fix: run prettier for luci app js assets

This commit is contained in:
divocat
2025-10-05 16:12:56 +03:00
parent d9a4f50f62
commit eb60e6edec
4 changed files with 2216 additions and 1484 deletions

View File

@@ -5,235 +5,358 @@
'require view.podkop.main as main'; 'require view.podkop.main as main';
function createAdditionalSection(mainSection) { function createAdditionalSection(mainSection) {
let o = mainSection.tab('additional', _('Additional Settings')); let o = mainSection.tab('additional', _('Additional Settings'));
o = mainSection.taboption('additional', form.Flag, 'yacd', _('Yacd enable'), `<a href="${main.getBaseUrl()}:9090/ui" target="_blank">${main.getBaseUrl()}:9090/ui</a>`); o = mainSection.taboption(
o.default = '0'; 'additional',
o.rmempty = false; form.Flag,
o.ucisection = 'main'; 'yacd',
_('Yacd enable'),
`<a href="${main.getBaseUrl()}:9090/ui" target="_blank">${main.getBaseUrl()}:9090/ui</a>`,
);
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption('additional', form.Flag, 'exclude_ntp', _('Exclude NTP'), _('Allows you to exclude NTP protocol traffic from the tunnel')); o = mainSection.taboption(
o.default = '0'; 'additional',
o.rmempty = false; form.Flag,
o.ucisection = 'main'; '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 = mainSection.taboption(
o.default = '0'; 'additional',
o.rmempty = false; form.Flag,
o.ucisection = 'main'; 'quic_disable',
_('QUIC disable'),
_('For issues with the video stream'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption('additional', form.ListValue, 'update_interval', _('List Update Frequency'), _('Select how often the lists will be updated')); o = mainSection.taboption(
Object.entries(main.UPDATE_INTERVAL_OPTIONS).forEach(([key, label]) => { 'additional',
o.value(key, _(label)); form.ListValue,
}); 'update_interval',
o.default = '1d'; _('List Update Frequency'),
o.rmempty = false; _('Select how often the lists will be updated'),
o.ucisection = 'main'; );
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 = mainSection.taboption(
o.value('doh', _('DNS over HTTPS (DoH)')); 'additional',
o.value('dot', _('DNS over TLS (DoT)')); form.ListValue,
o.value('udp', _('UDP (Unprotected DNS)')); 'dns_type',
o.default = 'udp'; _('DNS Protocol Type'),
o.rmempty = false; _('Select DNS protocol to use'),
o.ucisection = 'main'; );
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')); o = mainSection.taboption(
Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => { 'additional',
o.value(key, _(label)); form.Value,
}); 'dns_server',
o.default = '8.8.8.8'; _('DNS Server'),
o.rmempty = false; _('Select or enter DNS server address'),
o.ucisection = 'main'; );
o.validate = function (section_id, value) { Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => {
const validation = main.validateDNS(value); 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) { if (validation.valid) {
return true; return true;
} }
return _(validation.message); return _(validation.message);
}; };
o = mainSection.taboption('additional', form.Value, 'bootstrap_dns_server', _('Bootstrap DNS server'), _('The DNS server used to look up the IP address of an upstream DNS server')); o = mainSection.taboption(
Object.entries(main.BOOTSTRAP_DNS_SERVER_OPTIONS).forEach(([key, label]) => { 'additional',
o.value(key, _(label)); form.Value,
}); 'bootstrap_dns_server',
o.default = '77.88.8.8'; _('Bootstrap DNS server'),
o.rmempty = false; _(
o.ucisection = 'main'; 'The DNS server used to look up the IP address of an upstream DNS server',
o.validate = function (section_id, value) { ),
const validation = main.validateDNS(value); );
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) { if (validation.valid) {
return true; return true;
} }
return _(validation.message); return _(validation.message);
}; };
o = mainSection.taboption('additional', form.Value, 'dns_rewrite_ttl', _('DNS Rewrite TTL'), _('Time in seconds for DNS record caching (default: 60)')); o = mainSection.taboption(
o.default = '60'; 'additional',
o.rmempty = false; form.Value,
o.ucisection = 'main'; 'dns_rewrite_ttl',
o.validate = function (section_id, value) { _('DNS Rewrite TTL'),
if (!value) { _('Time in seconds for DNS record caching (default: 60)'),
return _('TTL value cannot be empty'); );
} 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); const ttl = parseInt(value);
if (isNaN(ttl) || ttl < 0) { if (isNaN(ttl) || ttl < 0) {
return _('TTL must be a positive number'); return _('TTL must be a positive number');
} }
return true; return true;
}; };
o = mainSection.taboption('additional', form.ListValue, 'config_path', _('Config File Path'), _('Select path for sing-box config file. Change this ONLY if you know what you are doing')); o = mainSection.taboption(
o.value('/etc/sing-box/config.json', 'Flash (/etc/sing-box/config.json)'); 'additional',
o.value('/tmp/sing-box/config.json', 'RAM (/tmp/sing-box/config.json)'); form.ListValue,
o.default = '/etc/sing-box/config.json'; 'config_path',
o.rmempty = false; _('Config File Path'),
o.ucisection = 'main'; _(
'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 = mainSection.taboption(
o.value('/tmp/sing-box/cache.db', 'RAM (/tmp/sing-box/cache.db)'); 'additional',
o.value('/usr/share/sing-box/cache.db', 'Flash (/usr/share/sing-box/cache.db)'); form.Value,
o.default = '/tmp/sing-box/cache.db'; 'cache_path',
o.rmempty = false; _('Cache File Path'),
o.ucisection = 'main'; _(
o.validate = function (section_id, value) { 'Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing',
if (!value) { ),
return _('Cache file path cannot be empty'); );
} 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('/')) { if (!value.startsWith('/')) {
return _('Path must be absolute (start with /)'); return _('Path must be absolute (start with /)');
} }
if (!value.endsWith('cache.db')) { if (!value.endsWith('cache.db')) {
return _('Path must end with cache.db'); return _('Path must end with cache.db');
} }
const parts = value.split('/').filter(Boolean); const parts = value.split('/').filter(Boolean);
if (parts.length < 2) { if (parts.length < 2) {
return _('Path must contain at least one directory (like /tmp/cache.db)'); return _('Path must contain at least one directory (like /tmp/cache.db)');
} }
return true; return true;
}; };
o = mainSection.taboption('additional', widgets.DeviceSelect, 'iface', _('Source Network Interface'), _('Select the network interface from which the traffic will originate')); o = mainSection.taboption(
o.ucisection = 'main'; 'additional',
o.default = 'br-lan'; widgets.DeviceSelect,
o.noaliases = true; 'iface',
o.nobridges = false; _('Source Network Interface'),
o.noinactive = false; _('Select the network interface from which the traffic will originate'),
o.multiple = true; );
o.filter = function (section_id, value) { o.ucisection = 'main';
// Block specific interface names from being selectable o.default = 'br-lan';
const blocked = ['wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan']; o.noaliases = true;
if (blocked.includes(value)) { o.nobridges = false;
return 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 // Try to find the device object by its name
const device = this.devices.find(dev => dev.getName() === value); const device = this.devices.find((dev) => dev.getName() === value);
// If no device is found, allow the value // If no device is found, allow the value
if (!device) { if (!device) {
return true; return true;
} }
// Check the type of the device // Check the type of the device
const type = device.getType(); const type = device.getType();
// Consider any Wi-Fi / wireless / wlan device as invalid // Consider any Wi-Fi / wireless / wlan device as invalid
const isWireless = const isWireless =
type === 'wifi' || type === 'wireless' || type.includes('wlan'); type === 'wifi' || type === 'wireless' || type.includes('wlan');
// Allow only non-wireless devices // Allow only non-wireless devices
return !isWireless; return !isWireless;
}; };
o = mainSection.taboption('additional', form.Flag, 'mon_restart_ifaces', _('Interface monitoring'), _('Interface monitoring for bad WAN')); o = mainSection.taboption(
o.default = '0'; 'additional',
o.rmempty = false; form.Flag,
o.ucisection = 'main'; '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 = mainSection.taboption(
o.ucisection = 'main'; 'additional',
o.depends('mon_restart_ifaces', '1'); widgets.NetworkSelect,
o.multiple = true; 'restart_ifaces',
o.filter = function (section_id, value) { _('Interface for monitoring'),
// Reject if the value is in the blocked list ['lan', 'loopback'] _('Select the WAN interfaces to be monitored'),
if (['lan', 'loopback'].includes(value)) { );
return false; 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) // Reject if the value starts with '@' (means it's an alias/reference)
if (value.startsWith('@')) { if (value.startsWith('@')) {
return false; return false;
} }
// Otherwise allow it // Otherwise allow it
return true; return true;
}; };
o = mainSection.taboption('additional', form.Value, 'procd_reload_delay', _('Interface Monitoring Delay'), _('Delay in milliseconds before reloading podkop after interface UP')); o = mainSection.taboption(
o.ucisection = 'main'; 'additional',
o.depends('mon_restart_ifaces', '1'); form.Value,
o.default = '2000'; 'procd_reload_delay',
o.rmempty = false; _('Interface Monitoring Delay'),
o.validate = function (section_id, value) { _('Delay in milliseconds before reloading podkop after interface UP'),
if (!value) { );
return _('Delay value cannot be empty'); o.ucisection = 'main';
} o.depends('mon_restart_ifaces', '1');
return true; 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 = mainSection.taboption(
o.default = '0'; 'additional',
o.rmempty = false; form.Flag,
o.ucisection = 'main'; '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 = mainSection.taboption(
o.default = '0'; 'additional',
o.rmempty = false; form.Flag,
o.ucisection = 'main'; '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) // 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 = mainSection.taboption(
o.default = '0'; 'basic',
o.rmempty = false; form.Flag,
o.ucisection = 'main'; '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 = mainSection.taboption(
o.placeholder = 'IP'; 'basic',
o.depends('exclude_from_ip_enabled', '1'); form.DynamicList,
o.rmempty = false; 'exclude_traffic_ip',
o.ucisection = 'main'; _('Local IPs'),
o.validate = function (section_id, value) { _('Enter valid IPv4 addresses'),
// Optional );
if (!value || value.length === 0) { o.placeholder = 'IP';
return true o.depends('exclude_from_ip_enabled', '1');
} o.rmempty = false;
o.ucisection = 'main';
o.validate = function (section_id, value) {
// Optional
if (!value || value.length === 0) {
return true;
}
const validation = main.validateIPV4(value); const validation = main.validateIPV4(value);
if (validation.valid) { if (validation.valid) {
return true; return true;
} }
return _(validation.message) return _(validation.message);
}; };
o = mainSection.taboption('basic', form.Flag, 'socks5', _('Mixed enable'), _('Browser port: 2080')); o = mainSection.taboption(
o.default = '0'; 'basic',
o.rmempty = false; form.Flag,
o.ucisection = 'main'; 'socks5',
_('Mixed enable'),
_('Browser port: 2080'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
} }
return baseclass.extend({ return baseclass.extend({
createAdditionalSection createAdditionalSection,
}); });

View File

@@ -15,138 +15,149 @@ let errorPollTimer = null;
// Helper function to fetch errors from the podkop command // Helper function to fetch errors from the podkop command
async function getPodkopErrors() { async function getPodkopErrors() {
return new Promise(resolve => { return new Promise((resolve) => {
safeExec('/usr/bin/podkop', ['check_logs'], 'P0_PRIORITY', result => { safeExec('/usr/bin/podkop', ['check_logs'], 'P0_PRIORITY', (result) => {
if (!result || !result.stdout) return resolve([]); if (!result || !result.stdout) return resolve([]);
const logs = result.stdout.split('\n'); const logs = result.stdout.split('\n');
const errors = logs.filter(log => const errors = logs.filter((log) => log.includes('[critical]'));
log.includes('[critical]')
);
resolve(errors); resolve(errors);
});
}); });
});
} }
// Show error notification to the user // Show error notification to the user
function showErrorNotification(error, isMultiple = false) { function showErrorNotification(error, isMultiple = false) {
const notificationContent = E('div', { 'class': 'alert-message error' }, [ const notificationContent = E('div', { class: 'alert-message error' }, [
E('pre', { 'class': 'error-log' }, error) E('pre', { class: 'error-log' }, error),
]); ]);
ui.addNotification(null, notificationContent); ui.addNotification(null, notificationContent);
} }
// Helper function for command execution with prioritization // Helper function for command execution with prioritization
function safeExec(command, args, priority, callback, timeout = main.COMMAND_TIMEOUT) { function safeExec(
// Default to highest priority execution if priority is not provided or invalid command,
let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY; 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 priority is a string, try to get the corresponding delay value
if (typeof priority === 'string' && main.COMMAND_SCHEDULING[priority] !== undefined) { if (
schedulingDelay = main.COMMAND_SCHEDULING[priority]; typeof priority === 'string' &&
main.COMMAND_SCHEDULING[priority] !== undefined
) {
schedulingDelay = main.COMMAND_SCHEDULING[priority];
}
const executeCommand = async () => {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const result = await Promise.race([
fs.exec(command, args),
new Promise((_, reject) => {
controller.signal.addEventListener('abort', () => {
reject(new Error('Command execution timed out'));
});
}),
]);
clearTimeout(timeoutId);
if (callback && typeof callback === 'function') {
callback(result);
}
return result;
} catch (error) {
console.warn(
`Command execution failed or timed out: ${command} ${args.join(' ')}`,
);
const errorResult = { stdout: '', stderr: error.message, error: error };
if (callback && typeof callback === 'function') {
callback(errorResult);
}
return errorResult;
} }
};
const executeCommand = async () => { if (callback && typeof callback === 'function') {
try { setTimeout(executeCommand, schedulingDelay);
const controller = new AbortController(); return;
const timeoutId = setTimeout(() => controller.abort(), timeout); } else {
return executeCommand();
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 // Check for critical errors and show notifications
async function checkForCriticalErrors() { async function checkForCriticalErrors() {
try { try {
const errors = await getPodkopErrors(); const errors = await getPodkopErrors();
if (errors && errors.length > 0) { if (errors && errors.length > 0) {
// Filter out errors we've already seen // Filter out errors we've already seen
const newErrors = errors.filter(error => !lastErrorsSet.has(error)); const newErrors = errors.filter((error) => !lastErrorsSet.has(error));
if (newErrors.length > 0) { if (newErrors.length > 0) {
// On initial check, just store errors without showing notifications // On initial check, just store errors without showing notifications
if (!isInitialCheck) { if (!isInitialCheck) {
// Show each new error as a notification // Show each new error as a notification
newErrors.forEach(error => { newErrors.forEach((error) => {
showErrorNotification(error, newErrors.length > 1); 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 // Add new errors to our set of seen errors
isInitialCheck = false; newErrors.forEach((error) => lastErrorsSet.add(error));
} catch (error) { }
console.error('Error checking for critical messages:', 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 // Start polling for errors at regular intervals
function startErrorPolling() { function startErrorPolling() {
if (errorPollTimer) { if (errorPollTimer) {
clearInterval(errorPollTimer); clearInterval(errorPollTimer);
} }
// Reset initial check flag to make sure we show errors // Reset initial check flag to make sure we show errors
isInitialCheck = false; isInitialCheck = false;
// Immediately check for errors on start // Immediately check for errors on start
checkForCriticalErrors(); checkForCriticalErrors();
// Then set up periodic checks // Then set up periodic checks
errorPollTimer = setInterval(checkForCriticalErrors, main.ERROR_POLL_INTERVAL); errorPollTimer = setInterval(
checkForCriticalErrors,
main.ERROR_POLL_INTERVAL,
);
} }
// Stop polling for errors // Stop polling for errors
function stopErrorPolling() { function stopErrorPolling() {
if (errorPollTimer) { if (errorPollTimer) {
clearInterval(errorPollTimer); clearInterval(errorPollTimer);
errorPollTimer = null; errorPollTimer = null;
} }
} }
return baseclass.extend({ return baseclass.extend({
startErrorPolling, startErrorPolling,
stopErrorPolling, stopErrorPolling,
checkForCriticalErrors, checkForCriticalErrors,
safeExec safeExec,
}); });