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 6bc649f..d63ecc6 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 @@ -5,7 +5,12 @@ 'require network'; 'require fs'; -// Add helper function for safe command execution with timeout +const STATUS_COLORS = { + SUCCESS: '#4caf50', + ERROR: '#f44336', + WARNING: '#ff9800' +}; + async function safeExec(command, args = [], timeout = 3000) { try { const controller = new AbortController(); @@ -31,8 +36,8 @@ async function safeExec(command, args = [], timeout = 3000) { function formatDiagnosticOutput(output) { if (typeof output !== 'string') return ''; return output.trim() - .replace(/\x1b\[[0-9;]*m/g, '') // Remove ANSI color codes - .replace(/\r\n/g, '\n') // Normalize line endings + .replace(/\x1b\[[0-9;]*m/g, '') + .replace(/\r\n/g, '\n') .replace(/\r/g, '\n'); } @@ -53,7 +58,6 @@ function getNetworkInterfaces(o) { }); } -// Общая функция для создания конфигурационных секций function createConfigSection(section, map, network) { const s = section; @@ -71,24 +75,37 @@ function createConfigSection(section, map, network) { o.depends('mode', 'proxy'); o.ucisection = s.section; - o = s.taboption('basic', form.TextValue, 'proxy_string', _('Proxy Configuration URL'), _('Enter connection string starting with vless:// or ss:// for proxy configuration')); + o = s.taboption('basic', form.TextValue, 'proxy_string', _('Proxy Configuration URL'), ''); o.depends('proxy_config_type', 'url'); o.rows = 5; o.ucisection = s.section; - o.load = function (section_id) { - return safeExec('/etc/init.d/podkop', ['get_proxy_label', section_id]).then(res => { - if (res.stdout) { - try { - const decodedLabel = decodeURIComponent(res.stdout.trim()); - this.description = _('Current config: ') + decodedLabel; - } catch (e) { - console.error('Error decoding label:', e); - this.description = _('Current config: ') + res.stdout.trim(); - } + o.sectionDescriptions = new Map(); + + 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 label = cfgvalue.split('#').pop() || 'unnamed'; + const decodedLabel = decodeURIComponent(label); + const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Current config: ') + decodedLabel); + container.appendChild(descDiv); + } catch (e) { + console.error('Error parsing config label:', e); + const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Current config: ') + (cfgvalue.split('#').pop() || 'unnamed')); + container.appendChild(descDiv); } - return this.super('load', section_id); - }); + } else { + const defaultDesc = E('div', { 'class': 'cbi-value-description' }, + _('Enter connection string starting with vless:// or ss:// for proxy configuration')); + container.appendChild(defaultDesc); + } + + return container; }; + o.validate = function (section_id, value) { if (!value || value.length === 0) { return true; @@ -357,13 +374,7 @@ function createConfigSection(section, map, network) { o.ucisection = s.section; o.validate = function (section_id, value) { if (!value || value.length === 0) return true; - try { - const url = new URL(value); - if (!['http:', 'https:'].includes(url.protocol)) return _('URL must use http:// or https:// protocol'); - return true; - } catch (e) { - return _('Invalid URL format. URL must start with http:// or https://'); - } + return validateUrl(value); }; o = s.taboption('basic', form.ListValue, 'custom_subnets_list_enabled', _('User Subnet List Type'), _('Select how to add your custom subnets')); @@ -434,13 +445,7 @@ function createConfigSection(section, map, network) { o.ucisection = s.section; o.validate = function (section_id, value) { if (!value || value.length === 0) return true; - try { - const url = new URL(value); - if (!['http:', 'https:'].includes(url.protocol)) return _('URL must use http:// or https:// protocol'); - return true; - } catch (e) { - return _('Invalid URL format. URL must start with http:// or https://'); - } + return validateUrl(value); }; o = s.taboption('basic', form.Flag, 'all_traffic_from_ip_enabled', _('IP for full redirection'), _('Specify local IP addresses whose traffic will always use the configured route')); @@ -466,9 +471,225 @@ function createConfigSection(section, map, network) { }; } +// Utility functions +const copyToClipboard = (text, button) => { + const textarea = document.createElement('textarea'); + textarea.value = text; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand('copy'); + const originalText = button.textContent; + button.textContent = _('Copied!'); + setTimeout(() => button.textContent = originalText, 1000); + } catch (err) { + ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message)); + } + document.body.removeChild(textarea); +}; + +const validateUrl = (url, protocols = ['http:', 'https:']) => { + try { + const parsedUrl = new URL(url); + if (!protocols.includes(parsedUrl.protocol)) { + return _('URL must use one of the following protocols: ') + protocols.join(', '); + } + return true; + } catch (e) { + return _('Invalid URL format'); + } +}; + +// UI Helper functions +const createModalContent = (title, content) => { + return [ + E('div', { + 'class': 'panel-body', + style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; ' + + 'font-family: monospace; white-space: pre-wrap; word-wrap: break-word; ' + + 'line-height: 1.5; font-size: 14px;' + }, [ + E('pre', { style: 'margin: 0;' }, content) + ]), + E('div', { + 'class': 'right', + style: 'margin-top: 1em;' + }, [ + E('button', { + 'class': 'btn', + 'click': ev => copyToClipboard('```txt\n' + content + '\n```', ev.target) + }, _('Copy to Clipboard')), + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Close')) + ]) + ]; +}; + +const showConfigModal = async (command, title) => { + const res = await safeExec('/etc/init.d/podkop', [command]); + const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); + ui.showModal(_(title), createModalContent(title, formattedOutput)); +}; + +// 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('/etc/init.d/podkop', [config.action]) + .then(() => config.reload && location.reload()), + style: config.style + }); + }, + + createModalButton: function (config) { + return this.createButton({ + label: config.label, + onClick: () => showConfigModal(config.command, config.title), + style: config.style + }); + } +}; + +// Status Panel Factory +const createStatusPanel = (title, status, buttons) => { + const headerContent = [ + E('strong', {}, _(title)), + status && E('br'), + status && E('span', { + 'style': `color: ${status.running ? STATUS_COLORS.SUCCESS : STATUS_COLORS.ERROR}` + }, [ + status.running ? '✔' : '✘', + ' ', + status.status + ]) + ].filter(Boolean); + + return E('div', { + 'class': 'panel', + 'style': 'flex: 1; padding: 15px;' + }, [ + E('div', { 'class': 'panel-heading' }, headerContent), + E('div', { + 'class': 'panel-body', + 'style': 'display: flex; flex-direction: column; gap: 8px;' + }, buttons) + ]); +}; + +// Update the status section creation +let createStatusSection = function (podkopStatus, singboxStatus, podkop, luci, singbox, system, fakeipStatus) { + return E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Service Status')), + E('div', { 'class': 'table', style: 'display: flex; gap: 20px;' }, [ + // Podkop Status Panel + createStatusPanel('Podkop Status', podkopStatus, [ + podkopStatus.running ? + ButtonFactory.createActionButton({ + label: 'Stop Podkop', + type: 'remove', + action: 'stop', + reload: true + }) : + ButtonFactory.createActionButton({ + label: 'Start Podkop', + type: 'apply', + action: 'start', + reload: true + }), + ButtonFactory.createActionButton({ + label: 'Restart Podkop', + type: 'apply', + action: 'restart', + reload: true + }), + ButtonFactory.createActionButton({ + label: podkopStatus.enabled ? 'Disable Podkop' : 'Enable Podkop', + type: podkopStatus.enabled ? 'remove' : 'apply', + action: podkopStatus.enabled ? 'disable' : 'enable', + reload: true + }), + ButtonFactory.createModalButton({ + label: 'Show Config', + command: 'show_config', + title: 'Podkop Configuration' + }), + ButtonFactory.createModalButton({ + label: 'View Logs', + command: 'check_logs', + title: 'Podkop Logs' + }) + ]), + + // Sing-box Status Panel + createStatusPanel('Sing-box Status', singboxStatus, [ + ButtonFactory.createModalButton({ + label: 'Show Config', + command: 'show_sing_box_config', + title: 'Sing-box Configuration' + }), + ButtonFactory.createModalButton({ + label: 'View Logs', + command: 'check_sing_box_logs', + title: 'Sing-box Logs' + }), + ButtonFactory.createModalButton({ + label: 'Check Connections', + command: 'check_sing_box_connections', + title: 'Active Connections' + }) + ]), + + // FakeIP Status Panel with dynamic status + createStatusPanel('FakeIP Status', { + running: fakeipStatus.state === 'working', + status: fakeipStatus.message + }, [ + ButtonFactory.createModalButton({ + label: 'Check NFT Rules', + command: 'check_nft', + title: 'NFT Rules' + }), + ButtonFactory.createModalButton({ + label: 'Check DNSMasq', + command: 'check_dnsmasq', + title: 'DNSMasq Configuration' + }), + ButtonFactory.createModalButton({ + label: 'Update Lists', + command: 'list_update', + title: 'Lists Update Results' + }) + ]), + + // Version Information Panel + createStatusPanel('Version Information', null, [ + E('div', { 'style': 'margin-top: 10px; font-family: monospace; white-space: pre-wrap;' }, [ + E('strong', {}, 'Podkop: '), podkop.stdout ? podkop.stdout.trim() : _('Unknown'), '\n', + E('strong', {}, 'LuCI App: '), luci.stdout ? luci.stdout.trim() : _('Unknown'), '\n', + E('strong', {}, 'Sing-box: '), singbox.stdout ? singbox.stdout.trim() : _('Unknown'), '\n', + E('strong', {}, 'OpenWrt Version: '), system.stdout ? system.stdout.split('\n')[1].trim() : _('Unknown'), '\n', + E('strong', {}, 'Device Model: '), system.stdout ? system.stdout.split('\n')[4].trim() : _('Unknown') + ]) + ]) + ]) + ]); +}; + return view.extend({ async render() { - document.getElementsByTagName('head')[0].insertAdjacentHTML('beforeend', ` + document.head.insertAdjacentHTML('beforeend', ` @@ -476,13 +697,26 @@ return view.extend({ .cbi-value { margin-bottom: 10px !important; } + + #diagnostics-status .table > div { + background: var(--background-color-primary); + border: 1px solid var(--border-color-medium); + border-radius: var(--border-radius); + } + + #diagnostics-status .table > div pre, + #diagnostics-status .table > div div[style*="monospace"] { + color: var(--color-text-primary); + } + + #diagnostics-status .alert-message { + background: var(--background-color-primary); + border-color: var(--border-color-medium); + } `); - const m = new form.Map('podkop', _('Podkop configuration'), null, ['main', 'extra']); - safeExec('/etc/init.d/podkop', ['show_version']).then(res => { - if (res.stdout) m.title = _('Podkop') + ' v' + res.stdout.trim(); - }); + const m = new form.Map('podkop', _(''), null, ['main', 'extra']); // Main Section const mainSection = m.section(form.TypedSection, 'main'); @@ -635,367 +869,53 @@ return view.extend({ // Diagnostics Tab (main section) o = mainSection.tab('diagnostics', _('Diagnostics')); - let createStatusSection = function (podkopStatus, singboxStatus, podkop, luci, singbox, system) { - return E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('Service Status')), - E('div', { 'class': 'table', style: 'display: flex; gap: 20px;' }, [ - E('div', { 'style': 'flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;' }, [ - E('div', { 'style': 'margin-bottom: 15px;' }, [ - E('strong', {}, _('Podkop Status')), - E('br'), - E('span', { 'style': `color: ${podkopStatus.running ? '#4caf50' : '#f44336'}` }, [ - podkopStatus.running ? '✔' : '✘', - ' ', - podkopStatus.status - ]) - ]), - E('div', { 'class': 'btn-group', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [ - podkopStatus.running ? - E('button', { - 'class': 'btn cbi-button-remove', - 'click': () => safeExec('/etc/init.d/podkop', ['stop']).then(() => location.reload()) - }, _('Stop Podkop')) : - E('button', { - 'class': 'btn cbi-button-apply', - 'click': () => safeExec('/etc/init.d/podkop', ['start']).then(() => location.reload()) - }, _('Start Podkop')), - E('button', { - 'class': 'btn cbi-button-apply', - 'click': () => safeExec('/etc/init.d/podkop', ['restart']).then(() => location.reload()) - }, _('Restart Podkop')), - E('button', { - 'class': 'btn cbi-button-' + (podkopStatus.enabled ? 'remove' : 'apply'), - 'click': () => safeExec('/etc/init.d/podkop', [podkopStatus.enabled ? 'disable' : 'enable']).then(() => location.reload()) - }, podkopStatus.enabled ? _('Disable Podkop') : _('Enable Podkop')), - E('button', { - 'class': 'btn', - 'click': () => safeExec('/etc/init.d/podkop', ['show_config']).then(res => { - const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); - ui.showModal(_('Podkop Configuration'), [ - E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [ - E('pre', { style: 'margin: 0;' }, formattedOutput) - ]), - E('div', { style: 'display: flex; justify-content: space-between; margin-top: 1em;' }, [ - E('button', { - 'class': 'btn', - 'click': function (ev) { - const textarea = document.createElement('textarea'); - textarea.value = '```txt\n' + formattedOutput + '\n```'; - document.body.appendChild(textarea); - textarea.select(); - try { - document.execCommand('copy'); - ev.target.textContent = _('Copied!'); - setTimeout(() => ev.target.textContent = _('Copy to Clipboard'), 1000); - } catch (err) { - ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message)); - } - document.body.removeChild(textarea); - } - }, _('Copy to Clipboard')), - E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close')) - ]) - ]); - }) - }, _('Show Config')), - E('button', { - 'class': 'btn', - 'click': () => safeExec('/etc/init.d/podkop', ['check_logs']).then(res => { - const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); - ui.showModal(_('Podkop Logs'), [ - E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [ - E('pre', { style: 'margin: 0;' }, formattedOutput) - ]), - E('div', { style: 'display: flex; justify-content: space-between; margin-top: 1em;' }, [ - E('button', { - 'class': 'btn', - 'click': function (ev) { - const textarea = document.createElement('textarea'); - textarea.value = '```txt\n' + formattedOutput + '\n```'; - document.body.appendChild(textarea); - textarea.select(); - try { - document.execCommand('copy'); - ev.target.textContent = _('Copied!'); - setTimeout(() => ev.target.textContent = _('Copy to Clipboard'), 1000); - } catch (err) { - ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message)); - } - document.body.removeChild(textarea); - } - }, _('Copy to Clipboard')), - E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close')) - ]) - ]); - }) - }, _('View Logs')) - ]) - ]), - E('div', { 'style': 'flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;' }, [ - E('div', { 'style': 'margin-bottom: 15px;' }, [ - E('strong', {}, _('Sing-box Status')), - E('br'), - E('span', { 'style': `color: ${singboxStatus.running ? '#4caf50' : '#f44336'}` }, [ - singboxStatus.running ? '✔' : '✘', - ' ', - `${singboxStatus.status}` - ]) - ]), - E('div', { 'class': 'btn-group', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [ - E('button', { - 'class': 'btn', - 'click': () => safeExec('/etc/init.d/podkop', ['show_sing_box_config']).then(res => { - const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); - ui.showModal(_('Sing-box Configuration'), [ - E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [ - E('pre', { style: 'margin: 0;' }, formattedOutput) - ]), - E('div', { style: 'display: flex; justify-content: space-between; margin-top: 1em;' }, [ - E('button', { - 'class': 'btn', - 'click': function (ev) { - const textarea = document.createElement('textarea'); - textarea.value = '```txt\n' + formattedOutput + '\n```'; - document.body.appendChild(textarea); - textarea.select(); - try { - document.execCommand('copy'); - ev.target.textContent = _('Copied!'); - setTimeout(() => ev.target.textContent = _('Copy to Clipboard'), 1000); - } catch (err) { - ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message)); - } - document.body.removeChild(textarea); - } - }, _('Copy to Clipboard')), - E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close')) - ]) - ]); - }) - }, _('Show Config')), - E('button', { - 'class': 'btn', - 'click': () => safeExec('/etc/init.d/podkop', ['check_sing_box_logs']).then(res => { - const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); - ui.showModal(_('Sing-box Logs'), [ - E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [ - E('pre', { style: 'margin: 0;' }, formattedOutput) - ]), - E('div', { style: 'display: flex; justify-content: space-between; margin-top: 1em;' }, [ - E('button', { - 'class': 'btn', - 'click': function (ev) { - const textarea = document.createElement('textarea'); - textarea.value = '```txt\n' + formattedOutput + '\n```'; - document.body.appendChild(textarea); - textarea.select(); - try { - document.execCommand('copy'); - ev.target.textContent = _('Copied!'); - setTimeout(() => ev.target.textContent = _('Copy to Clipboard'), 1000); - } catch (err) { - ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message)); - } - document.body.removeChild(textarea); - } - }, _('Copy to Clipboard')), - E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close')) - ]) - ]); - }) - }, _('View Logs')), - E('button', { - 'class': 'btn', - 'click': () => safeExec('/etc/init.d/podkop', ['check_sing_box_connections']).then(res => { - const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); - ui.showModal(_('Active Connections'), [ - E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [ - E('pre', { style: 'margin: 0;' }, formattedOutput) - ]), - E('div', { style: 'display: flex; justify-content: space-between; margin-top: 1em;' }, [ - E('button', { - 'class': 'btn', - 'click': function (ev) { - const textarea = document.createElement('textarea'); - textarea.value = '```txt\n' + formattedOutput + '\n```'; - document.body.appendChild(textarea); - textarea.select(); - try { - document.execCommand('copy'); - ev.target.textContent = _('Copied!'); - setTimeout(() => ev.target.textContent = _('Copy to Clipboard'), 1000); - } catch (err) { - ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message)); - } - document.body.removeChild(textarea); - } - }, _('Copy to Clipboard')), - E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close')) - ]) - ]); - }) - }, _('Check Connections')) - ]) - ]), - E('div', { 'style': 'flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;' }, [ - E('div', { 'style': 'margin-bottom: 15px;' }, [ - E('strong', {}, _('FakeIP Status')), - E('div', { 'id': 'fakeip-status' }, [E('span', {}, _('Checking FakeIP...'))]) - ]), - E('div', { 'class': 'btn-group', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [ - E('button', { - 'class': 'btn', - 'click': () => safeExec('/etc/init.d/podkop', ['check_nft']).then(res => { - const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); - ui.showModal(_('NFT Rules'), [ - E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [ - E('pre', { style: 'margin: 0;' }, formattedOutput) - ]), - E('div', { style: 'display: flex; justify-content: space-between; margin-top: 1em;' }, [ - E('button', { - 'class': 'btn', - 'click': function (ev) { - const textarea = document.createElement('textarea'); - textarea.value = '```txt\n' + formattedOutput + '\n```'; - document.body.appendChild(textarea); - textarea.select(); - try { - document.execCommand('copy'); - ev.target.textContent = _('Copied!'); - setTimeout(() => ev.target.textContent = _('Copy to Clipboard'), 1000); - } catch (err) { - ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message)); - } - document.body.removeChild(textarea); - } - }, _('Copy to Clipboard')), - E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close')) - ]) - ]); - }) - }, _('Check NFT Rules')), - E('button', { - 'class': 'btn', - 'click': () => safeExec('/etc/init.d/podkop', ['check_dnsmasq']).then(res => { - const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); - ui.showModal(_('DNSMasq Configuration'), [ - E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [ - E('pre', { style: 'margin: 0;' }, formattedOutput) - ]), - E('div', { style: 'display: flex; justify-content: space-between; margin-top: 1em;' }, [ - E('button', { - 'class': 'btn', - 'click': function (ev) { - const textarea = document.createElement('textarea'); - textarea.value = '```txt\n' + formattedOutput + '\n```'; - document.body.appendChild(textarea); - textarea.select(); - try { - document.execCommand('copy'); - ev.target.textContent = _('Copied!'); - setTimeout(() => ev.target.textContent = _('Copy to Clipboard'), 1000); - } catch (err) { - ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message)); - } - document.body.removeChild(textarea); - } - }, _('Copy to Clipboard')), - E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close')) - ]) - ]); - }) - }, _('Check DNSMasq')), - E('button', { - 'class': 'btn', - 'click': () => safeExec('/etc/init.d/podkop', ['list_update']).then(res => { - const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); - ui.showModal(_('Lists Update Results'), [ - E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [ - E('pre', { style: 'margin: 0;' }, formattedOutput) - ]), - E('div', { style: 'display: flex; justify-content: space-between; margin-top: 1em;' }, [ - E('button', { - 'class': 'btn', - 'click': function (ev) { - const textarea = document.createElement('textarea'); - textarea.value = '```txt\n' + formattedOutput + '\n```'; - document.body.appendChild(textarea); - textarea.select(); - try { - document.execCommand('copy'); - ev.target.textContent = _('Copied!'); - setTimeout(() => ev.target.textContent = _('Copy to Clipboard'), 1000); - } catch (err) { - ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message)); - } - document.body.removeChild(textarea); - } - }, _('Copy to Clipboard')), - E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close')) - ]) - ]); - }) - }, _('Update Lists')) - ]) - ]), - E('div', { 'style': 'flex: 1; padding: 15px; background: #f8f9fa; border-radius: 8px;' }, [ - E('div', { 'style': 'margin-bottom: 15px;' }, [ - E('strong', {}, _('Version Information')), - E('br'), - E('div', { 'style': 'margin-top: 10px; font-family: monospace; white-space: pre-wrap;' }, [ - E('strong', {}, 'Podkop: '), podkop.stdout ? podkop.stdout.trim() : _('Unknown'), '\n', - E('strong', {}, 'LuCI App: '), luci.stdout ? luci.stdout.trim() : _('Unknown'), '\n', - E('strong', {}, 'Sing-box: '), singbox.stdout ? singbox.stdout.trim() : _('Unknown'), '\n', - E('strong', {}, 'OpenWrt Version: '), system.stdout ? system.stdout.split('\n')[1].trim() : _('Unknown'), '\n', - E('strong', {}, 'Device Model: '), system.stdout ? system.stdout.split('\n')[4].trim() : _('Unknown') - ]) - ]) - ]) - ]) - ]); - }; - o = mainSection.taboption('diagnostics', form.DummyValue, '_status'); o.rawhtml = true; o.cfgvalue = () => E('div', { id: 'diagnostics-status' }, _('Loading diagnostics...')); function checkFakeIP() { - return new Promise((resolve) => { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); + const createStatus = (state, message, color) => ({ + state, + message: _(message), + color: STATUS_COLORS[color] + }); - fetch('http://httpbin.org/ip', { signal: controller.signal }) - .then(response => response.text()) - .then(text => { + return new Promise(async (resolve) => { + try { + const singboxStatusResult = await safeExec('/etc/init.d/podkop', ['get_sing_box_status']); + const singboxStatus = JSON.parse(singboxStatusResult.stdout || '{"running":0,"dns_configured":0}'); + + if (!singboxStatus.running) { + return resolve(createStatus('not_working', 'sing-box not running', 'ERROR')); + } + if (!singboxStatus.dns_configured) { + return resolve(createStatus('not_working', 'DNS not configured', 'ERROR')); + } + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); + + try { + const response = await fetch('http://httpbin.org/ip', { signal: controller.signal }); + const text = await response.text(); clearTimeout(timeoutId); - let status = { - state: 'unknown', - message: '', - color: '#ff9800' - }; if (text.includes('Cannot GET /ip')) { - status.state = 'working'; - status.message = _('working'); - status.color = '#4caf50'; - } else if (text.includes('"origin":')) { - status.state = 'not_working'; - status.message = _('not working'); - status.color = '#f44336'; - } else { - status.state = 'error'; - status.message = _('check error'); + return resolve(createStatus('working', 'working', 'SUCCESS')); } - resolve(status); - }) - .catch(error => { + if (text.includes('"origin":')) { + return resolve(createStatus('not_working', 'not working', 'ERROR')); + } + return resolve(createStatus('error', 'check error', 'WARNING')); + } catch (fetchError) { clearTimeout(timeoutId); - resolve({ - state: 'error', - message: error.name === 'AbortError' ? _('timeout') : _('check error'), - color: '#ff9800' - }); - }); + const message = fetchError.name === 'AbortError' ? 'timeout' : 'check error'; + return resolve(createStatus('error', message, 'WARNING')); + } + } catch (error) { + console.error('Error in checkFakeIP:', error); + return resolve(createStatus('error', 'check error', 'WARNING')); + } }); } @@ -1025,11 +945,10 @@ return view.extend({ const container = document.getElementById('diagnostics-status'); if (!container) return; - const statusSection = createStatusSection(parsedPodkopStatus, parsedSingboxStatus, podkop, luci, singbox, system); + const statusSection = createStatusSection(parsedPodkopStatus, parsedSingboxStatus, podkop, luci, singbox, system, fakeipStatus); container.innerHTML = ''; container.appendChild(statusSection); - // Update FakeIP status const fakeipElement = document.getElementById('fakeip-status'); if (fakeipElement) { fakeipElement.innerHTML = E('span', { 'style': `color: ${fakeipStatus.color}` }, [ @@ -1050,11 +969,59 @@ return view.extend({ } } - // Start periodic updates - function startPeriodicUpdates() { - updateDiagnostics(); - const intervalId = setInterval(updateDiagnostics, 10000); - window.addEventListener('unload', () => clearInterval(intervalId)); + function startPeriodicUpdates(titleDiv) { + let updateTimer = null; + let isVisible = !document.hidden; + let versionText = _('Podkop'); + let versionReceived = false; + + const updateStatus = async () => { + try { + if (!versionReceived) { + const version = await safeExec('/etc/init.d/podkop', ['show_version'], 2000); + if (version.stdout) { + versionText = _('Podkop') + ' v' + version.stdout.trim(); + versionReceived = true; + } + } + + const singboxStatusResult = await safeExec('/etc/init.d/podkop', ['get_sing_box_status']); + const singboxStatus = JSON.parse(singboxStatusResult.stdout || '{"running":0,"dns_configured":0}'); + const fakeipStatus = await checkFakeIP(); + + titleDiv.textContent = versionText + (!singboxStatus.running || !singboxStatus.dns_configured === 'not_working' ? ' (not working)' : ''); + + await updateDiagnostics(); + } catch (error) { + console.warn('Failed to update status:', error); + titleDiv.textContent = versionText + ' (not working)'; + } + }; + + const toggleUpdates = (visible) => { + if (visible) { + updateStatus(); + if (!updateTimer) { + updateTimer = setInterval(updateStatus, 10000); + } + } else if (updateTimer) { + clearInterval(updateTimer); + updateTimer = null; + } + }; + + document.addEventListener('visibilitychange', () => { + isVisible = !document.hidden; + toggleUpdates(isVisible); + }); + + toggleUpdates(isVisible); + + window.addEventListener('unload', () => { + if (updateTimer) { + clearInterval(updateTimer); + } + }); } // Extra Section @@ -1065,13 +1032,15 @@ return view.extend({ extraSection.multiple = true; createConfigSection(extraSection, m, network); - const map_promise = m.render(); - map_promise.then(node => { + const map_promise = m.render().then(node => { + const titleDiv = E('h2', { 'class': 'cbi-map-title' }, _('Podkop')); + node.insertBefore(titleDiv, node.firstChild); + node.classList.add('fade-in'); - startPeriodicUpdates(); + startPeriodicUpdates(titleDiv); return node; }); return map_promise; } -}); \ No newline at end of file +}); diff --git a/luci-app-podkop/po/ru/podkop.po b/luci-app-podkop/po/ru/podkop.po index dd32b14..a3631f3 100644 --- a/luci-app-podkop/po/ru/podkop.po +++ b/luci-app-podkop/po/ru/podkop.po @@ -554,4 +554,172 @@ msgid "Path must contain at least one directory (like /tmp/cache.db)" msgstr "Путь должен содержать хотя бы одну директорию (например /tmp/cache.db)" msgid "Invalid path format. Must be like /tmp/cache.db" -msgstr "Неверный формат пути. Пример: /tmp/cache.db" \ No newline at end of file +msgstr "Неверный формат пути. Пример: /tmp/cache.db" + +msgid "Copy to Clipboard" +msgstr "Копировать в буфер обмена" + +msgid "Close" +msgstr "Закрыть" + +msgid "Loading..." +msgstr "Загрузка..." + +msgid "Loading version information..." +msgstr "Загрузка информации о версии..." + +msgid "Checking FakeIP..." +msgstr "Проверка FakeIP..." + +msgid "timeout" +msgstr "таймаут" + +msgid "Current config: " +msgstr "Текущая конфигурация: " + +msgid "Invalid VLESS URL: type must be one of tcp, udp, grpc, http" +msgstr "Неверный URL VLESS: тип должен быть одним из tcp, udp, grpc, http" + +msgid "Invalid VLESS URL: security must be one of tls, reality, none" +msgstr "Неверный URL VLESS: security должен быть одним из tls, reality, none" + +msgid "Podkop" +msgstr "Podkop" + +msgid "Proxy" +msgstr "Прокси" + +msgid "VPN" +msgstr "VPN" + +msgid "http://openwrt.lan:9090/ui" +msgstr "http://openwrt.lan:9090/ui" + +msgid "Podkop Configuration" +msgstr "Конфигурация Podkop" + +msgid "Active Connections" +msgstr "Активные соединения" + +msgid "DNSMasq Configuration" +msgstr "Конфигурация DNSMasq" + +msgid "Sing-box Configuration" +msgstr "Конфигурация Sing-box" + +msgid "Extra configurations" +msgstr "Дополнительные конфигурации" + +msgid "Add Section" +msgstr "Добавить раздел" + +msgid "No output" +msgstr "Нет вывода" + +msgid "Failed to copy: " +msgstr "Не удалось скопировать: " + +msgid "Show Config" +msgstr "Показать конфигурацию" + +msgid "View Logs" +msgstr "Просмотр логов" + +msgid "Check Connections" +msgstr "Проверить соединения" + +msgid "FakeIP Status" +msgstr "Статус FakeIP" + +msgid "Device Model: " +msgstr "Модель устройства: " + +msgid "OpenWrt Version: " +msgstr "Версия OpenWrt: " + +msgid "Check DNSMasq" +msgstr "Проверить DNSMasq" + +msgid "Check NFT Rules" +msgstr "Проверить правила NFT" + +msgid "Update Lists" +msgstr "Обновить списки" + +msgid "Lists Update Results" +msgstr "Результаты обновления списков" + +msgid "NFT Rules" +msgstr "Правила NFT" + +msgid "GitHub Connectivity" +msgstr "Подключение к GitHub" + +msgid "Check GitHub" +msgstr "Проверить GitHub" + +msgid "GitHub Connectivity Results" +msgstr "Результаты проверки подключения к GitHub" + +msgid "Sing-Box Logs" +msgstr "Логи Sing-Box" + +msgid "View recent sing-box logs from system journal" +msgstr "Просмотр последних логов sing-box из системного журнала" + +msgid "View Sing-Box Logs" +msgstr "Просмотр логов Sing-Box" + +msgid "Podkop Logs" +msgstr "Логи Podkop" + +msgid "View recent podkop logs from system journal" +msgstr "Просмотр последних логов podkop из системного журнала" + +msgid "View Podkop Logs" +msgstr "Просмотр логов Podkop" + +msgid "Active Connections" +msgstr "Активные соединения" + +msgid "View active sing-box network connections" +msgstr "Просмотр активных сетевых подключений sing-box" + +msgid "DNSMasq Configuration" +msgstr "Конфигурация DNSMasq" + +msgid "View current DNSMasq configuration settings" +msgstr "Просмотр текущих настроек конфигурации DNSMasq" + +msgid "Sing-Box Configuration" +msgstr "Конфигурация Sing-Box" + +msgid "Show current sing-box configuration" +msgstr "Показать текущую конфигурацию sing-box" + +msgid "Show Sing-Box Config" +msgstr "Показать конфигурацию Sing-Box" + +msgid "Diagnostic Tools" +msgstr "Инструменты диагностики" + +msgid "Unknown" +msgstr "Неизвестно" + +msgid "sing-box not running" +msgstr "sing-box не запущен" + +msgid "DNS not configured" +msgstr "DNS не настроен" + +msgid "running & enabled" +msgstr "запущен и активирован" + +msgid "running but disabled" +msgstr "запущен, но деактивирован" + +msgid "stopped but enabled" +msgstr "остановлен, но активирован" + +msgid "stopped & disabled" +msgstr "остановлен и деактивирован" \ No newline at end of file diff --git a/luci-app-podkop/po/templates/podkop.pot b/luci-app-podkop/po/templates/podkop.pot index 28cb66f..3e8a711 100644 --- a/luci-app-podkop/po/templates/podkop.pot +++ b/luci-app-podkop/po/templates/podkop.pot @@ -908,4 +908,172 @@ msgid "Path must contain at least one directory (like /tmp/cache.db)" msgstr "" msgid "Invalid path format. Must be like /tmp/cache.db" +msgstr "" + +msgid "Copy to Clipboard" +msgstr "" + +msgid "Close" +msgstr "" + +msgid "Loading..." +msgstr "" + +msgid "Loading version information..." +msgstr "" + +msgid "Checking FakeIP..." +msgstr "" + +msgid "timeout" +msgstr "" + +msgid "Current config: " +msgstr "" + +msgid "Invalid VLESS URL: type must be one of tcp, udp, grpc, http" +msgstr "" + +msgid "Invalid VLESS URL: security must be one of tls, reality, none" +msgstr "" + +msgid "Podkop" +msgstr "" + +msgid "Proxy" +msgstr "" + +msgid "VPN" +msgstr "" + +msgid "http://openwrt.lan:9090/ui" +msgstr "" + +msgid "Podkop Configuration" +msgstr "" + +msgid "Active Connections" +msgstr "" + +msgid "DNSMasq Configuration" +msgstr "" + +msgid "Sing-box Configuration" +msgstr "" + +msgid "Extra configurations" +msgstr "" + +msgid "Add Section" +msgstr "" + +msgid "No output" +msgstr "" + +msgid "Failed to copy: " +msgstr "" + +msgid "Show Config" +msgstr "" + +msgid "View Logs" +msgstr "" + +msgid "Check Connections" +msgstr "" + +msgid "FakeIP Status" +msgstr "" + +msgid "Device Model: " +msgstr "" + +msgid "OpenWrt Version: " +msgstr "" + +msgid "Check DNSMasq" +msgstr "" + +msgid "Check NFT Rules" +msgstr "" + +msgid "Update Lists" +msgstr "" + +msgid "Lists Update Results" +msgstr "" + +msgid "NFT Rules" +msgstr "" + +msgid "GitHub Connectivity" +msgstr "" + +msgid "Check GitHub" +msgstr "" + +msgid "GitHub Connectivity Results" +msgstr "" + +msgid "Sing-Box Logs" +msgstr "" + +msgid "View recent sing-box logs from system journal" +msgstr "" + +msgid "View Sing-Box Logs" +msgstr "" + +msgid "Podkop Logs" +msgstr "" + +msgid "View recent podkop logs from system journal" +msgstr "" + +msgid "View Podkop Logs" +msgstr "" + +msgid "Active Connections" +msgstr "" + +msgid "View active sing-box network connections" +msgstr "" + +msgid "DNSMasq Configuration" +msgstr "" + +msgid "View current DNSMasq configuration settings" +msgstr "" + +msgid "Sing-Box Configuration" +msgstr "" + +msgid "Show current sing-box configuration" +msgstr "" + +msgid "Show Sing-Box Config" +msgstr "" + +msgid "Diagnostic Tools" +msgstr "" + +msgid "Unknown" +msgstr "" + +msgid "sing-box not running" +msgstr "" + +msgid "DNS not configured" +msgstr "" + +msgid "running & enabled" +msgstr "" + +msgid "running but disabled" +msgstr "" + +msgid "stopped but enabled" +msgstr "" + +msgid "stopped & disabled" msgstr "" \ No newline at end of file diff --git a/podkop/files/etc/init.d/podkop b/podkop/files/etc/init.d/podkop index 21cd8ce..6cae6df 100755 --- a/podkop/files/etc/init.d/podkop +++ b/podkop/files/etc/init.d/podkop @@ -7,7 +7,7 @@ script=$(readlink "$initscript") NAME="$(basename ${script:-$initscript})" config_load "$NAME" -EXTRA_COMMANDS="main list_update check_proxy check_nft check_github check_logs check_sing_box_connections check_sing_box_logs check_dnsmasq show_config show_version show_sing_box_config show_luci_version show_sing_box_version show_system_info get_status get_sing_box_status get_proxy_label" +EXTRA_COMMANDS="main list_update check_proxy check_nft check_github check_logs check_sing_box_connections check_sing_box_logs check_dnsmasq show_config show_version show_sing_box_config show_luci_version show_sing_box_version show_system_info get_status get_sing_box_status" EXTRA_HELP=" list_update Updating domain and subnet lists check_proxy Check if sing-box proxy works correctly check_nft Show PodkopTable nftables rules @@ -1780,6 +1780,7 @@ get_sing_box_status() { local enabled=0 local status="" local version="" + local dns_configured=0 # Check if service is enabled if [ -x /etc/rc.d/S99sing-box ]; then @@ -1792,6 +1793,12 @@ get_sing_box_status() { version=$(sing-box version | head -n 1 | awk '{print $3}') fi + # Check DNS configuration + local dns_server=$(uci get dhcp.@dnsmasq[0].server 2>/dev/null) + if [ "$dns_server" = "127.0.0.42" ]; then + dns_configured=1 + fi + # Format status message if [ $running -eq 1 ]; then if [ $enabled -eq 1 ]; then @@ -1807,7 +1814,7 @@ get_sing_box_status() { fi fi - echo "{\"running\":$running,\"enabled\":$enabled,\"status\":\"$status\"}" + echo "{\"running\":$running,\"enabled\":$enabled,\"status\":\"$status\",\"dns_configured\":$dns_configured}" } get_status() { @@ -1821,7 +1828,7 @@ get_status() { fi # Check if service is running - if pgrep -f "podkop" >/dev/null; then + if pgrep -f "sing-box" >/dev/null; then running=1 fi @@ -1843,18 +1850,6 @@ get_status() { echo "{\"running\":$running,\"enabled\":$enabled,\"status\":\"$status\"}" } -get_proxy_label() { - local section="$1" - local proxy_string - local label="" - - config_get proxy_string "$section" "proxy_string" - if [ -n "$proxy_string" ]; then - label=$(echo "$proxy_string" | sed -n 's/.*#\(.*\)$/\1/p') - echo "$label" - fi -} - sing_box_add_secure_dns_probe_domain() { local domain="httpbin.org" local override_address="numbersapi.com"