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"